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 * GCM registration URL.
100 var GCM_REGISTRATION_URL =
101 'https://android.googleapis.com/gcm/googlenotification';
104 * DevConsole project ID for GCM API use.
106 var GCM_PROJECT_ID = '437902709571';
109 * Number of cards that need an explanatory link.
111 var EXPLANATORY_CARDS_LINK_THRESHOLD = 4;
114 * Names for tasks that can be created by the extension.
116 var UPDATE_CARDS_TASK_NAME = 'update-cards';
117 var DISMISS_CARD_TASK_NAME = 'dismiss-card';
118 var RETRY_DISMISS_TASK_NAME = 'retry-dismiss';
119 var STATE_CHANGED_TASK_NAME = 'state-changed';
120 var SHOW_ON_START_TASK_NAME = 'show-cards-on-start';
121 var ON_PUSH_MESSAGE_START_TASK_NAME = 'on-push-message';
124 * Group as received from the server.
127 * nextPollSeconds: (string|undefined),
128 * rank: (number|undefined),
129 * requested: (boolean|undefined)
135 * Server response with notifications and groups.
138 * googleNowDisabled: (boolean|undefined),
139 * groups: Object<ReceivedGroup>,
140 * notifications: Array<ReceivedNotification>
146 * Notification group as the client stores it. |cardsTimestamp| and |rank| are
147 * defined if |cards| is non-empty. |nextPollTime| is undefined if the server
148 * (1) never sent 'nextPollSeconds' for the group or
149 * (2) didn't send 'nextPollSeconds' with the last group update containing a
150 * cards update and all the times after that.
153 * cards: Array<ReceivedNotification>,
154 * cardsTimestamp: (number|undefined),
155 * nextPollTime: (number|undefined),
156 * rank: (number|undefined)
159 var StoredNotificationGroup;
162 * Pending (not yet successfully sent) dismissal for a received notification.
163 * |time| is the moment when the user requested dismissal.
166 * chromeNotificationId: ChromeNotificationId,
168 * dismissalData: DismissalData
171 var PendingDismissal;
174 * Checks if a new task can't be scheduled when another task is already
176 * @param {string} newTaskName Name of the new task.
177 * @param {string} scheduledTaskName Name of the scheduled task.
178 * @return {boolean} Whether the new task conflicts with the existing task.
180 function areTasksConflicting(newTaskName, scheduledTaskName) {
181 if (newTaskName == UPDATE_CARDS_TASK_NAME &&
182 scheduledTaskName == UPDATE_CARDS_TASK_NAME) {
183 // If a card update is requested while an old update is still scheduled, we
184 // don't need the new update.
188 if (newTaskName == RETRY_DISMISS_TASK_NAME &&
189 (scheduledTaskName == UPDATE_CARDS_TASK_NAME ||
190 scheduledTaskName == DISMISS_CARD_TASK_NAME ||
191 scheduledTaskName == RETRY_DISMISS_TASK_NAME)) {
192 // No need to schedule retry-dismiss action if another action that tries to
193 // send dismissals is scheduled.
200 var tasks = buildTaskManager(areTasksConflicting);
202 // Add error processing to API calls.
203 wrapper.instrumentChromeApiFunction('gcm.onMessage.addListener', 0);
204 wrapper.instrumentChromeApiFunction('gcm.register', 1);
205 wrapper.instrumentChromeApiFunction('gcm.unregister', 0);
206 wrapper.instrumentChromeApiFunction('metricsPrivate.getVariationParams', 1);
207 wrapper.instrumentChromeApiFunction('notifications.clear', 1);
208 wrapper.instrumentChromeApiFunction('notifications.create', 2);
209 wrapper.instrumentChromeApiFunction('notifications.getPermissionLevel', 0);
210 wrapper.instrumentChromeApiFunction('notifications.update', 2);
211 wrapper.instrumentChromeApiFunction('notifications.getAll', 0);
212 wrapper.instrumentChromeApiFunction(
213 'notifications.onButtonClicked.addListener', 0);
214 wrapper.instrumentChromeApiFunction('notifications.onClicked.addListener', 0);
215 wrapper.instrumentChromeApiFunction('notifications.onClosed.addListener', 0);
216 wrapper.instrumentChromeApiFunction(
217 'notifications.onPermissionLevelChanged.addListener', 0);
218 wrapper.instrumentChromeApiFunction(
219 'notifications.onShowSettings.addListener', 0);
220 wrapper.instrumentChromeApiFunction('permissions.contains', 1);
221 wrapper.instrumentChromeApiFunction('runtime.onInstalled.addListener', 0);
222 wrapper.instrumentChromeApiFunction('runtime.onStartup.addListener', 0);
223 wrapper.instrumentChromeApiFunction('storage.onChanged.addListener', 0);
224 wrapper.instrumentChromeApiFunction('tabs.create', 1);
226 var updateCardsAttempts = buildAttemptManager(
229 INITIAL_POLLING_PERIOD_SECONDS,
230 MAXIMUM_POLLING_PERIOD_SECONDS);
231 var optInPollAttempts = buildAttemptManager(
233 pollOptedInNoImmediateRecheck,
234 INITIAL_POLLING_PERIOD_SECONDS,
235 MAXIMUM_POLLING_PERIOD_SECONDS);
236 var optInRecheckAttempts = buildAttemptManager(
238 pollOptedInWithRecheck,
239 INITIAL_OPTIN_RECHECK_PERIOD_SECONDS,
240 MAXIMUM_OPTIN_RECHECK_PERIOD_SECONDS);
241 var dismissalAttempts = buildAttemptManager(
243 retryPendingDismissals,
244 INITIAL_RETRY_DISMISS_PERIOD_SECONDS,
245 MAXIMUM_RETRY_DISMISS_PERIOD_SECONDS);
246 var cardSet = buildCardSet();
248 var authenticationManager = buildAuthenticationManager();
251 * Google Now UMA event identifier.
254 var GoogleNowEvent = {
255 REQUEST_FOR_CARDS_TOTAL: 0,
256 REQUEST_FOR_CARDS_SUCCESS: 1,
257 CARDS_PARSE_SUCCESS: 2,
258 DISMISS_REQUEST_TOTAL: 3,
259 DISMISS_REQUEST_SUCCESS: 4,
261 DELETED_LOCATION_UPDATE: 6,
263 DELETED_SHOW_WELCOME_TOAST: 8,
265 DELETED_USER_SUPPRESSED: 10,
267 NOTIFICATION_DISABLED: 12,
268 GOOGLE_NOW_DISABLED: 13,
269 EVENTS_TOTAL: 14 // EVENTS_TOTAL is not an event; all new events need to be
274 * Records a Google Now Event.
275 * @param {GoogleNowEvent} event Event identifier.
277 function recordEvent(event) {
278 var metricDescription = {
279 metricName: 'GoogleNow.Event',
280 type: 'histogram-linear',
282 max: GoogleNowEvent.EVENTS_TOTAL,
283 buckets: GoogleNowEvent.EVENTS_TOTAL + 1
286 chrome.metricsPrivate.recordValue(metricDescription, event);
290 * Records a notification clicked event.
291 * @param {number|undefined} cardTypeId Card type ID.
293 function recordNotificationClick(cardTypeId) {
294 if (cardTypeId !== undefined) {
295 chrome.metricsPrivate.recordSparseValue(
296 'GoogleNow.Card.Clicked', cardTypeId);
301 * Records a button clicked event.
302 * @param {number|undefined} cardTypeId Card type ID.
303 * @param {number} buttonIndex Button Index
305 function recordButtonClick(cardTypeId, buttonIndex) {
306 if (cardTypeId !== undefined) {
307 chrome.metricsPrivate.recordSparseValue(
308 'GoogleNow.Card.Button.Clicked' + buttonIndex, cardTypeId);
313 * Checks the result of the HTTP Request and updates the authentication
314 * manager on any failure.
315 * @param {string} token Authentication token to validate against an
317 * @return {function(XMLHttpRequest)} Function that validates the token with the
318 * supplied XMLHttpRequest.
320 function checkAuthenticationStatus(token) {
321 return function(request) {
322 if (request.status == HTTP_FORBIDDEN ||
323 request.status == HTTP_UNAUTHORIZED) {
324 authenticationManager.removeToken(token);
330 * Builds and sends an authenticated request to the notification server.
331 * @param {string} method Request method.
332 * @param {string} handlerName Server handler to send the request to.
333 * @param {string=} opt_contentType Value for the Content-type header.
334 * @return {Promise} A promise to issue a request to the server.
335 * The promise rejects if the response is not within the HTTP 200 range.
337 function requestFromServer(method, handlerName, opt_contentType) {
338 return authenticationManager.getAuthToken().then(function(token) {
339 var request = buildServerRequest(method, handlerName, opt_contentType);
340 request.setRequestHeader('Authorization', 'Bearer ' + token);
341 var requestPromise = new Promise(function(resolve, reject) {
342 request.addEventListener('loadend', function() {
343 if ((200 <= request.status) && (request.status < 300)) {
351 requestPromise.catch(checkAuthenticationStatus(token));
352 return requestPromise;
357 * Shows the notification groups as notification cards.
358 * @param {Object<StoredNotificationGroup>} notificationGroups Map from group
359 * name to group information.
360 * @param {function(ReceivedNotification)=} opt_onCardShown Optional parameter
361 * called when each card is shown.
362 * @return {Promise} A promise to show the notification groups as cards.
364 function showNotificationGroups(notificationGroups, opt_onCardShown) {
365 /** @type {Object<ChromeNotificationId, CombinedCard>} */
366 var cards = combineCardsFromGroups(notificationGroups);
367 console.log('showNotificationGroups ' + JSON.stringify(cards));
369 return new Promise(function(resolve) {
370 instrumented.notifications.getAll(function(notifications) {
371 console.log('showNotificationGroups-getAll ' +
372 JSON.stringify(notifications));
373 notifications = notifications || {};
375 // Mark notifications that didn't receive an update as having received
377 for (var chromeNotificationId in notifications) {
378 cards[chromeNotificationId] = cards[chromeNotificationId] || [];
381 /** @type {Object<ChromeNotificationId, NotificationDataEntry>} */
382 var notificationsData = {};
384 // Create/update/delete notifications.
385 for (var chromeNotificationId in cards) {
386 notificationsData[chromeNotificationId] = cardSet.update(
387 chromeNotificationId,
388 cards[chromeNotificationId],
392 chrome.storage.local.set({notificationsData: notificationsData});
399 * Removes all cards and card state on Google Now close down.
401 function removeAllCards() {
402 console.log('removeAllCards');
404 // TODO(robliao): Once Google Now clears its own checkbox in the
405 // notifications center and bug 260376 is fixed, the below clearing
406 // code is no longer necessary.
407 instrumented.notifications.getAll(function(notifications) {
408 notifications = notifications || {};
409 for (var chromeNotificationId in notifications) {
410 instrumented.notifications.clear(chromeNotificationId, function() {});
412 chrome.storage.local.remove(['notificationsData', 'notificationGroups']);
417 * Adds a card group into a set of combined cards.
418 * @param {Object<ChromeNotificationId, CombinedCard>} combinedCards Map from
419 * chromeNotificationId to a combined card.
420 * This is an input/output parameter.
421 * @param {StoredNotificationGroup} storedGroup Group to combine into the
424 function combineGroup(combinedCards, storedGroup) {
425 for (var i = 0; i < storedGroup.cards.length; i++) {
426 /** @type {ReceivedNotification} */
427 var receivedNotification = storedGroup.cards[i];
429 /** @type {UncombinedNotification} */
430 var uncombinedNotification = {
431 receivedNotification: receivedNotification,
432 showTime: receivedNotification.trigger.showTimeSec &&
433 (storedGroup.cardsTimestamp +
434 receivedNotification.trigger.showTimeSec * MS_IN_SECOND),
435 hideTime: storedGroup.cardsTimestamp +
436 receivedNotification.trigger.hideTimeSec * MS_IN_SECOND
440 combinedCards[receivedNotification.chromeNotificationId] || [];
441 combinedCard.push(uncombinedNotification);
442 combinedCards[receivedNotification.chromeNotificationId] = combinedCard;
447 * Calculates the soonest poll time from a map of groups as an absolute time.
448 * @param {Object<StoredNotificationGroup>} groups Map from group name to group
450 * @return {number} The next poll time based off of the groups.
452 function calculateNextPollTimeMilliseconds(groups) {
453 var nextPollTime = null;
455 for (var groupName in groups) {
456 var group = groups[groupName];
457 if (group.nextPollTime !== undefined) {
458 nextPollTime = nextPollTime == null ?
459 group.nextPollTime : Math.min(group.nextPollTime, nextPollTime);
463 // At least one of the groups must have nextPollTime.
464 verify(nextPollTime != null, 'calculateNextPollTime: nextPollTime is null');
469 * Schedules next cards poll.
470 * @param {Object<StoredNotificationGroup>} groups Map from group name to group
473 function scheduleNextCardsPoll(groups) {
474 var nextPollTimeMs = calculateNextPollTimeMilliseconds(groups);
476 var nextPollDelaySeconds = Math.max(
477 (nextPollTimeMs - Date.now()) / MS_IN_SECOND,
478 MINIMUM_POLLING_PERIOD_SECONDS);
479 updateCardsAttempts.start(nextPollDelaySeconds);
483 * Schedules the next opt-in check poll.
485 function scheduleOptInCheckPoll() {
486 instrumented.metricsPrivate.getVariationParams(
487 'GoogleNow', function(params) {
488 var optinPollPeriodSeconds =
489 parseInt(params && params.optinPollPeriodSeconds, 10) ||
490 DEFAULT_OPTIN_CHECK_PERIOD_SECONDS;
491 optInPollAttempts.start(optinPollPeriodSeconds);
496 * Combines notification groups into a set of Chrome notifications.
497 * @param {Object<StoredNotificationGroup>} notificationGroups Map from group
498 * name to group information.
499 * @return {Object<ChromeNotificationId, CombinedCard>} Cards to show.
501 function combineCardsFromGroups(notificationGroups) {
502 console.log('combineCardsFromGroups ' + JSON.stringify(notificationGroups));
503 /** @type {Object<ChromeNotificationId, CombinedCard>} */
504 var combinedCards = {};
506 for (var groupName in notificationGroups)
507 combineGroup(combinedCards, notificationGroups[groupName]);
509 return combinedCards;
513 * Processes a server response for consumption by showNotificationGroups.
514 * @param {ServerResponse} response Server response.
515 * @return {Promise} A promise to process the server response and provide
516 * updated groups. Rejects if the server response shouldn't be processed.
518 function processServerResponse(response) {
519 console.log('processServerResponse ' + JSON.stringify(response));
521 if (response.googleNowDisabled) {
522 chrome.storage.local.set({googleNowEnabled: false});
523 // Stop processing now. The state change will clear the cards.
524 return Promise.reject();
527 var receivedGroups = response.groups;
529 return fillFromChromeLocalStorage({
530 /** @type {Object<StoredNotificationGroup>} */
531 notificationGroups: {},
532 /** @type {Object<ServerNotificationId, number>} */
534 }).then(function(items) {
535 console.log('processServerResponse-get ' + JSON.stringify(items));
537 // Build a set of non-expired recent dismissals. It will be used for
538 // client-side filtering of cards.
539 /** @type {Object<ServerNotificationId, number>} */
540 var updatedRecentDismissals = {};
541 var now = Date.now();
542 for (var serverNotificationId in items.recentDismissals) {
543 var dismissalAge = now - items.recentDismissals[serverNotificationId];
544 if (dismissalAge < DISMISS_RETENTION_TIME_MS) {
545 updatedRecentDismissals[serverNotificationId] =
546 items.recentDismissals[serverNotificationId];
550 // Populate groups with corresponding cards.
551 if (response.notifications) {
552 for (var i = 0; i < response.notifications.length; ++i) {
553 /** @type {ReceivedNotification} */
554 var card = response.notifications[i];
555 if (!(card.notificationId in updatedRecentDismissals)) {
556 var group = receivedGroups[card.groupName];
557 group.cards = group.cards || [];
558 group.cards.push(card);
563 // Build updated set of groups.
564 var updatedGroups = {};
566 for (var groupName in receivedGroups) {
567 var receivedGroup = receivedGroups[groupName];
568 var storedGroup = items.notificationGroups[groupName] || {
570 cardsTimestamp: undefined,
571 nextPollTime: undefined,
575 if (receivedGroup.requested)
576 receivedGroup.cards = receivedGroup.cards || [];
578 if (receivedGroup.cards) {
579 // If the group contains a cards update, all its fields will get new
581 storedGroup.cards = receivedGroup.cards;
582 storedGroup.cardsTimestamp = now;
583 storedGroup.rank = receivedGroup.rank;
584 storedGroup.nextPollTime = undefined;
585 // The code below assigns nextPollTime a defined value if
586 // nextPollSeconds is specified in the received group.
587 // If the group's cards are not updated, and nextPollSeconds is
588 // unspecified, this method doesn't change group's nextPollTime.
591 // 'nextPollSeconds' may be sent even for groups that don't contain
593 if (receivedGroup.nextPollSeconds !== undefined) {
594 storedGroup.nextPollTime =
595 now + receivedGroup.nextPollSeconds * MS_IN_SECOND;
598 updatedGroups[groupName] = storedGroup;
601 scheduleNextCardsPoll(updatedGroups);
603 updatedGroups: updatedGroups,
604 recentDismissals: updatedRecentDismissals
610 * Update the Explanatory Total Cards Shown Count.
612 function countExplanatoryCard() {
613 localStorage['explanatoryCardsShown']++;
617 * Determines if cards should have an explanation link.
618 * @return {boolean} true if an explanatory card should be shown.
620 function shouldShowExplanatoryCard() {
621 var isBelowThreshold =
622 localStorage['explanatoryCardsShown'] < EXPLANATORY_CARDS_LINK_THRESHOLD;
623 return isBelowThreshold;
627 * Requests notification cards from the server for specified groups.
628 * @param {Array<string>} groupNames Names of groups that need to be refreshed.
629 * @return {Promise} A promise to request the specified notification groups.
631 function requestNotificationGroupsFromServer(groupNames) {
633 'requestNotificationGroupsFromServer from ' + NOTIFICATION_CARDS_URL +
634 ', groupNames=' + JSON.stringify(groupNames));
636 recordEvent(GoogleNowEvent.REQUEST_FOR_CARDS_TOTAL);
638 var requestParameters = '?timeZoneOffsetMs=' +
639 (-new Date().getTimezoneOffset() * MS_IN_MINUTE);
641 if (shouldShowExplanatoryCard()) {
642 requestParameters += '&cardExplanation=true';
645 groupNames.forEach(function(groupName) {
646 requestParameters += ('&requestTypes=' + groupName);
649 requestParameters += '&uiLocale=' + navigator.language;
652 'requestNotificationGroupsFromServer: request=' + requestParameters);
654 return requestFromServer('GET', 'notifications' + requestParameters).then(
657 'requestNotificationGroupsFromServer-received ' + request.status);
658 if (request.status == HTTP_OK) {
659 recordEvent(GoogleNowEvent.REQUEST_FOR_CARDS_SUCCESS);
660 return JSON.parse(request.responseText);
666 * Performs an opt-in poll without an immediate recheck.
667 * If the response is not opted-in, schedule an opt-in check poll.
669 function pollOptedInNoImmediateRecheck() {
670 requestAndUpdateOptedIn()
671 .then(function(optedIn) {
673 // Request a repoll if we're not opted in.
674 return Promise.reject();
678 scheduleOptInCheckPoll();
683 * Requests the account opted-in state from the server and updates any
684 * state as necessary.
685 * @return {Promise} A promise to request and update the opted-in state.
686 * The promise resolves with the opt-in state.
688 function requestAndUpdateOptedIn() {
689 console.log('requestOptedIn from ' + NOTIFICATION_CARDS_URL);
691 return requestFromServer('GET', 'settings/optin').then(function(request) {
693 'requestOptedIn-received ' + request.status + ' ' + request.response);
694 if (request.status == HTTP_OK) {
695 var parsedResponse = JSON.parse(request.responseText);
696 return parsedResponse.value;
698 }).then(function(optedIn) {
699 chrome.storage.local.set({googleNowEnabled: optedIn});
705 * Determines the groups that need to be requested right now.
706 * @return {Promise} A promise to determine the groups to request.
708 function getGroupsToRequest() {
709 return fillFromChromeLocalStorage({
710 /** @type {Object<StoredNotificationGroup>} */
711 notificationGroups: {}
712 }).then(function(items) {
713 console.log('getGroupsToRequest-storage-get ' + JSON.stringify(items));
714 var groupsToRequest = [];
715 var now = Date.now();
717 for (var groupName in items.notificationGroups) {
718 var group = items.notificationGroups[groupName];
719 if (group.nextPollTime !== undefined && group.nextPollTime <= now)
720 groupsToRequest.push(groupName);
722 return groupsToRequest;
727 * Requests notification cards from the server.
728 * @return {Promise} A promise to request the notification cards.
729 * Rejects if the cards won't be requested.
731 function requestNotificationCards() {
732 console.log('requestNotificationCards');
733 return getGroupsToRequest()
734 .then(requestNotificationGroupsFromServer)
735 .then(processServerResponse)
736 .then(function(processedResponse) {
738 shouldShowExplanatoryCard() ? countExplanatoryCard : undefined;
739 return showNotificationGroups(
740 processedResponse.updatedGroups, onCardShown).then(function() {
741 chrome.storage.local.set({
742 notificationGroups: processedResponse.updatedGroups,
743 recentDismissals: processedResponse.updatedRecentDismissals
745 recordEvent(GoogleNowEvent.CARDS_PARSE_SUCCESS);
752 * Determines if an immediate retry should occur based off of the given groups.
753 * The NOR group is expected most often and less latency sensitive, so we will
754 * simply wait MAXIMUM_POLLING_PERIOD_SECONDS before trying again.
755 * @param {Array<string>} groupNames Names of groups that need to be refreshed.
756 * @return {boolean} Whether a retry should occur.
758 function shouldScheduleRetryFromGroupList(groupNames) {
759 return (groupNames.length != 1) || (groupNames[0] !== 'NOR');
763 * Requests and shows notification cards.
765 function requestCards() {
766 console.log('requestCards @' + new Date());
767 // LOCATION_REQUEST is a legacy histogram value when we requested location.
768 // This corresponds to the extension attempting to request for cards.
769 // We're keeping the name the same to keep our histograms in order.
770 recordEvent(GoogleNowEvent.LOCATION_REQUEST);
771 tasks.add(UPDATE_CARDS_TASK_NAME, function() {
772 console.log('requestCards-task-begin');
773 updateCardsAttempts.isRunning(function(running) {
775 // The cards are requested only if there are no unsent dismissals.
776 processPendingDismissals()
777 .then(requestNotificationCards)
779 return getGroupsToRequest().then(function(groupsToRequest) {
780 if (shouldScheduleRetryFromGroupList(groupsToRequest)) {
781 updateCardsAttempts.scheduleRetry();
791 * Sends a server request to dismiss a card.
792 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
794 * @param {number} dismissalTimeMs Time of the user's dismissal of the card in
795 * milliseconds since epoch.
796 * @param {DismissalData} dismissalData Data to build a dismissal request.
797 * @return {Promise} A promise to request the card dismissal, rejects on error.
799 function requestCardDismissal(
800 chromeNotificationId, dismissalTimeMs, dismissalData) {
801 console.log('requestDismissingCard ' + chromeNotificationId +
802 ' from ' + NOTIFICATION_CARDS_URL +
803 ', dismissalData=' + JSON.stringify(dismissalData));
805 var dismissalAge = Date.now() - dismissalTimeMs;
807 if (dismissalAge > MAXIMUM_DISMISSAL_AGE_MS) {
808 return Promise.resolve();
811 recordEvent(GoogleNowEvent.DISMISS_REQUEST_TOTAL);
813 var requestParameters = 'notifications/' + dismissalData.notificationId +
814 '?age=' + dismissalAge +
815 '&chromeNotificationId=' + chromeNotificationId;
817 for (var paramField in dismissalData.parameters)
818 requestParameters += ('&' + paramField +
819 '=' + dismissalData.parameters[paramField]);
821 console.log('requestCardDismissal: requestParameters=' + requestParameters);
823 return requestFromServer('DELETE', requestParameters).then(function(request) {
824 console.log('requestDismissingCard-onloadend ' + request.status);
825 if (request.status == HTTP_NOCONTENT)
826 recordEvent(GoogleNowEvent.DISMISS_REQUEST_SUCCESS);
828 // A dismissal doesn't require further retries if it was successful or
829 // doesn't have a chance for successful completion.
830 return (request.status == HTTP_NOCONTENT) ?
833 }).catch(function(request) {
834 request = (typeof request === 'object') ? request : {};
835 return (request.status == HTTP_BAD_REQUEST ||
836 request.status == HTTP_METHOD_NOT_ALLOWED) ?
843 * Tries to send dismiss requests for all pending dismissals.
844 * @return {Promise} A promise to process the pending dismissals.
845 * The promise is rejected if a problem was encountered.
847 function processPendingDismissals() {
848 return fillFromChromeLocalStorage({
849 /** @type {Array<PendingDismissal>} */
850 pendingDismissals: [],
851 /** @type {Object<ServerNotificationId, number>} */
853 }).then(function(items) {
855 'processPendingDismissals-storage-get ' + JSON.stringify(items));
857 var dismissalsChanged = false;
859 function onFinish(success) {
860 if (dismissalsChanged) {
861 chrome.storage.local.set({
862 pendingDismissals: items.pendingDismissals,
863 recentDismissals: items.recentDismissals
866 return success ? Promise.resolve() : Promise.reject();
869 function doProcessDismissals() {
870 if (items.pendingDismissals.length == 0) {
871 dismissalAttempts.stop();
872 return onFinish(true);
875 // Send dismissal for the first card, and if successful, repeat
876 // recursively with the rest.
877 /** @type {PendingDismissal} */
878 var dismissal = items.pendingDismissals[0];
879 return requestCardDismissal(
880 dismissal.chromeNotificationId,
882 dismissal.dismissalData).then(function() {
883 dismissalsChanged = true;
884 items.pendingDismissals.splice(0, 1);
885 items.recentDismissals[dismissal.dismissalData.notificationId] =
887 return doProcessDismissals();
888 }).catch(function() {
889 return onFinish(false);
893 return doProcessDismissals();
898 * Submits a task to send pending dismissals.
900 function retryPendingDismissals() {
901 tasks.add(RETRY_DISMISS_TASK_NAME, function() {
902 processPendingDismissals().catch(dismissalAttempts.scheduleRetry);
907 * Opens a URL in a new tab.
908 * @param {string} url URL to open.
910 function openUrl(url) {
911 instrumented.tabs.create({url: url}, function(tab) {
913 chrome.windows.update(tab.windowId, {focused: true});
915 chrome.windows.create({url: url, focused: true});
920 * Opens URL corresponding to the clicked part of the notification.
921 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
923 * @param {function(NotificationDataEntry): (string|undefined)} selector
924 * Function that extracts the url for the clicked area from the
925 * notification data entry.
927 function onNotificationClicked(chromeNotificationId, selector) {
928 fillFromChromeLocalStorage({
929 /** @type {Object<ChromeNotificationId, NotificationDataEntry>} */
930 notificationsData: {}
931 }).then(function(items) {
932 /** @type {(NotificationDataEntry|undefined)} */
933 var notificationDataEntry = items.notificationsData[chromeNotificationId];
934 if (!notificationDataEntry)
937 var url = selector(notificationDataEntry);
946 * Callback for chrome.notifications.onClosed event.
947 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
949 * @param {boolean} byUser Whether the notification was closed by the user.
951 function onNotificationClosed(chromeNotificationId, byUser) {
955 // At this point we are guaranteed that the notification is a now card.
956 chrome.metricsPrivate.recordUserAction('GoogleNow.Dismissed');
958 tasks.add(DISMISS_CARD_TASK_NAME, function() {
959 dismissalAttempts.start();
961 fillFromChromeLocalStorage({
962 /** @type {Array<PendingDismissal>} */
963 pendingDismissals: [],
964 /** @type {Object<ChromeNotificationId, NotificationDataEntry>} */
965 notificationsData: {},
966 /** @type {Object<StoredNotificationGroup>} */
967 notificationGroups: {}
968 }).then(function(items) {
969 /** @type {NotificationDataEntry} */
970 var notificationData =
971 items.notificationsData[chromeNotificationId] ||
973 timestamp: Date.now(),
977 var dismissalResult =
979 chromeNotificationId,
981 items.notificationGroups);
983 for (var i = 0; i < dismissalResult.dismissals.length; i++) {
984 /** @type {PendingDismissal} */
986 chromeNotificationId: chromeNotificationId,
988 dismissalData: dismissalResult.dismissals[i]
990 items.pendingDismissals.push(dismissal);
993 items.notificationsData[chromeNotificationId] =
994 dismissalResult.notificationData;
996 chrome.storage.local.set(items);
998 processPendingDismissals();
1004 * Initializes the polling system to start fetching cards.
1006 function startPollingCards() {
1007 console.log('startPollingCards');
1008 // Create an update timer for a case when for some reason requesting
1009 // cards gets stuck.
1010 updateCardsAttempts.start(MAXIMUM_POLLING_PERIOD_SECONDS);
1015 * Stops all machinery in the polling system.
1017 function stopPollingCards() {
1018 console.log('stopPollingCards');
1019 updateCardsAttempts.stop();
1020 // Since we're stopping everything, clear all runtime storage.
1021 // We don't clear localStorage since those values are still relevant
1022 // across Google Now start-stop events.
1023 chrome.storage.local.clear();
1027 * Initializes the event page on install or on browser startup.
1029 function initialize() {
1030 recordEvent(GoogleNowEvent.EXTENSION_START);
1035 * Starts or stops the main pipeline for polling cards.
1036 * @param {boolean} shouldPollCardsRequest true to start and
1037 * false to stop polling cards.
1039 function setShouldPollCards(shouldPollCardsRequest) {
1040 updateCardsAttempts.isRunning(function(currentValue) {
1041 if (shouldPollCardsRequest != currentValue) {
1042 console.log('Action Taken setShouldPollCards=' + shouldPollCardsRequest);
1043 if (shouldPollCardsRequest)
1044 startPollingCards();
1049 'Action Ignored setShouldPollCards=' + shouldPollCardsRequest);
1055 * Starts or stops the optin check and GCM channel to receive optin
1057 * @param {boolean} shouldPollOptInStatus true to start and false to stop
1058 * polling the optin status.
1060 function setShouldPollOptInStatus(shouldPollOptInStatus) {
1061 optInPollAttempts.isRunning(function(currentValue) {
1062 if (shouldPollOptInStatus != currentValue) {
1064 'Action Taken setShouldPollOptInStatus=' + shouldPollOptInStatus);
1065 if (shouldPollOptInStatus) {
1066 pollOptedInNoImmediateRecheck();
1068 optInPollAttempts.stop();
1072 'Action Ignored setShouldPollOptInStatus=' + shouldPollOptInStatus);
1076 if (shouldPollOptInStatus) {
1079 unregisterFromGcm();
1084 * Enables or disables the Google Now background permission.
1085 * @param {boolean} backgroundEnable true to run in the background.
1086 * false to not run in the background.
1088 function setBackgroundEnable(backgroundEnable) {
1089 instrumented.permissions.contains({permissions: ['background']},
1090 function(hasPermission) {
1091 if (backgroundEnable != hasPermission) {
1092 console.log('Action Taken setBackgroundEnable=' + backgroundEnable);
1093 if (backgroundEnable)
1094 chrome.permissions.request({permissions: ['background']});
1096 chrome.permissions.remove({permissions: ['background']});
1098 console.log('Action Ignored setBackgroundEnable=' + backgroundEnable);
1104 * Record why this extension would not poll for cards.
1105 * @param {boolean} signedIn true if the user is signed in.
1106 * @param {boolean} notificationEnabled true if
1107 * Google Now for Chrome is allowed to show notifications.
1108 * @param {boolean} googleNowEnabled true if
1109 * the Google Now is enabled for the user.
1111 function recordEventIfNoCards(signedIn, notificationEnabled, googleNowEnabled) {
1113 recordEvent(GoogleNowEvent.SIGNED_OUT);
1114 } else if (!notificationEnabled) {
1115 recordEvent(GoogleNowEvent.NOTIFICATION_DISABLED);
1116 } else if (!googleNowEnabled) {
1117 recordEvent(GoogleNowEvent.GOOGLE_NOW_DISABLED);
1122 * Does the actual work of deciding what Google Now should do
1123 * based off of the current state of Chrome.
1124 * @param {boolean} signedIn true if the user is signed in.
1125 * @param {boolean} canEnableBackground true if
1126 * the background permission can be requested.
1127 * @param {boolean} notificationEnabled true if
1128 * Google Now for Chrome is allowed to show notifications.
1129 * @param {boolean} googleNowEnabled true if
1130 * the Google Now is enabled for the user.
1132 function updateRunningState(
1134 canEnableBackground,
1135 notificationEnabled,
1138 'State Update signedIn=' + signedIn + ' ' +
1139 'canEnableBackground=' + canEnableBackground + ' ' +
1140 'notificationEnabled=' + notificationEnabled + ' ' +
1141 'googleNowEnabled=' + googleNowEnabled);
1143 var shouldPollCards = false;
1144 var shouldPollOptInStatus = false;
1145 var shouldSetBackground = false;
1147 if (signedIn && notificationEnabled) {
1148 shouldPollCards = googleNowEnabled;
1149 shouldPollOptInStatus = !googleNowEnabled;
1150 shouldSetBackground = canEnableBackground && googleNowEnabled;
1152 recordEvent(GoogleNowEvent.STOPPED);
1155 recordEventIfNoCards(signedIn, notificationEnabled, googleNowEnabled);
1158 'Requested Actions shouldSetBackground=' + shouldSetBackground + ' ' +
1159 'setShouldPollCards=' + shouldPollCards + ' ' +
1160 'shouldPollOptInStatus=' + shouldPollOptInStatus);
1162 setBackgroundEnable(shouldSetBackground);
1163 setShouldPollCards(shouldPollCards);
1164 setShouldPollOptInStatus(shouldPollOptInStatus);
1165 if (!shouldPollCards) {
1171 * Coordinates the behavior of Google Now for Chrome depending on
1172 * Chrome and extension state.
1174 function onStateChange() {
1175 tasks.add(STATE_CHANGED_TASK_NAME, function() {
1177 authenticationManager.isSignedIn(),
1178 canEnableBackground(),
1179 isNotificationsEnabled(),
1180 isGoogleNowEnabled()])
1181 .then(function(results) {
1182 updateRunningState.apply(null, results);
1188 * Determines if background mode should be requested.
1189 * @return {Promise} A promise to determine if background can be enabled.
1191 function canEnableBackground() {
1192 return new Promise(function(resolve) {
1193 instrumented.metricsPrivate.getVariationParams(
1195 function(response) {
1196 resolve(!response || (response.canEnableBackground != 'false'));
1202 * Checks if Google Now is enabled in the notifications center.
1203 * @return {Promise} A promise to determine if Google Now is enabled
1204 * in the notifications center.
1206 function isNotificationsEnabled() {
1207 return new Promise(function(resolve) {
1208 instrumented.notifications.getPermissionLevel(function(level) {
1209 resolve(level == 'granted');
1215 * Gets the previous Google Now opt-in state.
1216 * @return {Promise} A promise to determine the previous Google Now
1219 function isGoogleNowEnabled() {
1220 return fillFromChromeLocalStorage({googleNowEnabled: false})
1221 .then(function(items) {
1222 return items.googleNowEnabled;
1227 * Ensures the extension is ready to listen for GCM messages.
1229 function registerForGcm() {
1230 // We don't need to use the key yet, just ensure the channel is set up.
1231 getGcmNotificationKey();
1235 * Returns a Promise resolving to either a cached or new GCM notification key.
1236 * Rejects if registration fails.
1237 * @return {Promise} A Promise that resolves to a potentially-cached GCM key.
1239 function getGcmNotificationKey() {
1240 return fillFromChromeLocalStorage({gcmNotificationKey: undefined})
1241 .then(function(items) {
1242 if (items.gcmNotificationKey) {
1243 console.log('Reused gcm key from storage.');
1244 return Promise.resolve(items.gcmNotificationKey);
1246 return requestNewGcmNotificationKey();
1251 * Returns a promise resolving to a GCM Notificaiton Key. May call
1252 * chrome.gcm.register() first if required. Rejects on registration failure.
1253 * @return {Promise} A Promise that resolves to a fresh GCM Notification key.
1255 function requestNewGcmNotificationKey() {
1256 return getGcmRegistrationId().then(function(gcmId) {
1257 authenticationManager.getAuthToken().then(function(token) {
1258 authenticationManager.getLogin().then(function(username) {
1259 return new Promise(function(resolve, reject) {
1260 var xhr = new XMLHttpRequest();
1261 xhr.responseType = 'application/json';
1262 xhr.open('POST', GCM_REGISTRATION_URL, true);
1263 xhr.setRequestHeader('Content-Type', 'application/json');
1264 xhr.setRequestHeader('Authorization', 'Bearer ' + token);
1265 xhr.setRequestHeader('project_id', GCM_PROJECT_ID);
1268 'notification_key_name': username,
1269 'registration_ids': [gcmId]
1271 xhr.onloadend = function() {
1272 if (xhr.status != 200) {
1275 var obj = JSON.parse(xhr.responseText);
1276 var key = obj && obj.notification_key;
1280 console.log('gcm notification key POST: ' + key);
1281 chrome.storage.local.set({gcmNotificationKey: key});
1284 xhr.send(JSON.stringify(payload));
1287 }).catch(function() {
1288 // Couldn't obtain a GCM ID. Ignore and fallback to polling.
1294 * Returns a promise resolving to either a cached or new GCM registration ID.
1295 * Rejects if registration fails.
1296 * @return {Promise} A Promise that resolves to a GCM registration ID.
1298 function getGcmRegistrationId() {
1299 return fillFromChromeLocalStorage({gcmRegistrationId: undefined})
1300 .then(function(items) {
1301 if (items.gcmRegistrationId) {
1302 console.log('Reused gcm registration id from storage.');
1303 return Promise.resolve(items.gcmRegistrationId);
1306 return new Promise(function(resolve, reject) {
1307 instrumented.gcm.register([GCM_PROJECT_ID], function(registrationId) {
1308 console.log('gcm.register(): ' + registrationId);
1309 if (registrationId) {
1310 chrome.storage.local.set({gcmRegistrationId: registrationId});
1311 resolve(registrationId);
1321 * Unregisters from GCM if previously registered.
1323 function unregisterFromGcm() {
1324 fillFromChromeLocalStorage({gcmRegistrationId: undefined})
1325 .then(function(items) {
1326 if (items.gcmRegistrationId) {
1327 console.log('Unregistering from gcm.');
1328 instrumented.gcm.unregister(function() {
1329 if (!chrome.runtime.lastError) {
1330 chrome.storage.local.remove(
1331 ['gcmNotificationKey', 'gcmRegistrationId']);
1339 * Polls the optin state.
1340 * Sometimes we get the response to the opted in result too soon during
1341 * push messaging. We'll recheck the optin state a few times before giving up.
1343 function pollOptedInWithRecheck() {
1345 * Cleans up any state used to recheck the opt-in poll.
1347 function clearPollingState() {
1348 localStorage.removeItem('optedInCheckCount');
1349 optInRecheckAttempts.stop();
1352 if (localStorage.optedInCheckCount === undefined) {
1353 localStorage.optedInCheckCount = 0;
1354 optInRecheckAttempts.start();
1357 console.log(new Date() +
1358 ' checkOptedIn Attempt ' + localStorage.optedInCheckCount);
1360 requestAndUpdateOptedIn().then(function(optedIn) {
1362 clearPollingState();
1363 return Promise.resolve();
1365 // If we're not opted in, reject to retry.
1366 return Promise.reject();
1368 }).catch(function() {
1369 if (localStorage.optedInCheckCount < 5) {
1370 localStorage.optedInCheckCount++;
1371 optInRecheckAttempts.scheduleRetry();
1373 clearPollingState();
1378 instrumented.runtime.onInstalled.addListener(function(details) {
1379 console.log('onInstalled ' + JSON.stringify(details));
1380 if (details.reason != 'chrome_update') {
1385 instrumented.runtime.onStartup.addListener(function() {
1386 console.log('onStartup');
1388 // Show notifications received by earlier polls. Doing this as early as
1389 // possible to reduce latency of showing first notifications. This mimics how
1390 // persistent notifications will work.
1391 tasks.add(SHOW_ON_START_TASK_NAME, function() {
1392 fillFromChromeLocalStorage({
1393 /** @type {Object<StoredNotificationGroup>} */
1394 notificationGroups: {}
1395 }).then(function(items) {
1396 console.log('onStartup-get ' + JSON.stringify(items));
1398 showNotificationGroups(items.notificationGroups).then(function() {
1399 chrome.storage.local.set(items);
1407 authenticationManager.addListener(function() {
1408 console.log('signIn State Change');
1412 instrumented.notifications.onClicked.addListener(
1413 function(chromeNotificationId) {
1414 chrome.metricsPrivate.recordUserAction('GoogleNow.MessageClicked');
1415 onNotificationClicked(chromeNotificationId,
1416 function(notificationDataEntry) {
1417 var actionUrls = notificationDataEntry.actionUrls;
1418 var url = actionUrls && actionUrls.messageUrl;
1420 recordNotificationClick(notificationDataEntry.cardTypeId);
1426 instrumented.notifications.onButtonClicked.addListener(
1427 function(chromeNotificationId, buttonIndex) {
1428 chrome.metricsPrivate.recordUserAction(
1429 'GoogleNow.ButtonClicked' + buttonIndex);
1430 onNotificationClicked(chromeNotificationId,
1431 function(notificationDataEntry) {
1432 var actionUrls = notificationDataEntry.actionUrls;
1433 var url = actionUrls.buttonUrls[buttonIndex];
1435 recordButtonClick(notificationDataEntry.cardTypeId, buttonIndex);
1437 verify(false, 'onButtonClicked: no url for a button');
1439 'buttonIndex=' + buttonIndex + ' ' +
1440 'chromeNotificationId=' + chromeNotificationId + ' ' +
1441 'notificationDataEntry=' +
1442 JSON.stringify(notificationDataEntry));
1448 instrumented.notifications.onClosed.addListener(onNotificationClosed);
1450 instrumented.notifications.onPermissionLevelChanged.addListener(
1451 function(permissionLevel) {
1452 console.log('Notifications permissionLevel Change');
1456 instrumented.notifications.onShowSettings.addListener(function() {
1457 openUrl(SETTINGS_URL);
1460 // Handles state change notifications for the Google Now enabled bit.
1461 instrumented.storage.onChanged.addListener(function(changes, areaName) {
1462 if (areaName === 'local') {
1463 if ('googleNowEnabled' in changes) {
1469 instrumented.gcm.onMessage.addListener(function(message) {
1470 console.log('gcm.onMessage ' + JSON.stringify(message));
1471 if (!message || !message.data) {
1475 var payload = message.data.payload;
1476 var tag = message.data.tag;
1477 if (payload.indexOf('REQUEST_CARDS') == 0) {
1478 tasks.add(ON_PUSH_MESSAGE_START_TASK_NAME, function() {
1479 // Accept promise rejection on failure since it's safer to do nothing,
1480 // preventing polling the server when the payload really didn't change.
1481 fillFromChromeLocalStorage({
1482 lastPollNowPayloads: {},
1483 /** @type {Object<StoredNotificationGroup>} */
1484 notificationGroups: {}
1485 }, PromiseRejection.ALLOW).then(function(items) {
1486 if (items.lastPollNowPayloads[tag] != payload) {
1487 items.lastPollNowPayloads[tag] = payload;
1489 items.notificationGroups['PUSH' + tag] = {
1491 nextPollTime: Date.now()
1494 chrome.storage.local.set({
1495 lastPollNowPayloads: items.lastPollNowPayloads,
1496 notificationGroups: items.notificationGroups
1499 pollOptedInWithRecheck();