1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
8 * @fileoverview The event page for Google Now for Chrome implementation.
9 * The Google Now event page gets Google Now cards from the server and shows
10 * them as Chrome notifications.
11 * The service performs periodic updating of Google Now cards.
12 * Each updating of the cards includes 4 steps:
13 * 1. Processing requests for cards dismissals that are not yet sent to the
15 * 2. Making a server request.
16 * 3. Showing the received cards as notifications.
19 // TODO(robliao): Decide what to do in incognito mode.
22 * Standard response code for successful HTTP requests. This is the only success
23 * code the server will send.
26 var HTTP_NOCONTENT = 204;
28 var HTTP_BAD_REQUEST = 400;
29 var HTTP_UNAUTHORIZED = 401;
30 var HTTP_FORBIDDEN = 403;
31 var HTTP_METHOD_NOT_ALLOWED = 405;
33 var MS_IN_SECOND = 1000;
34 var MS_IN_MINUTE = 60 * 1000;
37 * Initial period for polling for Google Now Notifications cards to use when the
38 * period from the server is not available.
40 var INITIAL_POLLING_PERIOD_SECONDS = 5 * 60; // 5 minutes
43 * Mininal period for polling for Google Now Notifications cards.
45 var MINIMUM_POLLING_PERIOD_SECONDS = 5 * 60; // 5 minutes
48 * Maximal period for polling for Google Now Notifications cards to use when the
49 * period from the server is not available.
51 var MAXIMUM_POLLING_PERIOD_SECONDS = 30 * 60; // 30 minutes
54 * Initial period for polling for Google Now optin notification after push
55 * messaging indicates Google Now is enabled.
57 var INITIAL_OPTIN_RECHECK_PERIOD_SECONDS = 60; // 1 minute
60 * Maximum period for polling for Google Now optin notification after push
61 * messaging indicates Google Now is enabled. It is expected that the alarm
62 * will be stopped after this.
64 var MAXIMUM_OPTIN_RECHECK_PERIOD_SECONDS = 16 * 60; // 16 minutes
67 * Initial period for retrying the server request for dismissing cards.
69 var INITIAL_RETRY_DISMISS_PERIOD_SECONDS = 60; // 1 minute
72 * Maximum period for retrying the server request for dismissing cards.
74 var MAXIMUM_RETRY_DISMISS_PERIOD_SECONDS = 60 * 60; // 1 hour
77 * Time we keep retrying dismissals.
79 var MAXIMUM_DISMISSAL_AGE_MS = 24 * 60 * 60 * 1000; // 1 day
82 * Time we keep dismissals after successful server dismiss requests.
84 var DISMISS_RETENTION_TIME_MS = 20 * 60 * 1000; // 20 minutes
87 * Default period for checking whether the user is opted in to Google Now.
89 var DEFAULT_OPTIN_CHECK_PERIOD_SECONDS = 60 * 60 * 24 * 7; // 1 week
92 * URL to open when the user clicked on a link for the our notification
95 var SETTINGS_URL = 'https://support.google.com/chrome/?p=ib_google_now_welcome';
98 * Number of cards that need an explanatory link.
100 var EXPLANATORY_CARDS_LINK_THRESHOLD = 4;
103 * Names for tasks that can be created by the extension.
105 var UPDATE_CARDS_TASK_NAME = 'update-cards';
106 var DISMISS_CARD_TASK_NAME = 'dismiss-card';
107 var RETRY_DISMISS_TASK_NAME = 'retry-dismiss';
108 var STATE_CHANGED_TASK_NAME = 'state-changed';
109 var SHOW_ON_START_TASK_NAME = 'show-cards-on-start';
110 var ON_PUSH_MESSAGE_START_TASK_NAME = 'on-push-message';
113 * Group as received from the server.
116 * nextPollSeconds: (string|undefined),
117 * rank: (number|undefined),
118 * requested: (boolean|undefined)
124 * Server response with notifications and groups.
127 * googleNowDisabled: (boolean|undefined),
128 * groups: Object.<string, ReceivedGroup>,
129 * notifications: Array.<ReceivedNotification>
135 * Notification group as the client stores it. |cardsTimestamp| and |rank| are
136 * defined if |cards| is non-empty. |nextPollTime| is undefined if the server
137 * (1) never sent 'nextPollSeconds' for the group or
138 * (2) didn't send 'nextPollSeconds' with the last group update containing a
139 * cards update and all the times after that.
142 * cards: Array.<ReceivedNotification>,
143 * cardsTimestamp: (number|undefined),
144 * nextPollTime: (number|undefined),
145 * rank: (number|undefined)
148 var StoredNotificationGroup;
151 * Pending (not yet successfully sent) dismissal for a received notification.
152 * |time| is the moment when the user requested dismissal.
155 * chromeNotificationId: ChromeNotificationId,
157 * dismissalData: DismissalData
160 var PendingDismissal;
163 * Checks if a new task can't be scheduled when another task is already
165 * @param {string} newTaskName Name of the new task.
166 * @param {string} scheduledTaskName Name of the scheduled task.
167 * @return {boolean} Whether the new task conflicts with the existing task.
169 function areTasksConflicting(newTaskName, scheduledTaskName) {
170 if (newTaskName == UPDATE_CARDS_TASK_NAME &&
171 scheduledTaskName == UPDATE_CARDS_TASK_NAME) {
172 // If a card update is requested while an old update is still scheduled, we
173 // don't need the new update.
177 if (newTaskName == RETRY_DISMISS_TASK_NAME &&
178 (scheduledTaskName == UPDATE_CARDS_TASK_NAME ||
179 scheduledTaskName == DISMISS_CARD_TASK_NAME ||
180 scheduledTaskName == RETRY_DISMISS_TASK_NAME)) {
181 // No need to schedule retry-dismiss action if another action that tries to
182 // send dismissals is scheduled.
189 var tasks = buildTaskManager(areTasksConflicting);
191 // Add error processing to API calls.
192 wrapper.instrumentChromeApiFunction('metricsPrivate.getVariationParams', 1);
193 wrapper.instrumentChromeApiFunction('notifications.clear', 1);
194 wrapper.instrumentChromeApiFunction('notifications.create', 2);
195 wrapper.instrumentChromeApiFunction('notifications.getPermissionLevel', 0);
196 wrapper.instrumentChromeApiFunction('notifications.update', 2);
197 wrapper.instrumentChromeApiFunction('notifications.getAll', 0);
198 wrapper.instrumentChromeApiFunction(
199 'notifications.onButtonClicked.addListener', 0);
200 wrapper.instrumentChromeApiFunction('notifications.onClicked.addListener', 0);
201 wrapper.instrumentChromeApiFunction('notifications.onClosed.addListener', 0);
202 wrapper.instrumentChromeApiFunction(
203 'notifications.onPermissionLevelChanged.addListener', 0);
204 wrapper.instrumentChromeApiFunction(
205 'notifications.onShowSettings.addListener', 0);
206 wrapper.instrumentChromeApiFunction('permissions.contains', 1);
207 wrapper.instrumentChromeApiFunction('pushMessaging.onMessage.addListener', 0);
208 wrapper.instrumentChromeApiFunction('storage.onChanged.addListener', 0);
209 wrapper.instrumentChromeApiFunction('runtime.onInstalled.addListener', 0);
210 wrapper.instrumentChromeApiFunction('runtime.onStartup.addListener', 0);
211 wrapper.instrumentChromeApiFunction('tabs.create', 1);
213 var updateCardsAttempts = buildAttemptManager(
216 INITIAL_POLLING_PERIOD_SECONDS,
217 MAXIMUM_POLLING_PERIOD_SECONDS);
218 var optInPollAttempts = buildAttemptManager(
220 pollOptedInNoImmediateRecheck,
221 INITIAL_POLLING_PERIOD_SECONDS,
222 MAXIMUM_POLLING_PERIOD_SECONDS);
223 var optInRecheckAttempts = buildAttemptManager(
225 pollOptedInWithRecheck,
226 INITIAL_OPTIN_RECHECK_PERIOD_SECONDS,
227 MAXIMUM_OPTIN_RECHECK_PERIOD_SECONDS);
228 var dismissalAttempts = buildAttemptManager(
230 retryPendingDismissals,
231 INITIAL_RETRY_DISMISS_PERIOD_SECONDS,
232 MAXIMUM_RETRY_DISMISS_PERIOD_SECONDS);
233 var cardSet = buildCardSet();
235 var authenticationManager = buildAuthenticationManager();
238 * Google Now UMA event identifier.
241 var GoogleNowEvent = {
242 REQUEST_FOR_CARDS_TOTAL: 0,
243 REQUEST_FOR_CARDS_SUCCESS: 1,
244 CARDS_PARSE_SUCCESS: 2,
245 DISMISS_REQUEST_TOTAL: 3,
246 DISMISS_REQUEST_SUCCESS: 4,
248 DELETED_LOCATION_UPDATE: 6,
250 DELETED_SHOW_WELCOME_TOAST: 8,
252 DELETED_USER_SUPPRESSED: 10,
254 NOTIFICATION_DISABLED: 12,
255 GOOGLE_NOW_DISABLED: 13,
256 EVENTS_TOTAL: 14 // EVENTS_TOTAL is not an event; all new events need to be
261 * Records a Google Now Event.
262 * @param {GoogleNowEvent} event Event identifier.
264 function recordEvent(event) {
265 var metricDescription = {
266 metricName: 'GoogleNow.Event',
267 type: 'histogram-linear',
269 max: GoogleNowEvent.EVENTS_TOTAL,
270 buckets: GoogleNowEvent.EVENTS_TOTAL + 1
273 chrome.metricsPrivate.recordValue(metricDescription, event);
277 * Records a notification clicked event.
278 * @param {number|undefined} cardTypeId Card type ID.
280 function recordNotificationClick(cardTypeId) {
281 if (cardTypeId !== undefined) {
282 chrome.metricsPrivate.recordSparseValue(
283 'GoogleNow.Card.Clicked', cardTypeId);
288 * Records a button clicked event.
289 * @param {number|undefined} cardTypeId Card type ID.
290 * @param {number} buttonIndex Button Index
292 function recordButtonClick(cardTypeId, buttonIndex) {
293 if (cardTypeId !== undefined) {
294 chrome.metricsPrivate.recordSparseValue(
295 'GoogleNow.Card.Button.Clicked' + buttonIndex, cardTypeId);
300 * Checks the result of the HTTP Request and updates the authentication
301 * manager on any failure.
302 * @param {string} token Authentication token to validate against an
304 * @return {function(XMLHttpRequest)} Function that validates the token with the
305 * supplied XMLHttpRequest.
307 function checkAuthenticationStatus(token) {
308 return function(request) {
309 if (request.status == HTTP_FORBIDDEN ||
310 request.status == HTTP_UNAUTHORIZED) {
311 authenticationManager.removeToken(token);
317 * Builds and sends an authenticated request to the notification server.
318 * @param {string} method Request method.
319 * @param {string} handlerName Server handler to send the request to.
320 * @param {string=} opt_contentType Value for the Content-type header.
321 * @return {Promise} A promise to issue a request to the server.
322 * The promise rejects if the response is not within the HTTP 200 range.
324 function requestFromServer(method, handlerName, opt_contentType) {
325 return authenticationManager.getAuthToken().then(function(token) {
326 var request = buildServerRequest(method, handlerName, opt_contentType);
327 request.setRequestHeader('Authorization', 'Bearer ' + token);
328 var requestPromise = new Promise(function(resolve, reject) {
329 request.addEventListener('loadend', function() {
330 if ((200 <= request.status) && (request.status < 300)) {
338 requestPromise.catch(checkAuthenticationStatus(token));
339 return requestPromise;
344 * Shows the notification groups as notification cards.
345 * @param {Object.<string, StoredNotificationGroup>} notificationGroups Map from
346 * group name to group information.
347 * @param {function(ReceivedNotification)=} opt_onCardShown Optional parameter
348 * called when each card is shown.
349 * @return {Promise} A promise to show the notification groups as cards.
351 function showNotificationGroups(notificationGroups, opt_onCardShown) {
352 /** @type {Object.<ChromeNotificationId, CombinedCard>} */
353 var cards = combineCardsFromGroups(notificationGroups);
354 console.log('showNotificationGroups ' + JSON.stringify(cards));
356 return new Promise(function(resolve) {
357 instrumented.notifications.getAll(function(notifications) {
358 console.log('showNotificationGroups-getAll ' +
359 JSON.stringify(notifications));
360 notifications = notifications || {};
362 // Mark notifications that didn't receive an update as having received
364 for (var chromeNotificationId in notifications) {
365 cards[chromeNotificationId] = cards[chromeNotificationId] || [];
368 /** @type {Object.<ChromeNotificationId, NotificationDataEntry>} */
369 var notificationsData = {};
371 // Create/update/delete notifications.
372 for (var chromeNotificationId in cards) {
373 notificationsData[chromeNotificationId] = cardSet.update(
374 chromeNotificationId,
375 cards[chromeNotificationId],
379 chrome.storage.local.set({notificationsData: notificationsData});
386 * Removes all cards and card state on Google Now close down.
388 function removeAllCards() {
389 console.log('removeAllCards');
391 // TODO(robliao): Once Google Now clears its own checkbox in the
392 // notifications center and bug 260376 is fixed, the below clearing
393 // code is no longer necessary.
394 instrumented.notifications.getAll(function(notifications) {
395 notifications = notifications || {};
396 for (var chromeNotificationId in notifications) {
397 instrumented.notifications.clear(chromeNotificationId, function() {});
399 chrome.storage.local.remove(['notificationsData', 'notificationGroups']);
404 * Adds a card group into a set of combined cards.
405 * @param {Object.<ChromeNotificationId, CombinedCard>} combinedCards Map from
406 * chromeNotificationId to a combined card.
407 * This is an input/output parameter.
408 * @param {StoredNotificationGroup} storedGroup Group to combine into the
411 function combineGroup(combinedCards, storedGroup) {
412 for (var i = 0; i < storedGroup.cards.length; i++) {
413 /** @type {ReceivedNotification} */
414 var receivedNotification = storedGroup.cards[i];
416 /** @type {UncombinedNotification} */
417 var uncombinedNotification = {
418 receivedNotification: receivedNotification,
419 showTime: receivedNotification.trigger.showTimeSec &&
420 (storedGroup.cardsTimestamp +
421 receivedNotification.trigger.showTimeSec * MS_IN_SECOND),
422 hideTime: storedGroup.cardsTimestamp +
423 receivedNotification.trigger.hideTimeSec * MS_IN_SECOND
427 combinedCards[receivedNotification.chromeNotificationId] || [];
428 combinedCard.push(uncombinedNotification);
429 combinedCards[receivedNotification.chromeNotificationId] = combinedCard;
434 * Calculates the soonest poll time from a map of groups as an absolute time.
435 * @param {Object.<string, StoredNotificationGroup>} groups Map from group name
436 * to group information.
437 * @return {number} The next poll time based off of the groups.
439 function calculateNextPollTimeMilliseconds(groups) {
440 var nextPollTime = null;
442 for (var groupName in groups) {
443 var group = groups[groupName];
444 if (group.nextPollTime !== undefined) {
445 nextPollTime = nextPollTime == null ?
446 group.nextPollTime : Math.min(group.nextPollTime, nextPollTime);
450 // At least one of the groups must have nextPollTime.
451 verify(nextPollTime != null, 'calculateNextPollTime: nextPollTime is null');
456 * Schedules next cards poll.
457 * @param {Object.<string, StoredNotificationGroup>} groups Map from group name
458 * to group information.
460 function scheduleNextCardsPoll(groups) {
461 var nextPollTimeMs = calculateNextPollTimeMilliseconds(groups);
463 var nextPollDelaySeconds = Math.max(
464 (nextPollTimeMs - Date.now()) / MS_IN_SECOND,
465 MINIMUM_POLLING_PERIOD_SECONDS);
466 updateCardsAttempts.start(nextPollDelaySeconds);
470 * Schedules the next opt-in check poll.
472 function scheduleOptInCheckPoll() {
473 instrumented.metricsPrivate.getVariationParams(
474 'GoogleNow', function(params) {
475 var optinPollPeriodSeconds =
476 parseInt(params && params.optinPollPeriodSeconds, 10) ||
477 DEFAULT_OPTIN_CHECK_PERIOD_SECONDS;
478 optInPollAttempts.start(optinPollPeriodSeconds);
483 * Combines notification groups into a set of Chrome notifications.
484 * @param {Object.<string, StoredNotificationGroup>} notificationGroups Map from
485 * group name to group information.
486 * @return {Object.<ChromeNotificationId, CombinedCard>} Cards to show.
488 function combineCardsFromGroups(notificationGroups) {
489 console.log('combineCardsFromGroups ' + JSON.stringify(notificationGroups));
490 /** @type {Object.<ChromeNotificationId, CombinedCard>} */
491 var combinedCards = {};
493 for (var groupName in notificationGroups)
494 combineGroup(combinedCards, notificationGroups[groupName]);
496 return combinedCards;
500 * Processes a server response for consumption by showNotificationGroups.
501 * @param {ServerResponse} response Server response.
502 * @return {Promise} A promise to process the server response and provide
503 * updated groups. Rejects if the server response shouldn't be processed.
505 function processServerResponse(response) {
506 console.log('processServerResponse ' + JSON.stringify(response));
508 if (response.googleNowDisabled) {
509 chrome.storage.local.set({googleNowEnabled: false});
510 // Stop processing now. The state change will clear the cards.
511 return Promise.reject();
514 var receivedGroups = response.groups;
516 return fillFromChromeLocalStorage({
517 /** @type {Object.<string, StoredNotificationGroup>} */
518 notificationGroups: {},
519 /** @type {Object.<ServerNotificationId, number>} */
521 }).then(function(items) {
522 console.log('processServerResponse-get ' + JSON.stringify(items));
524 // Build a set of non-expired recent dismissals. It will be used for
525 // client-side filtering of cards.
526 /** @type {Object.<ServerNotificationId, number>} */
527 var updatedRecentDismissals = {};
528 var now = Date.now();
529 for (var serverNotificationId in items.recentDismissals) {
530 var dismissalAge = now - items.recentDismissals[serverNotificationId];
531 if (dismissalAge < DISMISS_RETENTION_TIME_MS) {
532 updatedRecentDismissals[serverNotificationId] =
533 items.recentDismissals[serverNotificationId];
537 // Populate groups with corresponding cards.
538 if (response.notifications) {
539 for (var i = 0; i < response.notifications.length; ++i) {
540 /** @type {ReceivedNotification} */
541 var card = response.notifications[i];
542 if (!(card.notificationId in updatedRecentDismissals)) {
543 var group = receivedGroups[card.groupName];
544 group.cards = group.cards || [];
545 group.cards.push(card);
550 // Build updated set of groups.
551 var updatedGroups = {};
553 for (var groupName in receivedGroups) {
554 var receivedGroup = receivedGroups[groupName];
555 var storedGroup = items.notificationGroups[groupName] || {
557 cardsTimestamp: undefined,
558 nextPollTime: undefined,
562 if (receivedGroup.requested)
563 receivedGroup.cards = receivedGroup.cards || [];
565 if (receivedGroup.cards) {
566 // If the group contains a cards update, all its fields will get new
568 storedGroup.cards = receivedGroup.cards;
569 storedGroup.cardsTimestamp = now;
570 storedGroup.rank = receivedGroup.rank;
571 storedGroup.nextPollTime = undefined;
572 // The code below assigns nextPollTime a defined value if
573 // nextPollSeconds is specified in the received group.
574 // If the group's cards are not updated, and nextPollSeconds is
575 // unspecified, this method doesn't change group's nextPollTime.
578 // 'nextPollSeconds' may be sent even for groups that don't contain
580 if (receivedGroup.nextPollSeconds !== undefined) {
581 storedGroup.nextPollTime =
582 now + receivedGroup.nextPollSeconds * MS_IN_SECOND;
585 updatedGroups[groupName] = storedGroup;
588 scheduleNextCardsPoll(updatedGroups);
590 updatedGroups: updatedGroups,
591 recentDismissals: updatedRecentDismissals
597 * Update the Explanatory Total Cards Shown Count.
599 function countExplanatoryCard() {
600 localStorage['explanatoryCardsShown']++;
604 * Determines if cards should have an explanation link.
605 * @return {boolean} true if an explanatory card should be shown.
607 function shouldShowExplanatoryCard() {
608 var isBelowThreshold =
609 localStorage['explanatoryCardsShown'] < EXPLANATORY_CARDS_LINK_THRESHOLD;
610 return isBelowThreshold;
614 * Requests notification cards from the server for specified groups.
615 * @param {Array.<string>} groupNames Names of groups that need to be refreshed.
616 * @return {Promise} A promise to request the specified notification groups.
618 function requestNotificationGroupsFromServer(groupNames) {
620 'requestNotificationGroupsFromServer from ' + NOTIFICATION_CARDS_URL +
621 ', groupNames=' + JSON.stringify(groupNames));
623 recordEvent(GoogleNowEvent.REQUEST_FOR_CARDS_TOTAL);
625 var requestParameters = '?timeZoneOffsetMs=' +
626 (-new Date().getTimezoneOffset() * MS_IN_MINUTE);
628 if (shouldShowExplanatoryCard()) {
629 requestParameters += '&cardExplanation=true';
632 groupNames.forEach(function(groupName) {
633 requestParameters += ('&requestTypes=' + groupName);
636 requestParameters += '&uiLocale=' + navigator.language;
639 'requestNotificationGroupsFromServer: request=' + requestParameters);
641 return requestFromServer('GET', 'notifications' + requestParameters).then(
644 'requestNotificationGroupsFromServer-received ' + request.status);
645 if (request.status == HTTP_OK) {
646 recordEvent(GoogleNowEvent.REQUEST_FOR_CARDS_SUCCESS);
647 return JSON.parse(request.responseText);
653 * Performs an opt-in poll without an immediate recheck.
654 * If the response is not opted-in, schedule an opt-in check poll.
656 function pollOptedInNoImmediateRecheck() {
657 requestAndUpdateOptedIn()
658 .then(function(optedIn) {
660 // Request a repoll if we're not opted in.
661 return Promise.reject();
665 scheduleOptInCheckPoll();
670 * Requests the account opted-in state from the server and updates any
671 * state as necessary.
672 * @return {Promise} A promise to request and update the opted-in state.
673 * The promise resolves with the opt-in state.
675 function requestAndUpdateOptedIn() {
676 console.log('requestOptedIn from ' + NOTIFICATION_CARDS_URL);
678 return requestFromServer('GET', 'settings/optin').then(function(request) {
680 'requestOptedIn-received ' + request.status + ' ' + request.response);
681 if (request.status == HTTP_OK) {
682 var parsedResponse = JSON.parse(request.responseText);
683 return parsedResponse.value;
685 }).then(function(optedIn) {
686 chrome.storage.local.set({googleNowEnabled: optedIn});
692 * Determines the groups that need to be requested right now.
693 * @return {Promise} A promise to determine the groups to request.
695 function getGroupsToRequest() {
696 return fillFromChromeLocalStorage({
697 /** @type {Object.<string, StoredNotificationGroup>} */
698 notificationGroups: {}
699 }).then(function(items) {
700 console.log('getGroupsToRequest-storage-get ' + JSON.stringify(items));
701 var groupsToRequest = [];
702 var now = Date.now();
704 for (var groupName in items.notificationGroups) {
705 var group = items.notificationGroups[groupName];
706 if (group.nextPollTime !== undefined && group.nextPollTime <= now)
707 groupsToRequest.push(groupName);
709 return groupsToRequest;
714 * Requests notification cards from the server.
715 * @return {Promise} A promise to request the notification cards.
716 * Rejects if the cards won't be requested.
718 function requestNotificationCards() {
719 console.log('requestNotificationCards');
720 return getGroupsToRequest()
721 .then(requestNotificationGroupsFromServer)
722 .then(processServerResponse)
723 .then(function(processedResponse) {
725 shouldShowExplanatoryCard() ? countExplanatoryCard : undefined;
726 return showNotificationGroups(
727 processedResponse.updatedGroups, onCardShown).then(function() {
728 chrome.storage.local.set({
729 notificationGroups: processedResponse.updatedGroups,
730 recentDismissals: processedResponse.updatedRecentDismissals
732 recordEvent(GoogleNowEvent.CARDS_PARSE_SUCCESS);
739 * Determines if an immediate retry should occur based off of the given groups.
740 * The NOR group is expected most often and less latency sensitive, so we will
741 * simply wait MAXIMUM_POLLING_PERIOD_SECONDS before trying again.
742 * @param {Array.<string>} groupNames Names of groups that need to be refreshed.
743 * @return {boolean} Whether a retry should occur.
745 function shouldScheduleRetryFromGroupList(groupNames) {
746 return (groupNames.length != 1) || (groupNames[0] !== 'NOR');
750 * Requests and shows notification cards.
752 function requestCards() {
753 console.log('requestCards @' + new Date());
754 // LOCATION_REQUEST is a legacy histogram value when we requested location.
755 // This corresponds to the extension attempting to request for cards.
756 // We're keeping the name the same to keep our histograms in order.
757 recordEvent(GoogleNowEvent.LOCATION_REQUEST);
758 tasks.add(UPDATE_CARDS_TASK_NAME, function() {
759 console.log('requestCards-task-begin');
760 updateCardsAttempts.isRunning(function(running) {
762 // The cards are requested only if there are no unsent dismissals.
763 processPendingDismissals()
764 .then(requestNotificationCards)
766 return getGroupsToRequest().then(function(groupsToRequest) {
767 if (shouldScheduleRetryFromGroupList(groupsToRequest)) {
768 updateCardsAttempts.scheduleRetry();
778 * Sends a server request to dismiss a card.
779 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
781 * @param {number} dismissalTimeMs Time of the user's dismissal of the card in
782 * milliseconds since epoch.
783 * @param {DismissalData} dismissalData Data to build a dismissal request.
784 * @return {Promise} A promise to request the card dismissal, rejects on error.
786 function requestCardDismissal(
787 chromeNotificationId, dismissalTimeMs, dismissalData) {
788 console.log('requestDismissingCard ' + chromeNotificationId +
789 ' from ' + NOTIFICATION_CARDS_URL +
790 ', dismissalData=' + JSON.stringify(dismissalData));
792 var dismissalAge = Date.now() - dismissalTimeMs;
794 if (dismissalAge > MAXIMUM_DISMISSAL_AGE_MS) {
795 return Promise.resolve();
798 recordEvent(GoogleNowEvent.DISMISS_REQUEST_TOTAL);
800 var requestParameters = 'notifications/' + dismissalData.notificationId +
801 '?age=' + dismissalAge +
802 '&chromeNotificationId=' + chromeNotificationId;
804 for (var paramField in dismissalData.parameters)
805 requestParameters += ('&' + paramField +
806 '=' + dismissalData.parameters[paramField]);
808 console.log('requestCardDismissal: requestParameters=' + requestParameters);
810 return requestFromServer('DELETE', requestParameters).then(function(request) {
811 console.log('requestDismissingCard-onloadend ' + request.status);
812 if (request.status == HTTP_NOCONTENT)
813 recordEvent(GoogleNowEvent.DISMISS_REQUEST_SUCCESS);
815 // A dismissal doesn't require further retries if it was successful or
816 // doesn't have a chance for successful completion.
817 return (request.status == HTTP_NOCONTENT) ?
820 }).catch(function(request) {
821 request = (typeof request === 'object') ? request : {};
822 return (request.status == HTTP_BAD_REQUEST ||
823 request.status == HTTP_METHOD_NOT_ALLOWED) ?
830 * Tries to send dismiss requests for all pending dismissals.
831 * @return {Promise} A promise to process the pending dismissals.
832 * The promise is rejected if a problem was encountered.
834 function processPendingDismissals() {
835 return fillFromChromeLocalStorage({
836 /** @type {Array.<PendingDismissal>} */
837 pendingDismissals: [],
838 /** @type {Object.<ServerNotificationId, number>} */
840 }).then(function(items) {
842 'processPendingDismissals-storage-get ' + JSON.stringify(items));
844 var dismissalsChanged = false;
846 function onFinish(success) {
847 if (dismissalsChanged) {
848 chrome.storage.local.set({
849 pendingDismissals: items.pendingDismissals,
850 recentDismissals: items.recentDismissals
853 return success ? Promise.resolve() : Promise.reject();
856 function doProcessDismissals() {
857 if (items.pendingDismissals.length == 0) {
858 dismissalAttempts.stop();
859 return onFinish(true);
862 // Send dismissal for the first card, and if successful, repeat
863 // recursively with the rest.
864 /** @type {PendingDismissal} */
865 var dismissal = items.pendingDismissals[0];
866 return requestCardDismissal(
867 dismissal.chromeNotificationId,
869 dismissal.dismissalData).then(function() {
870 dismissalsChanged = true;
871 items.pendingDismissals.splice(0, 1);
872 items.recentDismissals[dismissal.dismissalData.notificationId] =
874 return doProcessDismissals();
875 }).catch(function() {
876 return onFinish(false);
880 return doProcessDismissals();
885 * Submits a task to send pending dismissals.
887 function retryPendingDismissals() {
888 tasks.add(RETRY_DISMISS_TASK_NAME, function() {
889 processPendingDismissals().catch(dismissalAttempts.scheduleRetry);
894 * Opens a URL in a new tab.
895 * @param {string} url URL to open.
897 function openUrl(url) {
898 instrumented.tabs.create({url: url}, function(tab) {
900 chrome.windows.update(tab.windowId, {focused: true});
902 chrome.windows.create({url: url, focused: true});
907 * Opens URL corresponding to the clicked part of the notification.
908 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
910 * @param {function(NotificationDataEntry): (string|undefined)} selector
911 * Function that extracts the url for the clicked area from the
912 * notification data entry.
914 function onNotificationClicked(chromeNotificationId, selector) {
915 fillFromChromeLocalStorage({
916 /** @type {Object.<ChromeNotificationId, NotificationDataEntry>} */
917 notificationsData: {}
918 }).then(function(items) {
919 /** @type {(NotificationDataEntry|undefined)} */
920 var notificationDataEntry = items.notificationsData[chromeNotificationId];
921 if (!notificationDataEntry)
924 var url = selector(notificationDataEntry);
933 * Callback for chrome.notifications.onClosed event.
934 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
936 * @param {boolean} byUser Whether the notification was closed by the user.
938 function onNotificationClosed(chromeNotificationId, byUser) {
942 // At this point we are guaranteed that the notification is a now card.
943 chrome.metricsPrivate.recordUserAction('GoogleNow.Dismissed');
945 tasks.add(DISMISS_CARD_TASK_NAME, function() {
946 dismissalAttempts.start();
948 fillFromChromeLocalStorage({
949 /** @type {Array.<PendingDismissal>} */
950 pendingDismissals: [],
951 /** @type {Object.<ChromeNotificationId, NotificationDataEntry>} */
952 notificationsData: {},
953 /** @type {Object.<string, StoredNotificationGroup>} */
954 notificationGroups: {}
955 }).then(function(items) {
956 /** @type {NotificationDataEntry} */
957 var notificationData =
958 items.notificationsData[chromeNotificationId] ||
960 timestamp: Date.now(),
964 var dismissalResult =
966 chromeNotificationId,
968 items.notificationGroups);
970 for (var i = 0; i < dismissalResult.dismissals.length; i++) {
971 /** @type {PendingDismissal} */
973 chromeNotificationId: chromeNotificationId,
975 dismissalData: dismissalResult.dismissals[i]
977 items.pendingDismissals.push(dismissal);
980 items.notificationsData[chromeNotificationId] =
981 dismissalResult.notificationData;
983 chrome.storage.local.set(items);
985 processPendingDismissals();
991 * Initializes the polling system to start fetching cards.
993 function startPollingCards() {
994 console.log('startPollingCards');
995 // Create an update timer for a case when for some reason requesting
997 updateCardsAttempts.start(MAXIMUM_POLLING_PERIOD_SECONDS);
1002 * Stops all machinery in the polling system.
1004 function stopPollingCards() {
1005 console.log('stopPollingCards');
1006 updateCardsAttempts.stop();
1007 // Since we're stopping everything, clear all runtime storage.
1008 // We don't clear localStorage since those values are still relevant
1009 // across Google Now start-stop events.
1010 chrome.storage.local.clear();
1014 * Initializes the event page on install or on browser startup.
1016 function initialize() {
1017 recordEvent(GoogleNowEvent.EXTENSION_START);
1022 * Starts or stops the main pipeline for polling cards.
1023 * @param {boolean} shouldPollCardsRequest true to start and
1024 * false to stop polling cards.
1026 function setShouldPollCards(shouldPollCardsRequest) {
1027 updateCardsAttempts.isRunning(function(currentValue) {
1028 if (shouldPollCardsRequest != currentValue) {
1029 console.log('Action Taken setShouldPollCards=' + shouldPollCardsRequest);
1030 if (shouldPollCardsRequest)
1031 startPollingCards();
1036 'Action Ignored setShouldPollCards=' + shouldPollCardsRequest);
1042 * Starts or stops the optin check.
1043 * @param {boolean} shouldPollOptInStatus true to start and false to stop
1044 * polling the optin status.
1046 function setShouldPollOptInStatus(shouldPollOptInStatus) {
1047 optInPollAttempts.isRunning(function(currentValue) {
1048 if (shouldPollOptInStatus != currentValue) {
1050 'Action Taken setShouldPollOptInStatus=' + shouldPollOptInStatus);
1051 if (shouldPollOptInStatus) {
1052 pollOptedInNoImmediateRecheck();
1054 optInPollAttempts.stop();
1058 'Action Ignored setShouldPollOptInStatus=' + shouldPollOptInStatus);
1064 * Enables or disables the Google Now background permission.
1065 * @param {boolean} backgroundEnable true to run in the background.
1066 * false to not run in the background.
1068 function setBackgroundEnable(backgroundEnable) {
1069 instrumented.permissions.contains({permissions: ['background']},
1070 function(hasPermission) {
1071 if (backgroundEnable != hasPermission) {
1072 console.log('Action Taken setBackgroundEnable=' + backgroundEnable);
1073 if (backgroundEnable)
1074 chrome.permissions.request({permissions: ['background']});
1076 chrome.permissions.remove({permissions: ['background']});
1078 console.log('Action Ignored setBackgroundEnable=' + backgroundEnable);
1084 * Record why this extension would not poll for cards.
1085 * @param {boolean} signedIn true if the user is signed in.
1086 * @param {boolean} notificationEnabled true if
1087 * Google Now for Chrome is allowed to show notifications.
1088 * @param {boolean} googleNowEnabled true if
1089 * the Google Now is enabled for the user.
1091 function recordEventIfNoCards(signedIn, notificationEnabled, googleNowEnabled) {
1093 recordEvent(GoogleNowEvent.SIGNED_OUT);
1094 } else if (!notificationEnabled) {
1095 recordEvent(GoogleNowEvent.NOTIFICATION_DISABLED);
1096 } else if (!googleNowEnabled) {
1097 recordEvent(GoogleNowEvent.GOOGLE_NOW_DISABLED);
1102 * Does the actual work of deciding what Google Now should do
1103 * based off of the current state of Chrome.
1104 * @param {boolean} signedIn true if the user is signed in.
1105 * @param {boolean} canEnableBackground true if
1106 * the background permission can be requested.
1107 * @param {boolean} notificationEnabled true if
1108 * Google Now for Chrome is allowed to show notifications.
1109 * @param {boolean} googleNowEnabled true if
1110 * the Google Now is enabled for the user.
1112 function updateRunningState(
1114 canEnableBackground,
1115 notificationEnabled,
1118 'State Update signedIn=' + signedIn + ' ' +
1119 'canEnableBackground=' + canEnableBackground + ' ' +
1120 'notificationEnabled=' + notificationEnabled + ' ' +
1121 'googleNowEnabled=' + googleNowEnabled);
1123 var shouldPollCards = false;
1124 var shouldPollOptInStatus = false;
1125 var shouldSetBackground = false;
1127 if (signedIn && notificationEnabled) {
1128 shouldPollCards = googleNowEnabled;
1129 shouldPollOptInStatus = !googleNowEnabled;
1130 shouldSetBackground = canEnableBackground && googleNowEnabled;
1132 recordEvent(GoogleNowEvent.STOPPED);
1135 recordEventIfNoCards(signedIn, notificationEnabled, googleNowEnabled);
1138 'Requested Actions shouldSetBackground=' + shouldSetBackground + ' ' +
1139 'setShouldPollCards=' + shouldPollCards + ' ' +
1140 'shouldPollOptInStatus=' + shouldPollOptInStatus);
1142 setBackgroundEnable(shouldSetBackground);
1143 setShouldPollCards(shouldPollCards);
1144 setShouldPollOptInStatus(shouldPollOptInStatus);
1145 if (!shouldPollCards) {
1151 * Coordinates the behavior of Google Now for Chrome depending on
1152 * Chrome and extension state.
1154 function onStateChange() {
1155 tasks.add(STATE_CHANGED_TASK_NAME, function() {
1157 authenticationManager.isSignedIn(),
1158 canEnableBackground(),
1159 isNotificationsEnabled(),
1160 isGoogleNowEnabled()])
1161 .then(function(results) {
1162 updateRunningState.apply(null, results);
1168 * Determines if background mode should be requested.
1169 * @return {Promise} A promise to determine if background can be enabled.
1171 function canEnableBackground() {
1172 return new Promise(function(resolve) {
1173 instrumented.metricsPrivate.getVariationParams(
1175 function(response) {
1176 resolve(!response || (response.canEnableBackground != 'false'));
1182 * Checks if Google Now is enabled in the notifications center.
1183 * @return {Promise} A promise to determine if Google Now is enabled
1184 * in the notifications center.
1186 function isNotificationsEnabled() {
1187 return new Promise(function(resolve) {
1188 instrumented.notifications.getPermissionLevel(function(level) {
1189 resolve(level == 'granted');
1195 * Gets the previous Google Now opt-in state.
1196 * @return {Promise} A promise to determine the previous Google Now
1199 function isGoogleNowEnabled() {
1200 return fillFromChromeLocalStorage({googleNowEnabled: false})
1201 .then(function(items) {
1202 return items.googleNowEnabled;
1207 * Polls the optin state.
1208 * Sometimes we get the response to the opted in result too soon during
1209 * push messaging. We'll recheck the optin state a few times before giving up.
1211 function pollOptedInWithRecheck() {
1213 * Cleans up any state used to recheck the opt-in poll.
1215 function clearPollingState() {
1216 localStorage.removeItem('optedInCheckCount');
1217 optInRecheckAttempts.stop();
1220 if (localStorage.optedInCheckCount === undefined) {
1221 localStorage.optedInCheckCount = 0;
1222 optInRecheckAttempts.start();
1225 console.log(new Date() +
1226 ' checkOptedIn Attempt ' + localStorage.optedInCheckCount);
1228 requestAndUpdateOptedIn().then(function(optedIn) {
1230 clearPollingState();
1231 return Promise.resolve();
1233 // If we're not opted in, reject to retry.
1234 return Promise.reject();
1236 }).catch(function() {
1237 if (localStorage.optedInCheckCount < 5) {
1238 localStorage.optedInCheckCount++;
1239 optInRecheckAttempts.scheduleRetry();
1241 clearPollingState();
1246 instrumented.runtime.onInstalled.addListener(function(details) {
1247 console.log('onInstalled ' + JSON.stringify(details));
1248 if (details.reason != 'chrome_update') {
1253 instrumented.runtime.onStartup.addListener(function() {
1254 console.log('onStartup');
1256 // Show notifications received by earlier polls. Doing this as early as
1257 // possible to reduce latency of showing first notifications. This mimics how
1258 // persistent notifications will work.
1259 tasks.add(SHOW_ON_START_TASK_NAME, function() {
1260 fillFromChromeLocalStorage({
1261 /** @type {Object.<string, StoredNotificationGroup>} */
1262 notificationGroups: {}
1263 }).then(function(items) {
1264 console.log('onStartup-get ' + JSON.stringify(items));
1266 showNotificationGroups(items.notificationGroups).then(function() {
1267 chrome.storage.local.set(items);
1275 authenticationManager.addListener(function() {
1276 console.log('signIn State Change');
1280 instrumented.notifications.onClicked.addListener(
1281 function(chromeNotificationId) {
1282 chrome.metricsPrivate.recordUserAction('GoogleNow.MessageClicked');
1283 onNotificationClicked(chromeNotificationId,
1284 function(notificationDataEntry) {
1285 var actionUrls = notificationDataEntry.actionUrls;
1286 var url = actionUrls && actionUrls.messageUrl;
1288 recordNotificationClick(notificationDataEntry.cardTypeId);
1294 instrumented.notifications.onButtonClicked.addListener(
1295 function(chromeNotificationId, buttonIndex) {
1296 chrome.metricsPrivate.recordUserAction(
1297 'GoogleNow.ButtonClicked' + buttonIndex);
1298 onNotificationClicked(chromeNotificationId,
1299 function(notificationDataEntry) {
1300 var actionUrls = notificationDataEntry.actionUrls;
1301 var url = actionUrls.buttonUrls[buttonIndex];
1303 recordButtonClick(notificationDataEntry.cardTypeId, buttonIndex);
1305 verify(false, 'onButtonClicked: no url for a button');
1307 'buttonIndex=' + buttonIndex + ' ' +
1308 'chromeNotificationId=' + chromeNotificationId + ' ' +
1309 'notificationDataEntry=' +
1310 JSON.stringify(notificationDataEntry));
1316 instrumented.notifications.onClosed.addListener(onNotificationClosed);
1318 instrumented.notifications.onPermissionLevelChanged.addListener(
1319 function(permissionLevel) {
1320 console.log('Notifications permissionLevel Change');
1324 instrumented.notifications.onShowSettings.addListener(function() {
1325 openUrl(SETTINGS_URL);
1328 // Handles state change notifications for the Google Now enabled bit.
1329 instrumented.storage.onChanged.addListener(function(changes, areaName) {
1330 if (areaName === 'local') {
1331 if ('googleNowEnabled' in changes) {
1337 instrumented.pushMessaging.onMessage.addListener(function(message) {
1338 // message.payload will be '' when the extension first starts.
1339 // Each time after signing in, we'll get latest payload for all channels.
1340 // So, we need to poll the server only when the payload is non-empty and has
1342 console.log('pushMessaging.onMessage ' + JSON.stringify(message));
1343 if (message.payload.indexOf('REQUEST_CARDS') == 0) {
1344 tasks.add(ON_PUSH_MESSAGE_START_TASK_NAME, function() {
1345 // Accept promise rejection on failure since it's safer to do nothing,
1346 // preventing polling the server when the payload really didn't change.
1347 fillFromChromeLocalStorage({
1348 lastPollNowPayloads: {},
1349 /** @type {Object.<string, StoredNotificationGroup>} */
1350 notificationGroups: {}
1351 }, PromiseRejection.ALLOW).then(function(items) {
1352 if (items.lastPollNowPayloads[message.subchannelId] !=
1354 items.lastPollNowPayloads[message.subchannelId] = message.payload;
1356 items.notificationGroups['PUSH' + message.subchannelId] = {
1358 nextPollTime: Date.now()
1361 chrome.storage.local.set({
1362 lastPollNowPayloads: items.lastPollNowPayloads,
1363 notificationGroups: items.notificationGroups
1366 pollOptedInWithRecheck();