Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / google_now / background.js
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.
4
5 'use strict';
6
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  */
18
19 // TODO(robliao): Decide what to do in incognito mode.
20
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;
27
28 var HTTP_BAD_REQUEST = 400;
29 var HTTP_UNAUTHORIZED = 401;
30 var HTTP_FORBIDDEN = 403;
31 var HTTP_METHOD_NOT_ALLOWED = 405;
32
33 var MS_IN_SECOND = 1000;
34 var MS_IN_MINUTE = 60 * 1000;
35
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
41
42 /**
43  * Mininal period for polling for Google Now Notifications cards.
44  */
45 var MINIMUM_POLLING_PERIOD_SECONDS = 5 * 60;  // 5 minutes
46
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
52
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
58
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
65
66 /**
67  * Initial period for retrying the server request for dismissing cards.
68  */
69 var INITIAL_RETRY_DISMISS_PERIOD_SECONDS = 60;  // 1 minute
70
71 /**
72  * Maximum period for retrying the server request for dismissing cards.
73  */
74 var MAXIMUM_RETRY_DISMISS_PERIOD_SECONDS = 60 * 60;  // 1 hour
75
76 /**
77  * Time we keep retrying dismissals.
78  */
79 var MAXIMUM_DISMISSAL_AGE_MS = 24 * 60 * 60 * 1000; // 1 day
80
81 /**
82  * Time we keep dismissals after successful server dismiss requests.
83  */
84 var DISMISS_RETENTION_TIME_MS = 20 * 60 * 1000;  // 20 minutes
85
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
90
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';
96
97 /**
98  * Number of cards that need an explanatory link.
99  */
100 var EXPLANATORY_CARDS_LINK_THRESHOLD = 4;
101
102 /**
103  * Names for tasks that can be created by the extension.
104  */
105 var UPDATE_CARDS_TASK_NAME = 'update-cards';
106 var DISMISS_CARD_TASK_NAME = 'dismiss-card';
107 var RETRY_DISMISS_TASK_NAME = 'retry-dismiss';
108 var STATE_CHANGED_TASK_NAME = 'state-changed';
109 var SHOW_ON_START_TASK_NAME = 'show-cards-on-start';
110 var ON_PUSH_MESSAGE_START_TASK_NAME = 'on-push-message';
111
112 /**
113  * Group as received from the server.
114  *
115  * @typedef {{
116  *   nextPollSeconds: (string|undefined),
117  *   rank: (number|undefined),
118  *   requested: (boolean|undefined)
119  * }}
120  */
121 var ReceivedGroup;
122
123 /**
124  * Server response with notifications and groups.
125  *
126  * @typedef {{
127  *   googleNowDisabled: (boolean|undefined),
128  *   groups: Object.<string, ReceivedGroup>,
129  *   notifications: Array.<ReceivedNotification>
130  * }}
131  */
132 var ServerResponse;
133
134 /**
135  * Notification group as the client stores it. |cardsTimestamp| and |rank| are
136  * defined if |cards| is non-empty. |nextPollTime| is undefined if the server
137  * (1) never sent 'nextPollSeconds' for the group or
138  * (2) didn't send 'nextPollSeconds' with the last group update containing a
139  *     cards update and all the times after that.
140  *
141  * @typedef {{
142  *   cards: Array.<ReceivedNotification>,
143  *   cardsTimestamp: (number|undefined),
144  *   nextPollTime: (number|undefined),
145  *   rank: (number|undefined)
146  * }}
147  */
148 var StoredNotificationGroup;
149
150 /**
151  * Pending (not yet successfully sent) dismissal for a received notification.
152  * |time| is the moment when the user requested dismissal.
153  *
154  * @typedef {{
155  *   chromeNotificationId: ChromeNotificationId,
156  *   time: number,
157  *   dismissalData: DismissalData
158  * }}
159  */
160 var PendingDismissal;
161
162 /**
163  * Checks if a new task can't be scheduled when another task is already
164  * scheduled.
165  * @param {string} newTaskName Name of the new task.
166  * @param {string} scheduledTaskName Name of the scheduled task.
167  * @return {boolean} Whether the new task conflicts with the existing task.
168  */
169 function areTasksConflicting(newTaskName, scheduledTaskName) {
170   if (newTaskName == UPDATE_CARDS_TASK_NAME &&
171       scheduledTaskName == UPDATE_CARDS_TASK_NAME) {
172     // If a card update is requested while an old update is still scheduled, we
173     // don't need the new update.
174     return true;
175   }
176
177   if (newTaskName == RETRY_DISMISS_TASK_NAME &&
178       (scheduledTaskName == UPDATE_CARDS_TASK_NAME ||
179        scheduledTaskName == DISMISS_CARD_TASK_NAME ||
180        scheduledTaskName == RETRY_DISMISS_TASK_NAME)) {
181     // No need to schedule retry-dismiss action if another action that tries to
182     // send dismissals is scheduled.
183     return true;
184   }
185
186   return false;
187 }
188
189 var tasks = buildTaskManager(areTasksConflicting);
190
191 // Add error processing to API calls.
192 wrapper.instrumentChromeApiFunction('metricsPrivate.getVariationParams', 1);
193 wrapper.instrumentChromeApiFunction('notifications.clear', 1);
194 wrapper.instrumentChromeApiFunction('notifications.create', 2);
195 wrapper.instrumentChromeApiFunction('notifications.getPermissionLevel', 0);
196 wrapper.instrumentChromeApiFunction('notifications.update', 2);
197 wrapper.instrumentChromeApiFunction('notifications.getAll', 0);
198 wrapper.instrumentChromeApiFunction(
199     'notifications.onButtonClicked.addListener', 0);
200 wrapper.instrumentChromeApiFunction('notifications.onClicked.addListener', 0);
201 wrapper.instrumentChromeApiFunction('notifications.onClosed.addListener', 0);
202 wrapper.instrumentChromeApiFunction(
203     'notifications.onPermissionLevelChanged.addListener', 0);
204 wrapper.instrumentChromeApiFunction(
205     'notifications.onShowSettings.addListener', 0);
206 wrapper.instrumentChromeApiFunction('permissions.contains', 1);
207 wrapper.instrumentChromeApiFunction('pushMessaging.onMessage.addListener', 0);
208 wrapper.instrumentChromeApiFunction('storage.onChanged.addListener', 0);
209 wrapper.instrumentChromeApiFunction('runtime.onInstalled.addListener', 0);
210 wrapper.instrumentChromeApiFunction('runtime.onStartup.addListener', 0);
211 wrapper.instrumentChromeApiFunction('tabs.create', 1);
212
213 var updateCardsAttempts = buildAttemptManager(
214     'cards-update',
215     requestCards,
216     INITIAL_POLLING_PERIOD_SECONDS,
217     MAXIMUM_POLLING_PERIOD_SECONDS);
218 var optInPollAttempts = buildAttemptManager(
219     'optin',
220     pollOptedInNoImmediateRecheck,
221     INITIAL_POLLING_PERIOD_SECONDS,
222     MAXIMUM_POLLING_PERIOD_SECONDS);
223 var optInRecheckAttempts = buildAttemptManager(
224     'optin-recheck',
225     pollOptedInWithRecheck,
226     INITIAL_OPTIN_RECHECK_PERIOD_SECONDS,
227     MAXIMUM_OPTIN_RECHECK_PERIOD_SECONDS);
228 var dismissalAttempts = buildAttemptManager(
229     'dismiss',
230     retryPendingDismissals,
231     INITIAL_RETRY_DISMISS_PERIOD_SECONDS,
232     MAXIMUM_RETRY_DISMISS_PERIOD_SECONDS);
233 var cardSet = buildCardSet();
234
235 var authenticationManager = buildAuthenticationManager();
236
237 /**
238  * Google Now UMA event identifier.
239  * @enum {number}
240  */
241 var GoogleNowEvent = {
242   REQUEST_FOR_CARDS_TOTAL: 0,
243   REQUEST_FOR_CARDS_SUCCESS: 1,
244   CARDS_PARSE_SUCCESS: 2,
245   DISMISS_REQUEST_TOTAL: 3,
246   DISMISS_REQUEST_SUCCESS: 4,
247   LOCATION_REQUEST: 5,
248   DELETED_LOCATION_UPDATE: 6,
249   EXTENSION_START: 7,
250   DELETED_SHOW_WELCOME_TOAST: 8,
251   STOPPED: 9,
252   DELETED_USER_SUPPRESSED: 10,
253   SIGNED_OUT: 11,
254   NOTIFICATION_DISABLED: 12,
255   GOOGLE_NOW_DISABLED: 13,
256   EVENTS_TOTAL: 14  // EVENTS_TOTAL is not an event; all new events need to be
257                     // added before it.
258 };
259
260 /**
261  * Records a Google Now Event.
262  * @param {GoogleNowEvent} event Event identifier.
263  */
264 function recordEvent(event) {
265   var metricDescription = {
266     metricName: 'GoogleNow.Event',
267     type: 'histogram-linear',
268     min: 1,
269     max: GoogleNowEvent.EVENTS_TOTAL,
270     buckets: GoogleNowEvent.EVENTS_TOTAL + 1
271   };
272
273   chrome.metricsPrivate.recordValue(metricDescription, event);
274 }
275
276 /**
277  * Records a notification clicked event.
278  * @param {number|undefined} cardTypeId Card type ID.
279  */
280 function recordNotificationClick(cardTypeId) {
281   if (cardTypeId !== undefined) {
282     chrome.metricsPrivate.recordSparseValue(
283         'GoogleNow.Card.Clicked', cardTypeId);
284   }
285 }
286
287 /**
288  * Records a button clicked event.
289  * @param {number|undefined} cardTypeId Card type ID.
290  * @param {number} buttonIndex Button Index
291  */
292 function recordButtonClick(cardTypeId, buttonIndex) {
293   if (cardTypeId !== undefined) {
294     chrome.metricsPrivate.recordSparseValue(
295         'GoogleNow.Card.Button.Clicked' + buttonIndex, cardTypeId);
296   }
297 }
298
299 /**
300  * Checks the result of the HTTP Request and updates the authentication
301  * manager on any failure.
302  * @param {string} token Authentication token to validate against an
303  *     XMLHttpRequest.
304  * @return {function(XMLHttpRequest)} Function that validates the token with the
305  *     supplied XMLHttpRequest.
306  */
307 function checkAuthenticationStatus(token) {
308   return function(request) {
309     if (request.status == HTTP_FORBIDDEN ||
310         request.status == HTTP_UNAUTHORIZED) {
311       authenticationManager.removeToken(token);
312     }
313   }
314 }
315
316 /**
317  * Builds and sends an authenticated request to the notification server.
318  * @param {string} method Request method.
319  * @param {string} handlerName Server handler to send the request to.
320  * @param {string=} opt_contentType Value for the Content-type header.
321  * @return {Promise} A promise to issue a request to the server.
322  *     The promise rejects if the response is not within the HTTP 200 range.
323  */
324 function requestFromServer(method, handlerName, opt_contentType) {
325   return authenticationManager.getAuthToken().then(function(token) {
326     var request = buildServerRequest(method, handlerName, opt_contentType);
327     request.setRequestHeader('Authorization', 'Bearer ' + token);
328     var requestPromise = new Promise(function(resolve, reject) {
329       request.addEventListener('loadend', function() {
330         if ((200 <= request.status) && (request.status < 300)) {
331           resolve(request);
332         } else {
333           reject(request);
334         }
335       }, false);
336       request.send();
337     });
338     requestPromise.catch(checkAuthenticationStatus(token));
339     return requestPromise;
340   });
341 }
342
343 /**
344  * Shows the notification groups as notification cards.
345  * @param {Object.<string, StoredNotificationGroup>} notificationGroups Map from
346  *     group name to group information.
347  * @param {function(ReceivedNotification)=} opt_onCardShown Optional parameter
348  *     called when each card is shown.
349  * @return {Promise} A promise to show the notification groups as cards.
350  */
351 function showNotificationGroups(notificationGroups, opt_onCardShown) {
352   /** @type {Object.<ChromeNotificationId, CombinedCard>} */
353   var cards = combineCardsFromGroups(notificationGroups);
354   console.log('showNotificationGroups ' + JSON.stringify(cards));
355
356   return new Promise(function(resolve) {
357     instrumented.notifications.getAll(function(notifications) {
358       console.log('showNotificationGroups-getAll ' +
359           JSON.stringify(notifications));
360       notifications = notifications || {};
361
362       // Mark notifications that didn't receive an update as having received
363       // an empty update.
364       for (var chromeNotificationId in notifications) {
365         cards[chromeNotificationId] = cards[chromeNotificationId] || [];
366       }
367
368       /** @type {Object.<ChromeNotificationId, NotificationDataEntry>} */
369       var notificationsData = {};
370
371       // Create/update/delete notifications.
372       for (var chromeNotificationId in cards) {
373         notificationsData[chromeNotificationId] = cardSet.update(
374             chromeNotificationId,
375             cards[chromeNotificationId],
376             notificationGroups,
377             opt_onCardShown);
378       }
379       chrome.storage.local.set({notificationsData: notificationsData});
380       resolve();
381     });
382   });
383 }
384
385 /**
386  * Removes all cards and card state on Google Now close down.
387  */
388 function removeAllCards() {
389   console.log('removeAllCards');
390
391   // TODO(robliao): Once Google Now clears its own checkbox in the
392   // notifications center and bug 260376 is fixed, the below clearing
393   // code is no longer necessary.
394   instrumented.notifications.getAll(function(notifications) {
395     notifications = notifications || {};
396     for (var chromeNotificationId in notifications) {
397       instrumented.notifications.clear(chromeNotificationId, function() {});
398     }
399     chrome.storage.local.remove(['notificationsData', 'notificationGroups']);
400   });
401 }
402
403 /**
404  * Adds a card group into a set of combined cards.
405  * @param {Object.<ChromeNotificationId, CombinedCard>} combinedCards Map from
406  *     chromeNotificationId to a combined card.
407  *     This is an input/output parameter.
408  * @param {StoredNotificationGroup} storedGroup Group to combine into the
409  *     combined card set.
410  */
411 function combineGroup(combinedCards, storedGroup) {
412   for (var i = 0; i < storedGroup.cards.length; i++) {
413     /** @type {ReceivedNotification} */
414     var receivedNotification = storedGroup.cards[i];
415
416     /** @type {UncombinedNotification} */
417     var uncombinedNotification = {
418       receivedNotification: receivedNotification,
419       showTime: receivedNotification.trigger.showTimeSec &&
420                 (storedGroup.cardsTimestamp +
421                  receivedNotification.trigger.showTimeSec * MS_IN_SECOND),
422       hideTime: storedGroup.cardsTimestamp +
423                 receivedNotification.trigger.hideTimeSec * MS_IN_SECOND
424     };
425
426     var combinedCard =
427         combinedCards[receivedNotification.chromeNotificationId] || [];
428     combinedCard.push(uncombinedNotification);
429     combinedCards[receivedNotification.chromeNotificationId] = combinedCard;
430   }
431 }
432
433 /**
434  * Calculates the soonest poll time from a map of groups as an absolute time.
435  * @param {Object.<string, StoredNotificationGroup>} groups Map from group name
436  *     to group information.
437  * @return {number} The next poll time based off of the groups.
438  */
439 function calculateNextPollTimeMilliseconds(groups) {
440   var nextPollTime = null;
441
442   for (var groupName in groups) {
443     var group = groups[groupName];
444     if (group.nextPollTime !== undefined) {
445       nextPollTime = nextPollTime == null ?
446           group.nextPollTime : Math.min(group.nextPollTime, nextPollTime);
447     }
448   }
449
450   // At least one of the groups must have nextPollTime.
451   verify(nextPollTime != null, 'calculateNextPollTime: nextPollTime is null');
452   return nextPollTime;
453 }
454
455 /**
456  * Schedules next cards poll.
457  * @param {Object.<string, StoredNotificationGroup>} groups Map from group name
458  *     to group information.
459  */
460 function scheduleNextCardsPoll(groups) {
461   var nextPollTimeMs = calculateNextPollTimeMilliseconds(groups);
462
463   var nextPollDelaySeconds = Math.max(
464       (nextPollTimeMs - Date.now()) / MS_IN_SECOND,
465       MINIMUM_POLLING_PERIOD_SECONDS);
466   updateCardsAttempts.start(nextPollDelaySeconds);
467 }
468
469 /**
470  * Schedules the next opt-in check poll.
471  */
472 function scheduleOptInCheckPoll() {
473   instrumented.metricsPrivate.getVariationParams(
474       'GoogleNow', function(params) {
475     var optinPollPeriodSeconds =
476         parseInt(params && params.optinPollPeriodSeconds, 10) ||
477         DEFAULT_OPTIN_CHECK_PERIOD_SECONDS;
478     optInPollAttempts.start(optinPollPeriodSeconds);
479   });
480 }
481
482 /**
483  * Combines notification groups into a set of Chrome notifications.
484  * @param {Object.<string, StoredNotificationGroup>} notificationGroups Map from
485  *     group name to group information.
486  * @return {Object.<ChromeNotificationId, CombinedCard>} Cards to show.
487  */
488 function combineCardsFromGroups(notificationGroups) {
489   console.log('combineCardsFromGroups ' + JSON.stringify(notificationGroups));
490   /** @type {Object.<ChromeNotificationId, CombinedCard>} */
491   var combinedCards = {};
492
493   for (var groupName in notificationGroups)
494     combineGroup(combinedCards, notificationGroups[groupName]);
495
496   return combinedCards;
497 }
498
499 /**
500  * Processes a server response for consumption by showNotificationGroups.
501  * @param {ServerResponse} response Server response.
502  * @return {Promise} A promise to process the server response and provide
503  *     updated groups. Rejects if the server response shouldn't be processed.
504  */
505 function processServerResponse(response) {
506   console.log('processServerResponse ' + JSON.stringify(response));
507
508   if (response.googleNowDisabled) {
509     chrome.storage.local.set({googleNowEnabled: false});
510     // Stop processing now. The state change will clear the cards.
511     return Promise.reject();
512   }
513
514   var receivedGroups = response.groups;
515
516   return fillFromChromeLocalStorage({
517     /** @type {Object.<string, StoredNotificationGroup>} */
518     notificationGroups: {},
519     /** @type {Object.<ServerNotificationId, number>} */
520     recentDismissals: {}
521   }).then(function(items) {
522     console.log('processServerResponse-get ' + JSON.stringify(items));
523
524     // Build a set of non-expired recent dismissals. It will be used for
525     // client-side filtering of cards.
526     /** @type {Object.<ServerNotificationId, number>} */
527     var updatedRecentDismissals = {};
528     var now = Date.now();
529     for (var serverNotificationId in items.recentDismissals) {
530       var dismissalAge = now - items.recentDismissals[serverNotificationId];
531       if (dismissalAge < DISMISS_RETENTION_TIME_MS) {
532         updatedRecentDismissals[serverNotificationId] =
533             items.recentDismissals[serverNotificationId];
534       }
535     }
536
537     // Populate groups with corresponding cards.
538     if (response.notifications) {
539       for (var i = 0; i < response.notifications.length; ++i) {
540         /** @type {ReceivedNotification} */
541         var card = response.notifications[i];
542         if (!(card.notificationId in updatedRecentDismissals)) {
543           var group = receivedGroups[card.groupName];
544           group.cards = group.cards || [];
545           group.cards.push(card);
546         }
547       }
548     }
549
550     // Build updated set of groups.
551     var updatedGroups = {};
552
553     for (var groupName in receivedGroups) {
554       var receivedGroup = receivedGroups[groupName];
555       var storedGroup = items.notificationGroups[groupName] || {
556         cards: [],
557         cardsTimestamp: undefined,
558         nextPollTime: undefined,
559         rank: undefined
560       };
561
562       if (receivedGroup.requested)
563         receivedGroup.cards = receivedGroup.cards || [];
564
565       if (receivedGroup.cards) {
566         // If the group contains a cards update, all its fields will get new
567         // values.
568         storedGroup.cards = receivedGroup.cards;
569         storedGroup.cardsTimestamp = now;
570         storedGroup.rank = receivedGroup.rank;
571         storedGroup.nextPollTime = undefined;
572         // The code below assigns nextPollTime a defined value if
573         // nextPollSeconds is specified in the received group.
574         // If the group's cards are not updated, and nextPollSeconds is
575         // unspecified, this method doesn't change group's nextPollTime.
576       }
577
578       // 'nextPollSeconds' may be sent even for groups that don't contain
579       // cards updates.
580       if (receivedGroup.nextPollSeconds !== undefined) {
581         storedGroup.nextPollTime =
582             now + receivedGroup.nextPollSeconds * MS_IN_SECOND;
583       }
584
585       updatedGroups[groupName] = storedGroup;
586     }
587
588     scheduleNextCardsPoll(updatedGroups);
589     return {
590       updatedGroups: updatedGroups,
591       recentDismissals: updatedRecentDismissals
592     };
593   });
594 }
595
596 /**
597  * Update the Explanatory Total Cards Shown Count.
598  */
599 function countExplanatoryCard() {
600   localStorage['explanatoryCardsShown']++;
601 }
602
603 /**
604  * Determines if cards should have an explanation link.
605  * @return {boolean} true if an explanatory card should be shown.
606  */
607 function shouldShowExplanatoryCard() {
608   var isBelowThreshold =
609       localStorage['explanatoryCardsShown'] < EXPLANATORY_CARDS_LINK_THRESHOLD;
610   return isBelowThreshold;
611 }
612
613 /**
614  * Requests notification cards from the server for specified groups.
615  * @param {Array.<string>} groupNames Names of groups that need to be refreshed.
616  * @return {Promise} A promise to request the specified notification groups.
617  */
618 function requestNotificationGroupsFromServer(groupNames) {
619   console.log(
620       'requestNotificationGroupsFromServer from ' + NOTIFICATION_CARDS_URL +
621       ', groupNames=' + JSON.stringify(groupNames));
622
623   recordEvent(GoogleNowEvent.REQUEST_FOR_CARDS_TOTAL);
624
625   var requestParameters = '?timeZoneOffsetMs=' +
626     (-new Date().getTimezoneOffset() * MS_IN_MINUTE);
627
628   if (shouldShowExplanatoryCard()) {
629     requestParameters += '&cardExplanation=true';
630   }
631
632   groupNames.forEach(function(groupName) {
633     requestParameters += ('&requestTypes=' + groupName);
634   });
635
636   requestParameters += '&uiLocale=' + navigator.language;
637
638   console.log(
639       'requestNotificationGroupsFromServer: request=' + requestParameters);
640
641   return requestFromServer('GET', 'notifications' + requestParameters).then(
642     function(request) {
643       console.log(
644           'requestNotificationGroupsFromServer-received ' + request.status);
645       if (request.status == HTTP_OK) {
646         recordEvent(GoogleNowEvent.REQUEST_FOR_CARDS_SUCCESS);
647         return JSON.parse(request.responseText);
648       }
649     });
650 }
651
652 /**
653  * Performs an opt-in poll without an immediate recheck.
654  * If the response is not opted-in, schedule an opt-in check poll.
655  */
656 function pollOptedInNoImmediateRecheck() {
657   requestAndUpdateOptedIn()
658       .then(function(optedIn) {
659         if (!optedIn) {
660           // Request a repoll if we're not opted in.
661           return Promise.reject();
662         }
663       })
664       .catch(function() {
665         scheduleOptInCheckPoll();
666       });
667 }
668
669 /**
670  * Requests the account opted-in state from the server and updates any
671  * state as necessary.
672  * @return {Promise} A promise to request and update the opted-in state.
673  *     The promise resolves with the opt-in state.
674  */
675 function requestAndUpdateOptedIn() {
676   console.log('requestOptedIn from ' + NOTIFICATION_CARDS_URL);
677
678   return requestFromServer('GET', 'settings/optin').then(function(request) {
679     console.log(
680         'requestOptedIn-received ' + request.status + ' ' + request.response);
681     if (request.status == HTTP_OK) {
682       var parsedResponse = JSON.parse(request.responseText);
683       return parsedResponse.value;
684     }
685   }).then(function(optedIn) {
686     chrome.storage.local.set({googleNowEnabled: optedIn});
687     return optedIn;
688   });
689 }
690
691 /**
692  * Determines the groups that need to be requested right now.
693  * @return {Promise} A promise to determine the groups to request.
694  */
695 function getGroupsToRequest() {
696   return fillFromChromeLocalStorage({
697     /** @type {Object.<string, StoredNotificationGroup>} */
698     notificationGroups: {}
699   }).then(function(items) {
700     console.log('getGroupsToRequest-storage-get ' + JSON.stringify(items));
701     var groupsToRequest = [];
702     var now = Date.now();
703
704     for (var groupName in items.notificationGroups) {
705       var group = items.notificationGroups[groupName];
706       if (group.nextPollTime !== undefined && group.nextPollTime <= now)
707         groupsToRequest.push(groupName);
708     }
709     return groupsToRequest;
710   });
711 }
712
713 /**
714  * Requests notification cards from the server.
715  * @return {Promise} A promise to request the notification cards.
716  *     Rejects if the cards won't be requested.
717  */
718 function requestNotificationCards() {
719   console.log('requestNotificationCards');
720   return getGroupsToRequest()
721       .then(requestNotificationGroupsFromServer)
722       .then(processServerResponse)
723       .then(function(processedResponse) {
724         var onCardShown =
725             shouldShowExplanatoryCard() ? countExplanatoryCard : undefined;
726         return showNotificationGroups(
727             processedResponse.updatedGroups, onCardShown).then(function() {
728               chrome.storage.local.set({
729                 notificationGroups: processedResponse.updatedGroups,
730                 recentDismissals: processedResponse.updatedRecentDismissals
731               });
732               recordEvent(GoogleNowEvent.CARDS_PARSE_SUCCESS);
733             }
734           );
735       });
736 }
737
738 /**
739  * Determines if an immediate retry should occur based off of the given groups.
740  * The NOR group is expected most often and less latency sensitive, so we will
741  * simply wait MAXIMUM_POLLING_PERIOD_SECONDS before trying again.
742  * @param {Array.<string>} groupNames Names of groups that need to be refreshed.
743  * @return {boolean} Whether a retry should occur.
744  */
745 function shouldScheduleRetryFromGroupList(groupNames) {
746   return (groupNames.length != 1) || (groupNames[0] !== 'NOR');
747 }
748
749 /**
750  * Requests and shows notification cards.
751  */
752 function requestCards() {
753   console.log('requestCards @' + new Date());
754   // LOCATION_REQUEST is a legacy histogram value when we requested location.
755   // This corresponds to the extension attempting to request for cards.
756   // We're keeping the name the same to keep our histograms in order.
757   recordEvent(GoogleNowEvent.LOCATION_REQUEST);
758   tasks.add(UPDATE_CARDS_TASK_NAME, function() {
759     console.log('requestCards-task-begin');
760     updateCardsAttempts.isRunning(function(running) {
761       if (running) {
762         // The cards are requested only if there are no unsent dismissals.
763         processPendingDismissals()
764             .then(requestNotificationCards)
765             .catch(function() {
766               return getGroupsToRequest().then(function(groupsToRequest) {
767                 if (shouldScheduleRetryFromGroupList(groupsToRequest)) {
768                   updateCardsAttempts.scheduleRetry();
769                 }
770               });
771             });
772       }
773     });
774   });
775 }
776
777 /**
778  * Sends a server request to dismiss a card.
779  * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
780  *     the card.
781  * @param {number} dismissalTimeMs Time of the user's dismissal of the card in
782  *     milliseconds since epoch.
783  * @param {DismissalData} dismissalData Data to build a dismissal request.
784  * @return {Promise} A promise to request the card dismissal, rejects on error.
785  */
786 function requestCardDismissal(
787     chromeNotificationId, dismissalTimeMs, dismissalData) {
788   console.log('requestDismissingCard ' + chromeNotificationId +
789       ' from ' + NOTIFICATION_CARDS_URL +
790       ', dismissalData=' + JSON.stringify(dismissalData));
791
792   var dismissalAge = Date.now() - dismissalTimeMs;
793
794   if (dismissalAge > MAXIMUM_DISMISSAL_AGE_MS) {
795     return Promise.resolve();
796   }
797
798   recordEvent(GoogleNowEvent.DISMISS_REQUEST_TOTAL);
799
800   var requestParameters = 'notifications/' + dismissalData.notificationId +
801       '?age=' + dismissalAge +
802       '&chromeNotificationId=' + chromeNotificationId;
803
804   for (var paramField in dismissalData.parameters)
805     requestParameters += ('&' + paramField +
806     '=' + dismissalData.parameters[paramField]);
807
808   console.log('requestCardDismissal: requestParameters=' + requestParameters);
809
810   return requestFromServer('DELETE', requestParameters).then(function(request) {
811     console.log('requestDismissingCard-onloadend ' + request.status);
812     if (request.status == HTTP_NOCONTENT)
813       recordEvent(GoogleNowEvent.DISMISS_REQUEST_SUCCESS);
814
815     // A dismissal doesn't require further retries if it was successful or
816     // doesn't have a chance for successful completion.
817     return (request.status == HTTP_NOCONTENT) ?
818            Promise.resolve() :
819            Promise.reject();
820   }).catch(function(request) {
821     request = (typeof request === 'object') ? request : {};
822     return (request.status == HTTP_BAD_REQUEST ||
823            request.status == HTTP_METHOD_NOT_ALLOWED) ?
824            Promise.resolve() :
825            Promise.reject();
826   });
827 }
828
829 /**
830  * Tries to send dismiss requests for all pending dismissals.
831  * @return {Promise} A promise to process the pending dismissals.
832  *     The promise is rejected if a problem was encountered.
833  */
834 function processPendingDismissals() {
835   return fillFromChromeLocalStorage({
836     /** @type {Array.<PendingDismissal>} */
837     pendingDismissals: [],
838     /** @type {Object.<ServerNotificationId, number>} */
839     recentDismissals: {}
840   }).then(function(items) {
841     console.log(
842         'processPendingDismissals-storage-get ' + JSON.stringify(items));
843
844     var dismissalsChanged = false;
845
846     function onFinish(success) {
847       if (dismissalsChanged) {
848         chrome.storage.local.set({
849           pendingDismissals: items.pendingDismissals,
850           recentDismissals: items.recentDismissals
851         });
852       }
853       return success ? Promise.resolve() : Promise.reject();
854     }
855
856     function doProcessDismissals() {
857       if (items.pendingDismissals.length == 0) {
858         dismissalAttempts.stop();
859         return onFinish(true);
860       }
861
862       // Send dismissal for the first card, and if successful, repeat
863       // recursively with the rest.
864       /** @type {PendingDismissal} */
865       var dismissal = items.pendingDismissals[0];
866       return requestCardDismissal(
867           dismissal.chromeNotificationId,
868           dismissal.time,
869           dismissal.dismissalData).then(function() {
870             dismissalsChanged = true;
871             items.pendingDismissals.splice(0, 1);
872             items.recentDismissals[dismissal.dismissalData.notificationId] =
873                 Date.now();
874             return doProcessDismissals();
875           }).catch(function() {
876             return onFinish(false);
877           });
878     }
879
880     return doProcessDismissals();
881   });
882 }
883
884 /**
885  * Submits a task to send pending dismissals.
886  */
887 function retryPendingDismissals() {
888   tasks.add(RETRY_DISMISS_TASK_NAME, function() {
889     processPendingDismissals().catch(dismissalAttempts.scheduleRetry);
890   });
891 }
892
893 /**
894  * Opens a URL in a new tab.
895  * @param {string} url URL to open.
896  */
897 function openUrl(url) {
898   instrumented.tabs.create({url: url}, function(tab) {
899     if (tab)
900       chrome.windows.update(tab.windowId, {focused: true});
901     else
902       chrome.windows.create({url: url, focused: true});
903   });
904 }
905
906 /**
907  * Opens URL corresponding to the clicked part of the notification.
908  * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
909  *     the card.
910  * @param {function(NotificationDataEntry): (string|undefined)} selector
911  *     Function that extracts the url for the clicked area from the
912  *     notification data entry.
913  */
914 function onNotificationClicked(chromeNotificationId, selector) {
915   fillFromChromeLocalStorage({
916     /** @type {Object.<ChromeNotificationId, NotificationDataEntry>} */
917     notificationsData: {}
918   }).then(function(items) {
919     /** @type {(NotificationDataEntry|undefined)} */
920     var notificationDataEntry = items.notificationsData[chromeNotificationId];
921     if (!notificationDataEntry)
922       return;
923
924     var url = selector(notificationDataEntry);
925     if (!url)
926       return;
927
928     openUrl(url);
929   });
930 }
931
932 /**
933  * Callback for chrome.notifications.onClosed event.
934  * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
935  *     the card.
936  * @param {boolean} byUser Whether the notification was closed by the user.
937  */
938 function onNotificationClosed(chromeNotificationId, byUser) {
939   if (!byUser)
940     return;
941
942   // At this point we are guaranteed that the notification is a now card.
943   chrome.metricsPrivate.recordUserAction('GoogleNow.Dismissed');
944
945   tasks.add(DISMISS_CARD_TASK_NAME, function() {
946     dismissalAttempts.start();
947
948     fillFromChromeLocalStorage({
949       /** @type {Array.<PendingDismissal>} */
950       pendingDismissals: [],
951       /** @type {Object.<ChromeNotificationId, NotificationDataEntry>} */
952       notificationsData: {},
953       /** @type {Object.<string, StoredNotificationGroup>} */
954       notificationGroups: {}
955     }).then(function(items) {
956       /** @type {NotificationDataEntry} */
957       var notificationData =
958           items.notificationsData[chromeNotificationId] ||
959           {
960             timestamp: Date.now(),
961             combinedCard: []
962           };
963
964       var dismissalResult =
965           cardSet.onDismissal(
966               chromeNotificationId,
967               notificationData,
968               items.notificationGroups);
969
970       for (var i = 0; i < dismissalResult.dismissals.length; i++) {
971         /** @type {PendingDismissal} */
972         var dismissal = {
973           chromeNotificationId: chromeNotificationId,
974           time: Date.now(),
975           dismissalData: dismissalResult.dismissals[i]
976         };
977         items.pendingDismissals.push(dismissal);
978       }
979
980       items.notificationsData[chromeNotificationId] =
981           dismissalResult.notificationData;
982
983       chrome.storage.local.set(items);
984
985       processPendingDismissals();
986     });
987   });
988 }
989
990 /**
991  * Initializes the polling system to start fetching cards.
992  */
993 function startPollingCards() {
994   console.log('startPollingCards');
995   // Create an update timer for a case when for some reason requesting
996   // cards gets stuck.
997   updateCardsAttempts.start(MAXIMUM_POLLING_PERIOD_SECONDS);
998   requestCards();
999 }
1000
1001 /**
1002  * Stops all machinery in the polling system.
1003  */
1004 function stopPollingCards() {
1005   console.log('stopPollingCards');
1006   updateCardsAttempts.stop();
1007   // Since we're stopping everything, clear all runtime storage.
1008   // We don't clear localStorage since those values are still relevant
1009   // across Google Now start-stop events.
1010   chrome.storage.local.clear();
1011 }
1012
1013 /**
1014  * Initializes the event page on install or on browser startup.
1015  */
1016 function initialize() {
1017   recordEvent(GoogleNowEvent.EXTENSION_START);
1018   onStateChange();
1019 }
1020
1021 /**
1022  * Starts or stops the main pipeline for polling cards.
1023  * @param {boolean} shouldPollCardsRequest true to start and
1024  *     false to stop polling cards.
1025  */
1026 function setShouldPollCards(shouldPollCardsRequest) {
1027   updateCardsAttempts.isRunning(function(currentValue) {
1028     if (shouldPollCardsRequest != currentValue) {
1029       console.log('Action Taken setShouldPollCards=' + shouldPollCardsRequest);
1030       if (shouldPollCardsRequest)
1031         startPollingCards();
1032       else
1033         stopPollingCards();
1034     } else {
1035       console.log(
1036           'Action Ignored setShouldPollCards=' + shouldPollCardsRequest);
1037     }
1038   });
1039 }
1040
1041 /**
1042  * Starts or stops the optin check.
1043  * @param {boolean} shouldPollOptInStatus true to start and false to stop
1044  *     polling the optin status.
1045  */
1046 function setShouldPollOptInStatus(shouldPollOptInStatus) {
1047   optInPollAttempts.isRunning(function(currentValue) {
1048     if (shouldPollOptInStatus != currentValue) {
1049       console.log(
1050           'Action Taken setShouldPollOptInStatus=' + shouldPollOptInStatus);
1051       if (shouldPollOptInStatus) {
1052         pollOptedInNoImmediateRecheck();
1053       } else {
1054         optInPollAttempts.stop();
1055       }
1056     } else {
1057       console.log(
1058           'Action Ignored setShouldPollOptInStatus=' + shouldPollOptInStatus);
1059     }
1060   });
1061 }
1062
1063 /**
1064  * Enables or disables the Google Now background permission.
1065  * @param {boolean} backgroundEnable true to run in the background.
1066  *     false to not run in the background.
1067  */
1068 function setBackgroundEnable(backgroundEnable) {
1069   instrumented.permissions.contains({permissions: ['background']},
1070       function(hasPermission) {
1071         if (backgroundEnable != hasPermission) {
1072           console.log('Action Taken setBackgroundEnable=' + backgroundEnable);
1073           if (backgroundEnable)
1074             chrome.permissions.request({permissions: ['background']});
1075           else
1076             chrome.permissions.remove({permissions: ['background']});
1077         } else {
1078           console.log('Action Ignored setBackgroundEnable=' + backgroundEnable);
1079         }
1080       });
1081 }
1082
1083 /**
1084  * Record why this extension would not poll for cards.
1085  * @param {boolean} signedIn true if the user is signed in.
1086  * @param {boolean} notificationEnabled true if
1087  *     Google Now for Chrome is allowed to show notifications.
1088  * @param {boolean} googleNowEnabled true if
1089  *     the Google Now is enabled for the user.
1090  */
1091 function recordEventIfNoCards(signedIn, notificationEnabled, googleNowEnabled) {
1092   if (!signedIn) {
1093     recordEvent(GoogleNowEvent.SIGNED_OUT);
1094   } else if (!notificationEnabled) {
1095     recordEvent(GoogleNowEvent.NOTIFICATION_DISABLED);
1096   } else if (!googleNowEnabled) {
1097     recordEvent(GoogleNowEvent.GOOGLE_NOW_DISABLED);
1098   }
1099 }
1100
1101 /**
1102  * Does the actual work of deciding what Google Now should do
1103  * based off of the current state of Chrome.
1104  * @param {boolean} signedIn true if the user is signed in.
1105  * @param {boolean} canEnableBackground true if
1106  *     the background permission can be requested.
1107  * @param {boolean} notificationEnabled true if
1108  *     Google Now for Chrome is allowed to show notifications.
1109  * @param {boolean} googleNowEnabled true if
1110  *     the Google Now is enabled for the user.
1111  */
1112 function updateRunningState(
1113     signedIn,
1114     canEnableBackground,
1115     notificationEnabled,
1116     googleNowEnabled) {
1117   console.log(
1118       'State Update signedIn=' + signedIn + ' ' +
1119       'canEnableBackground=' + canEnableBackground + ' ' +
1120       'notificationEnabled=' + notificationEnabled + ' ' +
1121       'googleNowEnabled=' + googleNowEnabled);
1122
1123   var shouldPollCards = false;
1124   var shouldPollOptInStatus = false;
1125   var shouldSetBackground = false;
1126
1127   if (signedIn && notificationEnabled) {
1128     shouldPollCards = googleNowEnabled;
1129     shouldPollOptInStatus = !googleNowEnabled;
1130     shouldSetBackground = canEnableBackground && googleNowEnabled;
1131   } else {
1132     recordEvent(GoogleNowEvent.STOPPED);
1133   }
1134
1135   recordEventIfNoCards(signedIn, notificationEnabled, googleNowEnabled);
1136
1137   console.log(
1138       'Requested Actions shouldSetBackground=' + shouldSetBackground + ' ' +
1139       'setShouldPollCards=' + shouldPollCards + ' ' +
1140       'shouldPollOptInStatus=' + shouldPollOptInStatus);
1141
1142   setBackgroundEnable(shouldSetBackground);
1143   setShouldPollCards(shouldPollCards);
1144   setShouldPollOptInStatus(shouldPollOptInStatus);
1145   if (!shouldPollCards) {
1146     removeAllCards();
1147   }
1148 }
1149
1150 /**
1151  * Coordinates the behavior of Google Now for Chrome depending on
1152  * Chrome and extension state.
1153  */
1154 function onStateChange() {
1155   tasks.add(STATE_CHANGED_TASK_NAME, function() {
1156     Promise.all([
1157         authenticationManager.isSignedIn(),
1158         canEnableBackground(),
1159         isNotificationsEnabled(),
1160         isGoogleNowEnabled()])
1161         .then(function(results) {
1162           updateRunningState.apply(null, results);
1163         });
1164   });
1165 }
1166
1167 /**
1168  * Determines if background mode should be requested.
1169  * @return {Promise} A promise to determine if background can be enabled.
1170  */
1171 function canEnableBackground() {
1172   return new Promise(function(resolve) {
1173     instrumented.metricsPrivate.getVariationParams(
1174         'GoogleNow',
1175         function(response) {
1176           resolve(!response || (response.canEnableBackground != 'false'));
1177         });
1178   });
1179 }
1180
1181 /**
1182  * Checks if Google Now is enabled in the notifications center.
1183  * @return {Promise} A promise to determine if Google Now is enabled
1184  *     in the notifications center.
1185  */
1186 function isNotificationsEnabled() {
1187   return new Promise(function(resolve) {
1188     instrumented.notifications.getPermissionLevel(function(level) {
1189       resolve(level == 'granted');
1190     });
1191   });
1192 }
1193
1194 /**
1195  * Gets the previous Google Now opt-in state.
1196  * @return {Promise} A promise to determine the previous Google Now
1197  *     opt-in state.
1198  */
1199 function isGoogleNowEnabled() {
1200   return fillFromChromeLocalStorage({googleNowEnabled: false})
1201       .then(function(items) {
1202         return items.googleNowEnabled;
1203       });
1204 }
1205
1206 /**
1207  * Polls the optin state.
1208  * Sometimes we get the response to the opted in result too soon during
1209  * push messaging. We'll recheck the optin state a few times before giving up.
1210  */
1211 function pollOptedInWithRecheck() {
1212   /**
1213    * Cleans up any state used to recheck the opt-in poll.
1214    */
1215   function clearPollingState() {
1216     localStorage.removeItem('optedInCheckCount');
1217     optInRecheckAttempts.stop();
1218   }
1219
1220   if (localStorage.optedInCheckCount === undefined) {
1221     localStorage.optedInCheckCount = 0;
1222     optInRecheckAttempts.start();
1223   }
1224
1225   console.log(new Date() +
1226       ' checkOptedIn Attempt ' + localStorage.optedInCheckCount);
1227
1228   requestAndUpdateOptedIn().then(function(optedIn) {
1229     if (optedIn) {
1230       clearPollingState();
1231       return Promise.resolve();
1232     } else {
1233       // If we're not opted in, reject to retry.
1234       return Promise.reject();
1235     }
1236   }).catch(function() {
1237     if (localStorage.optedInCheckCount < 5) {
1238       localStorage.optedInCheckCount++;
1239       optInRecheckAttempts.scheduleRetry();
1240     } else {
1241       clearPollingState();
1242     }
1243   });
1244 }
1245
1246 instrumented.runtime.onInstalled.addListener(function(details) {
1247   console.log('onInstalled ' + JSON.stringify(details));
1248   if (details.reason != 'chrome_update') {
1249     initialize();
1250   }
1251 });
1252
1253 instrumented.runtime.onStartup.addListener(function() {
1254   console.log('onStartup');
1255
1256   // Show notifications received by earlier polls. Doing this as early as
1257   // possible to reduce latency of showing first notifications. This mimics how
1258   // persistent notifications will work.
1259   tasks.add(SHOW_ON_START_TASK_NAME, function() {
1260     fillFromChromeLocalStorage({
1261       /** @type {Object.<string, StoredNotificationGroup>} */
1262       notificationGroups: {}
1263     }).then(function(items) {
1264       console.log('onStartup-get ' + JSON.stringify(items));
1265
1266       showNotificationGroups(items.notificationGroups).then(function() {
1267         chrome.storage.local.set(items);
1268       });
1269     });
1270   });
1271
1272   initialize();
1273 });
1274
1275 authenticationManager.addListener(function() {
1276   console.log('signIn State Change');
1277   onStateChange();
1278 });
1279
1280 instrumented.notifications.onClicked.addListener(
1281     function(chromeNotificationId) {
1282       chrome.metricsPrivate.recordUserAction('GoogleNow.MessageClicked');
1283       onNotificationClicked(chromeNotificationId,
1284           function(notificationDataEntry) {
1285             var actionUrls = notificationDataEntry.actionUrls;
1286             var url = actionUrls && actionUrls.messageUrl;
1287             if (url) {
1288               recordNotificationClick(notificationDataEntry.cardTypeId);
1289             }
1290             return url;
1291           });
1292         });
1293
1294 instrumented.notifications.onButtonClicked.addListener(
1295     function(chromeNotificationId, buttonIndex) {
1296       chrome.metricsPrivate.recordUserAction(
1297           'GoogleNow.ButtonClicked' + buttonIndex);
1298       onNotificationClicked(chromeNotificationId,
1299           function(notificationDataEntry) {
1300             var actionUrls = notificationDataEntry.actionUrls;
1301             var url = actionUrls.buttonUrls[buttonIndex];
1302             if (url) {
1303               recordButtonClick(notificationDataEntry.cardTypeId, buttonIndex);
1304             } else {
1305               verify(false, 'onButtonClicked: no url for a button');
1306               console.log(
1307                   'buttonIndex=' + buttonIndex + ' ' +
1308                   'chromeNotificationId=' + chromeNotificationId + ' ' +
1309                   'notificationDataEntry=' +
1310                   JSON.stringify(notificationDataEntry));
1311             }
1312             return url;
1313           });
1314         });
1315
1316 instrumented.notifications.onClosed.addListener(onNotificationClosed);
1317
1318 instrumented.notifications.onPermissionLevelChanged.addListener(
1319     function(permissionLevel) {
1320       console.log('Notifications permissionLevel Change');
1321       onStateChange();
1322     });
1323
1324 instrumented.notifications.onShowSettings.addListener(function() {
1325   openUrl(SETTINGS_URL);
1326 });
1327
1328 // Handles state change notifications for the Google Now enabled bit.
1329 instrumented.storage.onChanged.addListener(function(changes, areaName) {
1330   if (areaName === 'local') {
1331     if ('googleNowEnabled' in changes) {
1332       onStateChange();
1333     }
1334   }
1335 });
1336
1337 instrumented.pushMessaging.onMessage.addListener(function(message) {
1338   // message.payload will be '' when the extension first starts.
1339   // Each time after signing in, we'll get latest payload for all channels.
1340   // So, we need to poll the server only when the payload is non-empty and has
1341   // changed.
1342   console.log('pushMessaging.onMessage ' + JSON.stringify(message));
1343   if (message.payload.indexOf('REQUEST_CARDS') == 0) {
1344     tasks.add(ON_PUSH_MESSAGE_START_TASK_NAME, function() {
1345       // Accept promise rejection on failure since it's safer to do nothing,
1346       // preventing polling the server when the payload really didn't change.
1347       fillFromChromeLocalStorage({
1348         lastPollNowPayloads: {},
1349         /** @type {Object.<string, StoredNotificationGroup>} */
1350         notificationGroups: {}
1351       }, PromiseRejection.ALLOW).then(function(items) {
1352         if (items.lastPollNowPayloads[message.subchannelId] !=
1353             message.payload) {
1354           items.lastPollNowPayloads[message.subchannelId] = message.payload;
1355
1356           items.notificationGroups['PUSH' + message.subchannelId] = {
1357             cards: [],
1358             nextPollTime: Date.now()
1359           };
1360
1361           chrome.storage.local.set({
1362             lastPollNowPayloads: items.lastPollNowPayloads,
1363             notificationGroups: items.notificationGroups
1364           });
1365
1366           pollOptedInWithRecheck();
1367         }
1368       });
1369     });
1370   }
1371 });