Home | History | Annotate | Download | only in google_now
      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 = 60 * 60;  // 1 hour
     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  * Requests and shows notification cards.
    740  */
    741 function requestCards() {
    742   console.log('requestCards @' + new Date());
    743   // LOCATION_REQUEST is a legacy histogram value when we requested location.
    744   // This corresponds to the extension attempting to request for cards.
    745   // We're keeping the name the same to keep our histograms in order.
    746   recordEvent(GoogleNowEvent.LOCATION_REQUEST);
    747   tasks.add(UPDATE_CARDS_TASK_NAME, function() {
    748     console.log('requestCards-task-begin');
    749     updateCardsAttempts.isRunning(function(running) {
    750       if (running) {
    751         // The cards are requested only if there are no unsent dismissals.
    752         processPendingDismissals()
    753             .then(requestNotificationCards)
    754             .catch(updateCardsAttempts.scheduleRetry);
    755       }
    756     });
    757   });
    758 }
    759 
    760 /**
    761  * Sends a server request to dismiss a card.
    762  * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
    763  *     the card.
    764  * @param {number} dismissalTimeMs Time of the user's dismissal of the card in
    765  *     milliseconds since epoch.
    766  * @param {DismissalData} dismissalData Data to build a dismissal request.
    767  * @return {Promise} A promise to request the card dismissal, rejects on error.
    768  */
    769 function requestCardDismissal(
    770     chromeNotificationId, dismissalTimeMs, dismissalData) {
    771   console.log('requestDismissingCard ' + chromeNotificationId +
    772       ' from ' + NOTIFICATION_CARDS_URL +
    773       ', dismissalData=' + JSON.stringify(dismissalData));
    774 
    775   var dismissalAge = Date.now() - dismissalTimeMs;
    776 
    777   if (dismissalAge > MAXIMUM_DISMISSAL_AGE_MS) {
    778     return Promise.resolve();
    779   }
    780 
    781   recordEvent(GoogleNowEvent.DISMISS_REQUEST_TOTAL);
    782 
    783   var requestParameters = 'notifications/' + dismissalData.notificationId +
    784       '?age=' + dismissalAge +
    785       '&chromeNotificationId=' + chromeNotificationId;
    786 
    787   for (var paramField in dismissalData.parameters)
    788     requestParameters += ('&' + paramField +
    789     '=' + dismissalData.parameters[paramField]);
    790 
    791   console.log('requestCardDismissal: requestParameters=' + requestParameters);
    792 
    793   return requestFromServer('DELETE', requestParameters).then(function(request) {
    794     console.log('requestDismissingCard-onloadend ' + request.status);
    795     if (request.status == HTTP_NOCONTENT)
    796       recordEvent(GoogleNowEvent.DISMISS_REQUEST_SUCCESS);
    797 
    798     // A dismissal doesn't require further retries if it was successful or
    799     // doesn't have a chance for successful completion.
    800     return (request.status == HTTP_NOCONTENT) ?
    801            Promise.resolve() :
    802            Promise.reject();
    803   }).catch(function(request) {
    804     return (request.status == HTTP_BAD_REQUEST ||
    805            request.status == HTTP_METHOD_NOT_ALLOWED) ?
    806            Promise.resolve() :
    807            Promise.reject();
    808   });
    809 }
    810 
    811 /**
    812  * Tries to send dismiss requests for all pending dismissals.
    813  * @return {Promise} A promise to process the pending dismissals.
    814  *     The promise is rejected if a problem was encountered.
    815  */
    816 function processPendingDismissals() {
    817   return fillFromChromeLocalStorage({
    818     /** @type {Array.<PendingDismissal>} */
    819     pendingDismissals: [],
    820     /** @type {Object.<ServerNotificationId, number>} */
    821     recentDismissals: {}
    822   }).then(function(items) {
    823     console.log(
    824         'processPendingDismissals-storage-get ' + JSON.stringify(items));
    825 
    826     var dismissalsChanged = false;
    827 
    828     function onFinish(success) {
    829       if (dismissalsChanged) {
    830         chrome.storage.local.set({
    831           pendingDismissals: items.pendingDismissals,
    832           recentDismissals: items.recentDismissals
    833         });
    834       }
    835       return success ? Promise.resolve() : Promise.reject();
    836     }
    837 
    838     function doProcessDismissals() {
    839       if (items.pendingDismissals.length == 0) {
    840         dismissalAttempts.stop();
    841         return onFinish(true);
    842       }
    843 
    844       // Send dismissal for the first card, and if successful, repeat
    845       // recursively with the rest.
    846       /** @type {PendingDismissal} */
    847       var dismissal = items.pendingDismissals[0];
    848       return requestCardDismissal(
    849           dismissal.chromeNotificationId,
    850           dismissal.time,
    851           dismissal.dismissalData).then(function() {
    852             dismissalsChanged = true;
    853             items.pendingDismissals.splice(0, 1);
    854             items.recentDismissals[dismissal.dismissalData.notificationId] =
    855                 Date.now();
    856             return doProcessDismissals();
    857           }).catch(function() {
    858             return onFinish(false);
    859           });
    860     }
    861 
    862     return doProcessDismissals();
    863   });
    864 }
    865 
    866 /**
    867  * Submits a task to send pending dismissals.
    868  */
    869 function retryPendingDismissals() {
    870   tasks.add(RETRY_DISMISS_TASK_NAME, function() {
    871     processPendingDismissals().catch(dismissalAttempts.scheduleRetry);
    872   });
    873 }
    874 
    875 /**
    876  * Opens a URL in a new tab.
    877  * @param {string} url URL to open.
    878  */
    879 function openUrl(url) {
    880   instrumented.tabs.create({url: url}, function(tab) {
    881     if (tab)
    882       chrome.windows.update(tab.windowId, {focused: true});
    883     else
    884       chrome.windows.create({url: url, focused: true});
    885   });
    886 }
    887 
    888 /**
    889  * Opens URL corresponding to the clicked part of the notification.
    890  * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
    891  *     the card.
    892  * @param {function(NotificationDataEntry): (string|undefined)} selector
    893  *     Function that extracts the url for the clicked area from the
    894  *     notification data entry.
    895  */
    896 function onNotificationClicked(chromeNotificationId, selector) {
    897   fillFromChromeLocalStorage({
    898     /** @type {Object.<ChromeNotificationId, NotificationDataEntry>} */
    899     notificationsData: {}
    900   }).then(function(items) {
    901     /** @type {(NotificationDataEntry|undefined)} */
    902     var notificationDataEntry = items.notificationsData[chromeNotificationId];
    903     if (!notificationDataEntry)
    904       return;
    905 
    906     var url = selector(notificationDataEntry);
    907     if (!url)
    908       return;
    909 
    910     openUrl(url);
    911   });
    912 }
    913 
    914 /**
    915  * Callback for chrome.notifications.onClosed event.
    916  * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
    917  *     the card.
    918  * @param {boolean} byUser Whether the notification was closed by the user.
    919  */
    920 function onNotificationClosed(chromeNotificationId, byUser) {
    921   if (!byUser)
    922     return;
    923 
    924   // At this point we are guaranteed that the notification is a now card.
    925   chrome.metricsPrivate.recordUserAction('GoogleNow.Dismissed');
    926 
    927   tasks.add(DISMISS_CARD_TASK_NAME, function() {
    928     dismissalAttempts.start();
    929 
    930     fillFromChromeLocalStorage({
    931       /** @type {Array.<PendingDismissal>} */
    932       pendingDismissals: [],
    933       /** @type {Object.<ChromeNotificationId, NotificationDataEntry>} */
    934       notificationsData: {},
    935       /** @type {Object.<string, StoredNotificationGroup>} */
    936       notificationGroups: {}
    937     }).then(function(items) {
    938       /** @type {NotificationDataEntry} */
    939       var notificationData =
    940           items.notificationsData[chromeNotificationId] ||
    941           {
    942             timestamp: Date.now(),
    943             combinedCard: []
    944           };
    945 
    946       var dismissalResult =
    947           cardSet.onDismissal(
    948               chromeNotificationId,
    949               notificationData,
    950               items.notificationGroups);
    951 
    952       for (var i = 0; i < dismissalResult.dismissals.length; i++) {
    953         /** @type {PendingDismissal} */
    954         var dismissal = {
    955           chromeNotificationId: chromeNotificationId,
    956           time: Date.now(),
    957           dismissalData: dismissalResult.dismissals[i]
    958         };
    959         items.pendingDismissals.push(dismissal);
    960       }
    961 
    962       items.notificationsData[chromeNotificationId] =
    963           dismissalResult.notificationData;
    964 
    965       chrome.storage.local.set(items);
    966 
    967       processPendingDismissals();
    968     });
    969   });
    970 }
    971 
    972 /**
    973  * Initializes the polling system to start fetching cards.
    974  */
    975 function startPollingCards() {
    976   console.log('startPollingCards');
    977   // Create an update timer for a case when for some reason requesting
    978   // cards gets stuck.
    979   updateCardsAttempts.start(MAXIMUM_POLLING_PERIOD_SECONDS);
    980   requestCards();
    981 }
    982 
    983 /**
    984  * Stops all machinery in the polling system.
    985  */
    986 function stopPollingCards() {
    987   console.log('stopPollingCards');
    988   updateCardsAttempts.stop();
    989   // Since we're stopping everything, clear all runtime storage.
    990   // We don't clear localStorage since those values are still relevant
    991   // across Google Now start-stop events.
    992   chrome.storage.local.clear();
    993 }
    994 
    995 /**
    996  * Initializes the event page on install or on browser startup.
    997  */
    998 function initialize() {
    999   recordEvent(GoogleNowEvent.EXTENSION_START);
   1000   onStateChange();
   1001 }
   1002 
   1003 /**
   1004  * Starts or stops the main pipeline for polling cards.
   1005  * @param {boolean} shouldPollCardsRequest true to start and
   1006  *     false to stop polling cards.
   1007  */
   1008 function setShouldPollCards(shouldPollCardsRequest) {
   1009   updateCardsAttempts.isRunning(function(currentValue) {
   1010     if (shouldPollCardsRequest != currentValue) {
   1011       console.log('Action Taken setShouldPollCards=' + shouldPollCardsRequest);
   1012       if (shouldPollCardsRequest)
   1013         startPollingCards();
   1014       else
   1015         stopPollingCards();
   1016     } else {
   1017       console.log(
   1018           'Action Ignored setShouldPollCards=' + shouldPollCardsRequest);
   1019     }
   1020   });
   1021 }
   1022 
   1023 /**
   1024  * Starts or stops the optin check.
   1025  * @param {boolean} shouldPollOptInStatus true to start and false to stop
   1026  *     polling the optin status.
   1027  */
   1028 function setShouldPollOptInStatus(shouldPollOptInStatus) {
   1029   optInPollAttempts.isRunning(function(currentValue) {
   1030     if (shouldPollOptInStatus != currentValue) {
   1031       console.log(
   1032           'Action Taken setShouldPollOptInStatus=' + shouldPollOptInStatus);
   1033       if (shouldPollOptInStatus) {
   1034         pollOptedInNoImmediateRecheck();
   1035       } else {
   1036         optInPollAttempts.stop();
   1037       }
   1038     } else {
   1039       console.log(
   1040           'Action Ignored setShouldPollOptInStatus=' + shouldPollOptInStatus);
   1041     }
   1042   });
   1043 }
   1044 
   1045 /**
   1046  * Enables or disables the Google Now background permission.
   1047  * @param {boolean} backgroundEnable true to run in the background.
   1048  *     false to not run in the background.
   1049  */
   1050 function setBackgroundEnable(backgroundEnable) {
   1051   instrumented.permissions.contains({permissions: ['background']},
   1052       function(hasPermission) {
   1053         if (backgroundEnable != hasPermission) {
   1054           console.log('Action Taken setBackgroundEnable=' + backgroundEnable);
   1055           if (backgroundEnable)
   1056             chrome.permissions.request({permissions: ['background']});
   1057           else
   1058             chrome.permissions.remove({permissions: ['background']});
   1059         } else {
   1060           console.log('Action Ignored setBackgroundEnable=' + backgroundEnable);
   1061         }
   1062       });
   1063 }
   1064 
   1065 /**
   1066  * Record why this extension would not poll for cards.
   1067  * @param {boolean} signedIn true if the user is signed in.
   1068  * @param {boolean} notificationEnabled true if
   1069  *     Google Now for Chrome is allowed to show notifications.
   1070  * @param {boolean} googleNowEnabled true if
   1071  *     the Google Now is enabled for the user.
   1072  */
   1073 function recordEventIfNoCards(signedIn, notificationEnabled, googleNowEnabled) {
   1074   if (!signedIn) {
   1075     recordEvent(GoogleNowEvent.SIGNED_OUT);
   1076   } else if (!notificationEnabled) {
   1077     recordEvent(GoogleNowEvent.NOTIFICATION_DISABLED);
   1078   } else if (!googleNowEnabled) {
   1079     recordEvent(GoogleNowEvent.GOOGLE_NOW_DISABLED);
   1080   }
   1081 }
   1082 
   1083 /**
   1084  * Does the actual work of deciding what Google Now should do
   1085  * based off of the current state of Chrome.
   1086  * @param {boolean} signedIn true if the user is signed in.
   1087  * @param {boolean} canEnableBackground true if
   1088  *     the background permission can be requested.
   1089  * @param {boolean} notificationEnabled true if
   1090  *     Google Now for Chrome is allowed to show notifications.
   1091  * @param {boolean} googleNowEnabled true if
   1092  *     the Google Now is enabled for the user.
   1093  */
   1094 function updateRunningState(
   1095     signedIn,
   1096     canEnableBackground,
   1097     notificationEnabled,
   1098     googleNowEnabled) {
   1099   console.log(
   1100       'State Update signedIn=' + signedIn + ' ' +
   1101       'canEnableBackground=' + canEnableBackground + ' ' +
   1102       'notificationEnabled=' + notificationEnabled + ' ' +
   1103       'googleNowEnabled=' + googleNowEnabled);
   1104 
   1105   var shouldPollCards = false;
   1106   var shouldPollOptInStatus = false;
   1107   var shouldSetBackground = false;
   1108 
   1109   if (signedIn && notificationEnabled) {
   1110     shouldPollCards = googleNowEnabled;
   1111     shouldPollOptInStatus = !googleNowEnabled;
   1112     shouldSetBackground = canEnableBackground && googleNowEnabled;
   1113   } else {
   1114     recordEvent(GoogleNowEvent.STOPPED);
   1115   }
   1116 
   1117   recordEventIfNoCards(signedIn, notificationEnabled, googleNowEnabled);
   1118 
   1119   console.log(
   1120       'Requested Actions shouldSetBackground=' + shouldSetBackground + ' ' +
   1121       'setShouldPollCards=' + shouldPollCards + ' ' +
   1122       'shouldPollOptInStatus=' + shouldPollOptInStatus);
   1123 
   1124   setBackgroundEnable(shouldSetBackground);
   1125   setShouldPollCards(shouldPollCards);
   1126   setShouldPollOptInStatus(shouldPollOptInStatus);
   1127   if (!shouldPollCards) {
   1128     removeAllCards();
   1129   }
   1130 }
   1131 
   1132 /**
   1133  * Coordinates the behavior of Google Now for Chrome depending on
   1134  * Chrome and extension state.
   1135  */
   1136 function onStateChange() {
   1137   tasks.add(STATE_CHANGED_TASK_NAME, function() {
   1138     Promise.all([
   1139         authenticationManager.isSignedIn(),
   1140         canEnableBackground(),
   1141         isNotificationsEnabled(),
   1142         isGoogleNowEnabled()])
   1143         .then(function(results) {
   1144           updateRunningState.apply(null, results);
   1145         });
   1146   });
   1147 }
   1148 
   1149 /**
   1150  * Determines if background mode should be requested.
   1151  * @return {Promise} A promise to determine if background can be enabled.
   1152  */
   1153 function canEnableBackground() {
   1154   return new Promise(function(resolve) {
   1155     instrumented.metricsPrivate.getVariationParams(
   1156         'GoogleNow',
   1157         function(response) {
   1158           resolve(!response || (response.canEnableBackground != 'false'));
   1159         });
   1160   });
   1161 }
   1162 
   1163 /**
   1164  * Checks if Google Now is enabled in the notifications center.
   1165  * @return {Promise} A promise to determine if Google Now is enabled
   1166  *     in the notifications center.
   1167  */
   1168 function isNotificationsEnabled() {
   1169   return new Promise(function(resolve) {
   1170     instrumented.notifications.getPermissionLevel(function(level) {
   1171       resolve(level == 'granted');
   1172     });
   1173   });
   1174 }
   1175 
   1176 /**
   1177  * Gets the previous Google Now opt-in state.
   1178  * @return {Promise} A promise to determine the previous Google Now
   1179  *     opt-in state.
   1180  */
   1181 function isGoogleNowEnabled() {
   1182   return fillFromChromeLocalStorage({googleNowEnabled: false})
   1183       .then(function(items) {
   1184         return items.googleNowEnabled;
   1185       });
   1186 }
   1187 
   1188 /**
   1189  * Polls the optin state.
   1190  * Sometimes we get the response to the opted in result too soon during
   1191  * push messaging. We'll recheck the optin state a few times before giving up.
   1192  */
   1193 function pollOptedInWithRecheck() {
   1194   /**
   1195    * Cleans up any state used to recheck the opt-in poll.
   1196    */
   1197   function clearPollingState() {
   1198     localStorage.removeItem('optedInCheckCount');
   1199     optInRecheckAttempts.stop();
   1200   }
   1201 
   1202   if (localStorage.optedInCheckCount === undefined) {
   1203     localStorage.optedInCheckCount = 0;
   1204     optInRecheckAttempts.start();
   1205   }
   1206 
   1207   console.log(new Date() +
   1208       ' checkOptedIn Attempt ' + localStorage.optedInCheckCount);
   1209 
   1210   requestAndUpdateOptedIn().then(function(optedIn) {
   1211     if (optedIn) {
   1212       clearPollingState();
   1213       return Promise.resolve();
   1214     } else {
   1215       // If we're not opted in, reject to retry.
   1216       return Promise.reject();
   1217     }
   1218   }).catch(function() {
   1219     if (localStorage.optedInCheckCount < 5) {
   1220       localStorage.optedInCheckCount++;
   1221       optInRecheckAttempts.scheduleRetry();
   1222     } else {
   1223       clearPollingState();
   1224     }
   1225   });
   1226 }
   1227 
   1228 instrumented.runtime.onInstalled.addListener(function(details) {
   1229   console.log('onInstalled ' + JSON.stringify(details));
   1230   if (details.reason != 'chrome_update') {
   1231     initialize();
   1232   }
   1233 });
   1234 
   1235 instrumented.runtime.onStartup.addListener(function() {
   1236   console.log('onStartup');
   1237 
   1238   // Show notifications received by earlier polls. Doing this as early as
   1239   // possible to reduce latency of showing first notifications. This mimics how
   1240   // persistent notifications will work.
   1241   tasks.add(SHOW_ON_START_TASK_NAME, function() {
   1242     fillFromChromeLocalStorage({
   1243       /** @type {Object.<string, StoredNotificationGroup>} */
   1244       notificationGroups: {}
   1245     }).then(function(items) {
   1246       console.log('onStartup-get ' + JSON.stringify(items));
   1247 
   1248       showNotificationGroups(items.notificationGroups).then(function() {
   1249         chrome.storage.local.set(items);
   1250       });
   1251     });
   1252   });
   1253 
   1254   initialize();
   1255 });
   1256 
   1257 authenticationManager.addListener(function() {
   1258   console.log('signIn State Change');
   1259   onStateChange();
   1260 });
   1261 
   1262 instrumented.notifications.onClicked.addListener(
   1263     function(chromeNotificationId) {
   1264       chrome.metricsPrivate.recordUserAction('GoogleNow.MessageClicked');
   1265       onNotificationClicked(chromeNotificationId,
   1266           function(notificationDataEntry) {
   1267             var actionUrls = notificationDataEntry.actionUrls;
   1268             var url = actionUrls && actionUrls.messageUrl;
   1269             if (url) {
   1270               recordNotificationClick(notificationDataEntry.cardTypeId);
   1271             }
   1272             return url;
   1273           });
   1274         });
   1275 
   1276 instrumented.notifications.onButtonClicked.addListener(
   1277     function(chromeNotificationId, buttonIndex) {
   1278       chrome.metricsPrivate.recordUserAction(
   1279           'GoogleNow.ButtonClicked' + buttonIndex);
   1280       onNotificationClicked(chromeNotificationId,
   1281           function(notificationDataEntry) {
   1282             var actionUrls = notificationDataEntry.actionUrls;
   1283             var url = actionUrls.buttonUrls[buttonIndex];
   1284             if (url) {
   1285               recordButtonClick(notificationDataEntry.cardTypeId, buttonIndex);
   1286             } else {
   1287               verify(false, 'onButtonClicked: no url for a button');
   1288               console.log(
   1289                   'buttonIndex=' + buttonIndex + ' ' +
   1290                   'chromeNotificationId=' + chromeNotificationId + ' ' +
   1291                   'notificationDataEntry=' +
   1292                   JSON.stringify(notificationDataEntry));
   1293             }
   1294             return url;
   1295           });
   1296         });
   1297 
   1298 instrumented.notifications.onClosed.addListener(onNotificationClosed);
   1299 
   1300 instrumented.notifications.onPermissionLevelChanged.addListener(
   1301     function(permissionLevel) {
   1302       console.log('Notifications permissionLevel Change');
   1303       onStateChange();
   1304     });
   1305 
   1306 instrumented.notifications.onShowSettings.addListener(function() {
   1307   openUrl(SETTINGS_URL);
   1308 });
   1309 
   1310 // Handles state change notifications for the Google Now enabled bit.
   1311 instrumented.storage.onChanged.addListener(function(changes, areaName) {
   1312   if (areaName === 'local') {
   1313     if ('googleNowEnabled' in changes) {
   1314       onStateChange();
   1315     }
   1316   }
   1317 });
   1318 
   1319 instrumented.pushMessaging.onMessage.addListener(function(message) {
   1320   // message.payload will be '' when the extension first starts.
   1321   // Each time after signing in, we'll get latest payload for all channels.
   1322   // So, we need to poll the server only when the payload is non-empty and has
   1323   // changed.
   1324   console.log('pushMessaging.onMessage ' + JSON.stringify(message));
   1325   if (message.payload.indexOf('REQUEST_CARDS') == 0) {
   1326     tasks.add(ON_PUSH_MESSAGE_START_TASK_NAME, function() {
   1327       // Accept promise rejection on failure since it's safer to do nothing,
   1328       // preventing polling the server when the payload really didn't change.
   1329       fillFromChromeLocalStorage({
   1330         lastPollNowPayloads: {},
   1331         /** @type {Object.<string, StoredNotificationGroup>} */
   1332         notificationGroups: {}
   1333       }, PromiseRejection.ALLOW).then(function(items) {
   1334         if (items.lastPollNowPayloads[message.subchannelId] !=
   1335             message.payload) {
   1336           items.lastPollNowPayloads[message.subchannelId] = message.payload;
   1337 
   1338           items.notificationGroups['PUSH' + message.subchannelId] = {
   1339             cards: [],
   1340             nextPollTime: Date.now()
   1341           };
   1342 
   1343           chrome.storage.local.set({
   1344             lastPollNowPayloads: items.lastPollNowPayloads,
   1345             notificationGroups: items.notificationGroups
   1346           });
   1347 
   1348           pollOptedInWithRecheck();
   1349         }
   1350       });
   1351     });
   1352   }
   1353 });
   1354