Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / chrome / browser / resources / google_now / background.js
blob7ef5d026052dfa8ba6e29f1057989c25d1cfd44e
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.
5 'use strict';
7 /**
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
14  *    server.
15  * 2. Making a server request.
16  * 3. Showing the received cards as notifications.
17  */
19 // TODO(robliao): Decide what to do in incognito mode.
21 /**
22  * Standard response code for successful HTTP requests. This is the only success
23  * code the server will send.
24  */
25 var HTTP_OK = 200;
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;
36 /**
37  * Initial period for polling for Google Now Notifications cards to use when the
38  * period from the server is not available.
39  */
40 var INITIAL_POLLING_PERIOD_SECONDS = 5 * 60;  // 5 minutes
42 /**
43  * Mininal period for polling for Google Now Notifications cards.
44  */
45 var MINIMUM_POLLING_PERIOD_SECONDS = 5 * 60;  // 5 minutes
47 /**
48  * Maximal period for polling for Google Now Notifications cards to use when the
49  * period from the server is not available.
50  */
51 var MAXIMUM_POLLING_PERIOD_SECONDS = 30 * 60;  // 30 minutes
53 /**
54  * Initial period for polling for Google Now optin notification after push
55  * messaging indicates Google Now is enabled.
56  */
57 var INITIAL_OPTIN_RECHECK_PERIOD_SECONDS = 60;  // 1 minute
59 /**
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.
63  */
64 var MAXIMUM_OPTIN_RECHECK_PERIOD_SECONDS = 16 * 60;  // 16 minutes
66 /**
67  * Initial period for retrying the server request for dismissing cards.
68  */
69 var INITIAL_RETRY_DISMISS_PERIOD_SECONDS = 60;  // 1 minute
71 /**
72  * Maximum period for retrying the server request for dismissing cards.
73  */
74 var MAXIMUM_RETRY_DISMISS_PERIOD_SECONDS = 60 * 60;  // 1 hour
76 /**
77  * Time we keep retrying dismissals.
78  */
79 var MAXIMUM_DISMISSAL_AGE_MS = 24 * 60 * 60 * 1000; // 1 day
81 /**
82  * Time we keep dismissals after successful server dismiss requests.
83  */
84 var DISMISS_RETENTION_TIME_MS = 20 * 60 * 1000;  // 20 minutes
86 /**
87  * Default period for checking whether the user is opted in to Google Now.
88  */
89 var DEFAULT_OPTIN_CHECK_PERIOD_SECONDS = 60 * 60 * 24 * 7; // 1 week
91 /**
92  * URL to open when the user clicked on a link for the our notification
93  * settings.
94  */
95 var SETTINGS_URL = 'https://support.google.com/chrome/?p=ib_google_now_welcome';
97 /**
98  * GCM registration URL.
99  */
100 var GCM_REGISTRATION_URL =
101     'https://android.googleapis.com/gcm/googlenotification';
104  * DevConsole project ID for GCM API use.
105  */
106 var GCM_PROJECT_ID = '437902709571';
109  * Number of cards that need an explanatory link.
110  */
111 var EXPLANATORY_CARDS_LINK_THRESHOLD = 4;
114  * Names for tasks that can be created by the extension.
115  */
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.
126  * @typedef {{
127  *   nextPollSeconds: (string|undefined),
128  *   rank: (number|undefined),
129  *   requested: (boolean|undefined)
130  * }}
131  */
132 var ReceivedGroup;
135  * Server response with notifications and groups.
137  * @typedef {{
138  *   googleNowDisabled: (boolean|undefined),
139  *   groups: Object<ReceivedGroup>,
140  *   notifications: Array<ReceivedNotification>
141  * }}
142  */
143 var ServerResponse;
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.
152  * @typedef {{
153  *   cards: Array<ReceivedNotification>,
154  *   cardsTimestamp: (number|undefined),
155  *   nextPollTime: (number|undefined),
156  *   rank: (number|undefined)
157  * }}
158  */
159 var StoredNotificationGroup;
162  * Pending (not yet successfully sent) dismissal for a received notification.
163  * |time| is the moment when the user requested dismissal.
165  * @typedef {{
166  *   chromeNotificationId: ChromeNotificationId,
167  *   time: number,
168  *   dismissalData: DismissalData
169  * }}
170  */
171 var PendingDismissal;
174  * Checks if a new task can't be scheduled when another task is already
175  * scheduled.
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.
179  */
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.
185     return true;
186   }
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.
194     return true;
195   }
197   return false;
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(
227     'cards-update',
228     requestCards,
229     INITIAL_POLLING_PERIOD_SECONDS,
230     MAXIMUM_POLLING_PERIOD_SECONDS);
231 var optInPollAttempts = buildAttemptManager(
232     'optin',
233     pollOptedInNoImmediateRecheck,
234     INITIAL_POLLING_PERIOD_SECONDS,
235     MAXIMUM_POLLING_PERIOD_SECONDS);
236 var optInRecheckAttempts = buildAttemptManager(
237     'optin-recheck',
238     pollOptedInWithRecheck,
239     INITIAL_OPTIN_RECHECK_PERIOD_SECONDS,
240     MAXIMUM_OPTIN_RECHECK_PERIOD_SECONDS);
241 var dismissalAttempts = buildAttemptManager(
242     'dismiss',
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.
252  * @enum {number}
253  */
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,
260   LOCATION_REQUEST: 5,
261   DELETED_LOCATION_UPDATE: 6,
262   EXTENSION_START: 7,
263   DELETED_SHOW_WELCOME_TOAST: 8,
264   STOPPED: 9,
265   DELETED_USER_SUPPRESSED: 10,
266   SIGNED_OUT: 11,
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
270                     // added before it.
274  * Records a Google Now Event.
275  * @param {GoogleNowEvent} event Event identifier.
276  */
277 function recordEvent(event) {
278   var metricDescription = {
279     metricName: 'GoogleNow.Event',
280     type: 'histogram-linear',
281     min: 1,
282     max: GoogleNowEvent.EVENTS_TOTAL,
283     buckets: GoogleNowEvent.EVENTS_TOTAL + 1
284   };
286   chrome.metricsPrivate.recordValue(metricDescription, event);
290  * Records a notification clicked event.
291  * @param {number|undefined} cardTypeId Card type ID.
292  */
293 function recordNotificationClick(cardTypeId) {
294   if (cardTypeId !== undefined) {
295     chrome.metricsPrivate.recordSparseValue(
296         'GoogleNow.Card.Clicked', cardTypeId);
297   }
301  * Records a button clicked event.
302  * @param {number|undefined} cardTypeId Card type ID.
303  * @param {number} buttonIndex Button Index
304  */
305 function recordButtonClick(cardTypeId, buttonIndex) {
306   if (cardTypeId !== undefined) {
307     chrome.metricsPrivate.recordSparseValue(
308         'GoogleNow.Card.Button.Clicked' + buttonIndex, cardTypeId);
309   }
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
316  *     XMLHttpRequest.
317  * @return {function(XMLHttpRequest)} Function that validates the token with the
318  *     supplied XMLHttpRequest.
319  */
320 function checkAuthenticationStatus(token) {
321   return function(request) {
322     if (request.status == HTTP_FORBIDDEN ||
323         request.status == HTTP_UNAUTHORIZED) {
324       authenticationManager.removeToken(token);
325     }
326   }
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.
336  */
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)) {
344           resolve(request);
345         } else {
346           reject(request);
347         }
348       }, false);
349       request.send();
350     });
351     requestPromise.catch(checkAuthenticationStatus(token));
352     return requestPromise;
353   });
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.
363  */
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
376       // an empty update.
377       for (var chromeNotificationId in notifications) {
378         cards[chromeNotificationId] = cards[chromeNotificationId] || [];
379       }
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],
389             notificationGroups,
390             opt_onCardShown);
391       }
392       chrome.storage.local.set({notificationsData: notificationsData});
393       resolve();
394     });
395   });
399  * Removes all cards and card state on Google Now close down.
400  */
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() {});
411     }
412     chrome.storage.local.remove(['notificationsData', 'notificationGroups']);
413   });
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
422  *     combined card set.
423  */
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
437     };
439     var combinedCard =
440         combinedCards[receivedNotification.chromeNotificationId] || [];
441     combinedCard.push(uncombinedNotification);
442     combinedCards[receivedNotification.chromeNotificationId] = combinedCard;
443   }
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
449  *     information.
450  * @return {number} The next poll time based off of the groups.
451  */
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);
460     }
461   }
463   // At least one of the groups must have nextPollTime.
464   verify(nextPollTime != null, 'calculateNextPollTime: nextPollTime is null');
465   return nextPollTime;
469  * Schedules next cards poll.
470  * @param {Object<StoredNotificationGroup>} groups Map from group name to group
471  *     information.
472  */
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.
484  */
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);
492   });
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.
500  */
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.
517  */
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();
525   }
527   var receivedGroups = response.groups;
529   return fillFromChromeLocalStorage({
530     /** @type {Object<StoredNotificationGroup>} */
531     notificationGroups: {},
532     /** @type {Object<ServerNotificationId, number>} */
533     recentDismissals: {}
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];
547       }
548     }
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);
559         }
560       }
561     }
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] || {
569         cards: [],
570         cardsTimestamp: undefined,
571         nextPollTime: undefined,
572         rank: undefined
573       };
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
580         // values.
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.
589       }
591       // 'nextPollSeconds' may be sent even for groups that don't contain
592       // cards updates.
593       if (receivedGroup.nextPollSeconds !== undefined) {
594         storedGroup.nextPollTime =
595             now + receivedGroup.nextPollSeconds * MS_IN_SECOND;
596       }
598       updatedGroups[groupName] = storedGroup;
599     }
601     scheduleNextCardsPoll(updatedGroups);
602     return {
603       updatedGroups: updatedGroups,
604       recentDismissals: updatedRecentDismissals
605     };
606   });
610  * Update the Explanatory Total Cards Shown Count.
611  */
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.
619  */
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.
630  */
631 function requestNotificationGroupsFromServer(groupNames) {
632   console.log(
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';
643   }
645   groupNames.forEach(function(groupName) {
646     requestParameters += ('&requestTypes=' + groupName);
647   });
649   requestParameters += '&uiLocale=' + navigator.language;
651   console.log(
652       'requestNotificationGroupsFromServer: request=' + requestParameters);
654   return requestFromServer('GET', 'notifications' + requestParameters).then(
655     function(request) {
656       console.log(
657           'requestNotificationGroupsFromServer-received ' + request.status);
658       if (request.status == HTTP_OK) {
659         recordEvent(GoogleNowEvent.REQUEST_FOR_CARDS_SUCCESS);
660         return JSON.parse(request.responseText);
661       }
662     });
666  * Performs an opt-in poll without an immediate recheck.
667  * If the response is not opted-in, schedule an opt-in check poll.
668  */
669 function pollOptedInNoImmediateRecheck() {
670   requestAndUpdateOptedIn()
671       .then(function(optedIn) {
672         if (!optedIn) {
673           // Request a repoll if we're not opted in.
674           return Promise.reject();
675         }
676       })
677       .catch(function() {
678         scheduleOptInCheckPoll();
679       });
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.
687  */
688 function requestAndUpdateOptedIn() {
689   console.log('requestOptedIn from ' + NOTIFICATION_CARDS_URL);
691   return requestFromServer('GET', 'settings/optin').then(function(request) {
692     console.log(
693         'requestOptedIn-received ' + request.status + ' ' + request.response);
694     if (request.status == HTTP_OK) {
695       var parsedResponse = JSON.parse(request.responseText);
696       return parsedResponse.value;
697     }
698   }).then(function(optedIn) {
699     chrome.storage.local.set({googleNowEnabled: optedIn});
700     return optedIn;
701   });
705  * Determines the groups that need to be requested right now.
706  * @return {Promise} A promise to determine the groups to request.
707  */
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);
721     }
722     return groupsToRequest;
723   });
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.
730  */
731 function requestNotificationCards() {
732   console.log('requestNotificationCards');
733   return getGroupsToRequest()
734       .then(requestNotificationGroupsFromServer)
735       .then(processServerResponse)
736       .then(function(processedResponse) {
737         var onCardShown =
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
744               });
745               recordEvent(GoogleNowEvent.CARDS_PARSE_SUCCESS);
746             }
747           );
748       });
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.
757  */
758 function shouldScheduleRetryFromGroupList(groupNames) {
759   return (groupNames.length != 1) || (groupNames[0] !== 'NOR');
763  * Requests and shows notification cards.
764  */
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) {
774       if (running) {
775         // The cards are requested only if there are no unsent dismissals.
776         processPendingDismissals()
777             .then(requestNotificationCards)
778             .catch(function() {
779               return getGroupsToRequest().then(function(groupsToRequest) {
780                 if (shouldScheduleRetryFromGroupList(groupsToRequest)) {
781                   updateCardsAttempts.scheduleRetry();
782                 }
783               });
784             });
785       }
786     });
787   });
791  * Sends a server request to dismiss a card.
792  * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
793  *     the card.
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.
798  */
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();
809   }
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) ?
831            Promise.resolve() :
832            Promise.reject();
833   }).catch(function(request) {
834     request = (typeof request === 'object') ? request : {};
835     return (request.status == HTTP_BAD_REQUEST ||
836            request.status == HTTP_METHOD_NOT_ALLOWED) ?
837            Promise.resolve() :
838            Promise.reject();
839   });
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.
846  */
847 function processPendingDismissals() {
848   return fillFromChromeLocalStorage({
849     /** @type {Array<PendingDismissal>} */
850     pendingDismissals: [],
851     /** @type {Object<ServerNotificationId, number>} */
852     recentDismissals: {}
853   }).then(function(items) {
854     console.log(
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
864         });
865       }
866       return success ? Promise.resolve() : Promise.reject();
867     }
869     function doProcessDismissals() {
870       if (items.pendingDismissals.length == 0) {
871         dismissalAttempts.stop();
872         return onFinish(true);
873       }
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,
881           dismissal.time,
882           dismissal.dismissalData).then(function() {
883             dismissalsChanged = true;
884             items.pendingDismissals.splice(0, 1);
885             items.recentDismissals[dismissal.dismissalData.notificationId] =
886                 Date.now();
887             return doProcessDismissals();
888           }).catch(function() {
889             return onFinish(false);
890           });
891     }
893     return doProcessDismissals();
894   });
898  * Submits a task to send pending dismissals.
899  */
900 function retryPendingDismissals() {
901   tasks.add(RETRY_DISMISS_TASK_NAME, function() {
902     processPendingDismissals().catch(dismissalAttempts.scheduleRetry);
903   });
907  * Opens a URL in a new tab.
908  * @param {string} url URL to open.
909  */
910 function openUrl(url) {
911   instrumented.tabs.create({url: url}, function(tab) {
912     if (tab)
913       chrome.windows.update(tab.windowId, {focused: true});
914     else
915       chrome.windows.create({url: url, focused: true});
916   });
920  * Opens URL corresponding to the clicked part of the notification.
921  * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
922  *     the card.
923  * @param {function(NotificationDataEntry): (string|undefined)} selector
924  *     Function that extracts the url for the clicked area from the
925  *     notification data entry.
926  */
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)
935       return;
937     var url = selector(notificationDataEntry);
938     if (!url)
939       return;
941     openUrl(url);
942   });
946  * Callback for chrome.notifications.onClosed event.
947  * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
948  *     the card.
949  * @param {boolean} byUser Whether the notification was closed by the user.
950  */
951 function onNotificationClosed(chromeNotificationId, byUser) {
952   if (!byUser)
953     return;
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] ||
972           {
973             timestamp: Date.now(),
974             combinedCard: []
975           };
977       var dismissalResult =
978           cardSet.onDismissal(
979               chromeNotificationId,
980               notificationData,
981               items.notificationGroups);
983       for (var i = 0; i < dismissalResult.dismissals.length; i++) {
984         /** @type {PendingDismissal} */
985         var dismissal = {
986           chromeNotificationId: chromeNotificationId,
987           time: Date.now(),
988           dismissalData: dismissalResult.dismissals[i]
989         };
990         items.pendingDismissals.push(dismissal);
991       }
993       items.notificationsData[chromeNotificationId] =
994           dismissalResult.notificationData;
996       chrome.storage.local.set(items);
998       processPendingDismissals();
999     });
1000   });
1004  * Initializes the polling system to start fetching cards.
1005  */
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);
1011   requestCards();
1015  * Stops all machinery in the polling system.
1016  */
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.
1028  */
1029 function initialize() {
1030   recordEvent(GoogleNowEvent.EXTENSION_START);
1031   onStateChange();
1035  * Starts or stops the main pipeline for polling cards.
1036  * @param {boolean} shouldPollCardsRequest true to start and
1037  *     false to stop polling cards.
1038  */
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();
1045       else
1046         stopPollingCards();
1047     } else {
1048       console.log(
1049           'Action Ignored setShouldPollCards=' + shouldPollCardsRequest);
1050     }
1051   });
1055  * Starts or stops the optin check and GCM channel to receive optin
1056  * notifications.
1057  * @param {boolean} shouldPollOptInStatus true to start and false to stop
1058  *     polling the optin status.
1059  */
1060 function setShouldPollOptInStatus(shouldPollOptInStatus) {
1061   optInPollAttempts.isRunning(function(currentValue) {
1062     if (shouldPollOptInStatus != currentValue) {
1063       console.log(
1064           'Action Taken setShouldPollOptInStatus=' + shouldPollOptInStatus);
1065       if (shouldPollOptInStatus) {
1066         pollOptedInNoImmediateRecheck();
1067       } else {
1068         optInPollAttempts.stop();
1069       }
1070     } else {
1071       console.log(
1072           'Action Ignored setShouldPollOptInStatus=' + shouldPollOptInStatus);
1073     }
1074   });
1076   if (shouldPollOptInStatus) {
1077     registerForGcm();
1078   } else {
1079     unregisterFromGcm();
1080   }
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.
1087  */
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']});
1095           else
1096             chrome.permissions.remove({permissions: ['background']});
1097         } else {
1098           console.log('Action Ignored setBackgroundEnable=' + backgroundEnable);
1099         }
1100       });
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.
1110  */
1111 function recordEventIfNoCards(signedIn, notificationEnabled, googleNowEnabled) {
1112   if (!signedIn) {
1113     recordEvent(GoogleNowEvent.SIGNED_OUT);
1114   } else if (!notificationEnabled) {
1115     recordEvent(GoogleNowEvent.NOTIFICATION_DISABLED);
1116   } else if (!googleNowEnabled) {
1117     recordEvent(GoogleNowEvent.GOOGLE_NOW_DISABLED);
1118   }
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.
1131  */
1132 function updateRunningState(
1133     signedIn,
1134     canEnableBackground,
1135     notificationEnabled,
1136     googleNowEnabled) {
1137   console.log(
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;
1151   } else {
1152     recordEvent(GoogleNowEvent.STOPPED);
1153   }
1155   recordEventIfNoCards(signedIn, notificationEnabled, googleNowEnabled);
1157   console.log(
1158       'Requested Actions shouldSetBackground=' + shouldSetBackground + ' ' +
1159       'setShouldPollCards=' + shouldPollCards + ' ' +
1160       'shouldPollOptInStatus=' + shouldPollOptInStatus);
1162   setBackgroundEnable(shouldSetBackground);
1163   setShouldPollCards(shouldPollCards);
1164   setShouldPollOptInStatus(shouldPollOptInStatus);
1165   if (!shouldPollCards) {
1166     removeAllCards();
1167   }
1171  * Coordinates the behavior of Google Now for Chrome depending on
1172  * Chrome and extension state.
1173  */
1174 function onStateChange() {
1175   tasks.add(STATE_CHANGED_TASK_NAME, function() {
1176     Promise.all([
1177         authenticationManager.isSignedIn(),
1178         canEnableBackground(),
1179         isNotificationsEnabled(),
1180         isGoogleNowEnabled()])
1181         .then(function(results) {
1182           updateRunningState.apply(null, results);
1183         });
1184   });
1188  * Determines if background mode should be requested.
1189  * @return {Promise} A promise to determine if background can be enabled.
1190  */
1191 function canEnableBackground() {
1192   return new Promise(function(resolve) {
1193     instrumented.metricsPrivate.getVariationParams(
1194         'GoogleNow',
1195         function(response) {
1196           resolve(!response || (response.canEnableBackground != 'false'));
1197         });
1198   });
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.
1205  */
1206 function isNotificationsEnabled() {
1207   return new Promise(function(resolve) {
1208     instrumented.notifications.getPermissionLevel(function(level) {
1209       resolve(level == 'granted');
1210     });
1211   });
1215  * Gets the previous Google Now opt-in state.
1216  * @return {Promise} A promise to determine the previous Google Now
1217  *     opt-in state.
1218  */
1219 function isGoogleNowEnabled() {
1220   return fillFromChromeLocalStorage({googleNowEnabled: false})
1221       .then(function(items) {
1222         return items.googleNowEnabled;
1223       });
1227  * Ensures the extension is ready to listen for GCM messages.
1228  */
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.
1238  */
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);
1245         }
1246         return requestNewGcmNotificationKey();
1247       });
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.
1254  */
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);
1266           var payload = {
1267             'operation': 'add',
1268             'notification_key_name': username,
1269             'registration_ids': [gcmId]
1270           };
1271           xhr.onloadend = function() {
1272             if (xhr.status != 200) {
1273               reject();
1274             }
1275             var obj = JSON.parse(xhr.responseText);
1276             var key = obj && obj.notification_key;
1277             if (!key) {
1278               reject();
1279             }
1280             console.log('gcm notification key POST: ' + key);
1281             chrome.storage.local.set({gcmNotificationKey: key});
1282             resolve(key);
1283           };
1284           xhr.send(JSON.stringify(payload));
1285         });
1286       });
1287     }).catch(function() {
1288       // Couldn't obtain a GCM ID. Ignore and fallback to polling.
1289     });
1290   });
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.
1297  */
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);
1304         }
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);
1312             } else {
1313               reject();
1314             }
1315           });
1316         });
1317       });
1321  * Unregisters from GCM if previously registered.
1322  */
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']);
1332             }
1333           });
1334         }
1335       });
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.
1342  */
1343 function pollOptedInWithRecheck() {
1344   /**
1345    * Cleans up any state used to recheck the opt-in poll.
1346    */
1347   function clearPollingState() {
1348     localStorage.removeItem('optedInCheckCount');
1349     optInRecheckAttempts.stop();
1350   }
1352   if (localStorage.optedInCheckCount === undefined) {
1353     localStorage.optedInCheckCount = 0;
1354     optInRecheckAttempts.start();
1355   }
1357   console.log(new Date() +
1358       ' checkOptedIn Attempt ' + localStorage.optedInCheckCount);
1360   requestAndUpdateOptedIn().then(function(optedIn) {
1361     if (optedIn) {
1362       clearPollingState();
1363       return Promise.resolve();
1364     } else {
1365       // If we're not opted in, reject to retry.
1366       return Promise.reject();
1367     }
1368   }).catch(function() {
1369     if (localStorage.optedInCheckCount < 5) {
1370       localStorage.optedInCheckCount++;
1371       optInRecheckAttempts.scheduleRetry();
1372     } else {
1373       clearPollingState();
1374     }
1375   });
1378 instrumented.runtime.onInstalled.addListener(function(details) {
1379   console.log('onInstalled ' + JSON.stringify(details));
1380   if (details.reason != 'chrome_update') {
1381     initialize();
1382   }
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);
1400       });
1401     });
1402   });
1404   initialize();
1407 authenticationManager.addListener(function() {
1408   console.log('signIn State Change');
1409   onStateChange();
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;
1419             if (url) {
1420               recordNotificationClick(notificationDataEntry.cardTypeId);
1421             }
1422             return url;
1423           });
1424         });
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];
1434             if (url) {
1435               recordButtonClick(notificationDataEntry.cardTypeId, buttonIndex);
1436             } else {
1437               verify(false, 'onButtonClicked: no url for a button');
1438               console.log(
1439                   'buttonIndex=' + buttonIndex + ' ' +
1440                   'chromeNotificationId=' + chromeNotificationId + ' ' +
1441                   'notificationDataEntry=' +
1442                   JSON.stringify(notificationDataEntry));
1443             }
1444             return url;
1445           });
1446         });
1448 instrumented.notifications.onClosed.addListener(onNotificationClosed);
1450 instrumented.notifications.onPermissionLevelChanged.addListener(
1451     function(permissionLevel) {
1452       console.log('Notifications permissionLevel Change');
1453       onStateChange();
1454     });
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) {
1464       onStateChange();
1465     }
1466   }
1469 instrumented.gcm.onMessage.addListener(function(message) {
1470   console.log('gcm.onMessage ' + JSON.stringify(message));
1471   if (!message || !message.data) {
1472     return;
1473   }
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] = {
1490             cards: [],
1491             nextPollTime: Date.now()
1492           };
1494           chrome.storage.local.set({
1495             lastPollNowPayloads: items.lastPollNowPayloads,
1496             notificationGroups: items.notificationGroups
1497           });
1499           pollOptedInWithRecheck();
1500         }
1501       });
1502     });
1503   }