Add ENABLE_MEDIA_ROUTER define to builds other than Android and iOS.
[chromium-blink-merge.git] / chrome / browser / resources / google_now / background.js
blobe24f70b499b482ea73d0a51204315cb4d34bcb2f
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 'use strict';
7 /**
8 * @fileoverview The event page for Google Now for Chrome implementation.
9 * The Google Now event page gets Google Now cards from the server and shows
10 * them as Chrome notifications.
11 * The service performs periodic updating of Google Now cards.
12 * Each updating of the cards includes 4 steps:
13 * 1. Processing requests for cards dismissals that are not yet sent to the
14 * server.
15 * 2. Making a server request.
16 * 3. Showing the received cards as notifications.
19 // TODO(robliao): Decide what to do in incognito mode.
21 /**
22 * Standard response code for successful HTTP requests. This is the only success
23 * code the server will send.
25 var HTTP_OK = 200;
26 var HTTP_NOCONTENT = 204;
28 var HTTP_BAD_REQUEST = 400;
29 var HTTP_UNAUTHORIZED = 401;
30 var HTTP_FORBIDDEN = 403;
31 var HTTP_METHOD_NOT_ALLOWED = 405;
33 var MS_IN_SECOND = 1000;
34 var MS_IN_MINUTE = 60 * 1000;
36 /**
37 * Initial period for polling for Google Now Notifications cards to use when the
38 * period from the server is not available.
40 var INITIAL_POLLING_PERIOD_SECONDS = 5 * 60; // 5 minutes
42 /**
43 * Mininal period for polling for Google Now Notifications cards.
45 var MINIMUM_POLLING_PERIOD_SECONDS = 5 * 60; // 5 minutes
47 /**
48 * Maximal period for polling for Google Now Notifications cards to use when the
49 * period from the server is not available.
51 var MAXIMUM_POLLING_PERIOD_SECONDS = 30 * 60; // 30 minutes
53 /**
54 * Initial period for polling for Google Now optin notification after push
55 * messaging indicates Google Now is enabled.
57 var INITIAL_OPTIN_RECHECK_PERIOD_SECONDS = 60; // 1 minute
59 /**
60 * Maximum period for polling for Google Now optin notification after push
61 * messaging indicates Google Now is enabled. It is expected that the alarm
62 * will be stopped after this.
64 var MAXIMUM_OPTIN_RECHECK_PERIOD_SECONDS = 16 * 60; // 16 minutes
66 /**
67 * Initial period for retrying the server request for dismissing cards.
69 var INITIAL_RETRY_DISMISS_PERIOD_SECONDS = 60; // 1 minute
71 /**
72 * Maximum period for retrying the server request for dismissing cards.
74 var MAXIMUM_RETRY_DISMISS_PERIOD_SECONDS = 60 * 60; // 1 hour
76 /**
77 * Time we keep retrying dismissals.
79 var MAXIMUM_DISMISSAL_AGE_MS = 24 * 60 * 60 * 1000; // 1 day
81 /**
82 * Time we keep dismissals after successful server dismiss requests.
84 var DISMISS_RETENTION_TIME_MS = 20 * 60 * 1000; // 20 minutes
86 /**
87 * Default period for checking whether the user is opted in to Google Now.
89 var DEFAULT_OPTIN_CHECK_PERIOD_SECONDS = 60 * 60 * 24 * 7; // 1 week
91 /**
92 * URL to open when the user clicked on a link for the our notification
93 * settings.
95 var SETTINGS_URL = 'https://support.google.com/chrome/?p=ib_google_now_welcome';
97 /**
98 * GCM registration URL.
100 var GCM_REGISTRATION_URL =
101 'https://android.googleapis.com/gcm/googlenotification';
104 * DevConsole project ID for GCM API use.
106 var GCM_PROJECT_ID = '437902709571';
109 * Number of cards that need an explanatory link.
111 var EXPLANATORY_CARDS_LINK_THRESHOLD = 4;
114 * Names for tasks that can be created by the extension.
116 var UPDATE_CARDS_TASK_NAME = 'update-cards';
117 var DISMISS_CARD_TASK_NAME = 'dismiss-card';
118 var RETRY_DISMISS_TASK_NAME = 'retry-dismiss';
119 var STATE_CHANGED_TASK_NAME = 'state-changed';
120 var SHOW_ON_START_TASK_NAME = 'show-cards-on-start';
121 var ON_PUSH_MESSAGE_START_TASK_NAME = 'on-push-message';
124 * Group as received from the server.
126 * @typedef {{
127 * nextPollSeconds: (string|undefined),
128 * rank: (number|undefined),
129 * requested: (boolean|undefined)
130 * }}
132 var ReceivedGroup;
135 * Server response with notifications and groups.
137 * @typedef {{
138 * googleNowDisabled: (boolean|undefined),
139 * groups: Object<string, ReceivedGroup>,
140 * notifications: Array<ReceivedNotification>
141 * }}
143 var ServerResponse;
146 * Notification group as the client stores it. |cardsTimestamp| and |rank| are
147 * defined if |cards| is non-empty. |nextPollTime| is undefined if the server
148 * (1) never sent 'nextPollSeconds' for the group or
149 * (2) didn't send 'nextPollSeconds' with the last group update containing a
150 * cards update and all the times after that.
152 * @typedef {{
153 * cards: Array<ReceivedNotification>,
154 * cardsTimestamp: (number|undefined),
155 * nextPollTime: (number|undefined),
156 * rank: (number|undefined)
157 * }}
159 var StoredNotificationGroup;
162 * Pending (not yet successfully sent) dismissal for a received notification.
163 * |time| is the moment when the user requested dismissal.
165 * @typedef {{
166 * chromeNotificationId: ChromeNotificationId,
167 * time: number,
168 * dismissalData: DismissalData
169 * }}
171 var PendingDismissal;
174 * Checks if a new task can't be scheduled when another task is already
175 * scheduled.
176 * @param {string} newTaskName Name of the new task.
177 * @param {string} scheduledTaskName Name of the scheduled task.
178 * @return {boolean} Whether the new task conflicts with the existing task.
180 function areTasksConflicting(newTaskName, scheduledTaskName) {
181 if (newTaskName == UPDATE_CARDS_TASK_NAME &&
182 scheduledTaskName == UPDATE_CARDS_TASK_NAME) {
183 // If a card update is requested while an old update is still scheduled, we
184 // don't need the new update.
185 return true;
188 if (newTaskName == RETRY_DISMISS_TASK_NAME &&
189 (scheduledTaskName == UPDATE_CARDS_TASK_NAME ||
190 scheduledTaskName == DISMISS_CARD_TASK_NAME ||
191 scheduledTaskName == RETRY_DISMISS_TASK_NAME)) {
192 // No need to schedule retry-dismiss action if another action that tries to
193 // send dismissals is scheduled.
194 return true;
197 return false;
200 var tasks = buildTaskManager(areTasksConflicting);
202 // Add error processing to API calls.
203 wrapper.instrumentChromeApiFunction('gcm.onMessage.addListener', 0);
204 wrapper.instrumentChromeApiFunction('gcm.register', 1);
205 wrapper.instrumentChromeApiFunction('metricsPrivate.getVariationParams', 1);
206 wrapper.instrumentChromeApiFunction('notifications.clear', 1);
207 wrapper.instrumentChromeApiFunction('notifications.create', 2);
208 wrapper.instrumentChromeApiFunction('notifications.getPermissionLevel', 0);
209 wrapper.instrumentChromeApiFunction('notifications.update', 2);
210 wrapper.instrumentChromeApiFunction('notifications.getAll', 0);
211 wrapper.instrumentChromeApiFunction(
212 'notifications.onButtonClicked.addListener', 0);
213 wrapper.instrumentChromeApiFunction('notifications.onClicked.addListener', 0);
214 wrapper.instrumentChromeApiFunction('notifications.onClosed.addListener', 0);
215 wrapper.instrumentChromeApiFunction(
216 'notifications.onPermissionLevelChanged.addListener', 0);
217 wrapper.instrumentChromeApiFunction(
218 'notifications.onShowSettings.addListener', 0);
219 wrapper.instrumentChromeApiFunction('permissions.contains', 1);
220 wrapper.instrumentChromeApiFunction('runtime.onInstalled.addListener', 0);
221 wrapper.instrumentChromeApiFunction('runtime.onStartup.addListener', 0);
222 wrapper.instrumentChromeApiFunction('storage.onChanged.addListener', 0);
223 wrapper.instrumentChromeApiFunction('tabs.create', 1);
225 var updateCardsAttempts = buildAttemptManager(
226 'cards-update',
227 requestCards,
228 INITIAL_POLLING_PERIOD_SECONDS,
229 MAXIMUM_POLLING_PERIOD_SECONDS);
230 var optInPollAttempts = buildAttemptManager(
231 'optin',
232 pollOptedInNoImmediateRecheck,
233 INITIAL_POLLING_PERIOD_SECONDS,
234 MAXIMUM_POLLING_PERIOD_SECONDS);
235 var optInRecheckAttempts = buildAttemptManager(
236 'optin-recheck',
237 pollOptedInWithRecheck,
238 INITIAL_OPTIN_RECHECK_PERIOD_SECONDS,
239 MAXIMUM_OPTIN_RECHECK_PERIOD_SECONDS);
240 var dismissalAttempts = buildAttemptManager(
241 'dismiss',
242 retryPendingDismissals,
243 INITIAL_RETRY_DISMISS_PERIOD_SECONDS,
244 MAXIMUM_RETRY_DISMISS_PERIOD_SECONDS);
245 var cardSet = buildCardSet();
247 var authenticationManager = buildAuthenticationManager();
250 * Google Now UMA event identifier.
251 * @enum {number}
253 var GoogleNowEvent = {
254 REQUEST_FOR_CARDS_TOTAL: 0,
255 REQUEST_FOR_CARDS_SUCCESS: 1,
256 CARDS_PARSE_SUCCESS: 2,
257 DISMISS_REQUEST_TOTAL: 3,
258 DISMISS_REQUEST_SUCCESS: 4,
259 LOCATION_REQUEST: 5,
260 DELETED_LOCATION_UPDATE: 6,
261 EXTENSION_START: 7,
262 DELETED_SHOW_WELCOME_TOAST: 8,
263 STOPPED: 9,
264 DELETED_USER_SUPPRESSED: 10,
265 SIGNED_OUT: 11,
266 NOTIFICATION_DISABLED: 12,
267 GOOGLE_NOW_DISABLED: 13,
268 EVENTS_TOTAL: 14 // EVENTS_TOTAL is not an event; all new events need to be
269 // added before it.
273 * Records a Google Now Event.
274 * @param {GoogleNowEvent} event Event identifier.
276 function recordEvent(event) {
277 var metricDescription = {
278 metricName: 'GoogleNow.Event',
279 type: 'histogram-linear',
280 min: 1,
281 max: GoogleNowEvent.EVENTS_TOTAL,
282 buckets: GoogleNowEvent.EVENTS_TOTAL + 1
285 chrome.metricsPrivate.recordValue(metricDescription, event);
289 * Records a notification clicked event.
290 * @param {number|undefined} cardTypeId Card type ID.
292 function recordNotificationClick(cardTypeId) {
293 if (cardTypeId !== undefined) {
294 chrome.metricsPrivate.recordSparseValue(
295 'GoogleNow.Card.Clicked', cardTypeId);
300 * Records a button clicked event.
301 * @param {number|undefined} cardTypeId Card type ID.
302 * @param {number} buttonIndex Button Index
304 function recordButtonClick(cardTypeId, buttonIndex) {
305 if (cardTypeId !== undefined) {
306 chrome.metricsPrivate.recordSparseValue(
307 'GoogleNow.Card.Button.Clicked' + buttonIndex, cardTypeId);
312 * Checks the result of the HTTP Request and updates the authentication
313 * manager on any failure.
314 * @param {string} token Authentication token to validate against an
315 * XMLHttpRequest.
316 * @return {function(XMLHttpRequest)} Function that validates the token with the
317 * supplied XMLHttpRequest.
319 function checkAuthenticationStatus(token) {
320 return function(request) {
321 if (request.status == HTTP_FORBIDDEN ||
322 request.status == HTTP_UNAUTHORIZED) {
323 authenticationManager.removeToken(token);
329 * Builds and sends an authenticated request to the notification server.
330 * @param {string} method Request method.
331 * @param {string} handlerName Server handler to send the request to.
332 * @param {string=} opt_contentType Value for the Content-type header.
333 * @return {Promise} A promise to issue a request to the server.
334 * The promise rejects if the response is not within the HTTP 200 range.
336 function requestFromServer(method, handlerName, opt_contentType) {
337 return authenticationManager.getAuthToken().then(function(token) {
338 var request = buildServerRequest(method, handlerName, opt_contentType);
339 request.setRequestHeader('Authorization', 'Bearer ' + token);
340 var requestPromise = new Promise(function(resolve, reject) {
341 request.addEventListener('loadend', function() {
342 if ((200 <= request.status) && (request.status < 300)) {
343 resolve(request);
344 } else {
345 reject(request);
347 }, false);
348 request.send();
350 requestPromise.catch(checkAuthenticationStatus(token));
351 return requestPromise;
356 * Shows the notification groups as notification cards.
357 * @param {Object<string, StoredNotificationGroup>} notificationGroups Map from
358 * group name to group information.
359 * @param {function(ReceivedNotification)=} opt_onCardShown Optional parameter
360 * called when each card is shown.
361 * @return {Promise} A promise to show the notification groups as cards.
363 function showNotificationGroups(notificationGroups, opt_onCardShown) {
364 /** @type {Object<ChromeNotificationId, CombinedCard>} */
365 var cards = combineCardsFromGroups(notificationGroups);
366 console.log('showNotificationGroups ' + JSON.stringify(cards));
368 return new Promise(function(resolve) {
369 instrumented.notifications.getAll(function(notifications) {
370 console.log('showNotificationGroups-getAll ' +
371 JSON.stringify(notifications));
372 notifications = notifications || {};
374 // Mark notifications that didn't receive an update as having received
375 // an empty update.
376 for (var chromeNotificationId in notifications) {
377 cards[chromeNotificationId] = cards[chromeNotificationId] || [];
380 /** @type {Object<ChromeNotificationId, NotificationDataEntry>} */
381 var notificationsData = {};
383 // Create/update/delete notifications.
384 for (var chromeNotificationId in cards) {
385 notificationsData[chromeNotificationId] = cardSet.update(
386 chromeNotificationId,
387 cards[chromeNotificationId],
388 notificationGroups,
389 opt_onCardShown);
391 chrome.storage.local.set({notificationsData: notificationsData});
392 resolve();
398 * Removes all cards and card state on Google Now close down.
400 function removeAllCards() {
401 console.log('removeAllCards');
403 // TODO(robliao): Once Google Now clears its own checkbox in the
404 // notifications center and bug 260376 is fixed, the below clearing
405 // code is no longer necessary.
406 instrumented.notifications.getAll(function(notifications) {
407 notifications = notifications || {};
408 for (var chromeNotificationId in notifications) {
409 instrumented.notifications.clear(chromeNotificationId, function() {});
411 chrome.storage.local.remove(['notificationsData', 'notificationGroups']);
416 * Adds a card group into a set of combined cards.
417 * @param {Object<ChromeNotificationId, CombinedCard>} combinedCards Map from
418 * chromeNotificationId to a combined card.
419 * This is an input/output parameter.
420 * @param {StoredNotificationGroup} storedGroup Group to combine into the
421 * combined card set.
423 function combineGroup(combinedCards, storedGroup) {
424 for (var i = 0; i < storedGroup.cards.length; i++) {
425 /** @type {ReceivedNotification} */
426 var receivedNotification = storedGroup.cards[i];
428 /** @type {UncombinedNotification} */
429 var uncombinedNotification = {
430 receivedNotification: receivedNotification,
431 showTime: receivedNotification.trigger.showTimeSec &&
432 (storedGroup.cardsTimestamp +
433 receivedNotification.trigger.showTimeSec * MS_IN_SECOND),
434 hideTime: storedGroup.cardsTimestamp +
435 receivedNotification.trigger.hideTimeSec * MS_IN_SECOND
438 var combinedCard =
439 combinedCards[receivedNotification.chromeNotificationId] || [];
440 combinedCard.push(uncombinedNotification);
441 combinedCards[receivedNotification.chromeNotificationId] = combinedCard;
446 * Calculates the soonest poll time from a map of groups as an absolute time.
447 * @param {Object<string, StoredNotificationGroup>} groups Map from group name
448 * to group information.
449 * @return {number} The next poll time based off of the groups.
451 function calculateNextPollTimeMilliseconds(groups) {
452 var nextPollTime = null;
454 for (var groupName in groups) {
455 var group = groups[groupName];
456 if (group.nextPollTime !== undefined) {
457 nextPollTime = nextPollTime == null ?
458 group.nextPollTime : Math.min(group.nextPollTime, nextPollTime);
462 // At least one of the groups must have nextPollTime.
463 verify(nextPollTime != null, 'calculateNextPollTime: nextPollTime is null');
464 return nextPollTime;
468 * Schedules next cards poll.
469 * @param {Object<string, StoredNotificationGroup>} groups Map from group name
470 * to group information.
472 function scheduleNextCardsPoll(groups) {
473 var nextPollTimeMs = calculateNextPollTimeMilliseconds(groups);
475 var nextPollDelaySeconds = Math.max(
476 (nextPollTimeMs - Date.now()) / MS_IN_SECOND,
477 MINIMUM_POLLING_PERIOD_SECONDS);
478 updateCardsAttempts.start(nextPollDelaySeconds);
482 * Schedules the next opt-in check poll.
484 function scheduleOptInCheckPoll() {
485 instrumented.metricsPrivate.getVariationParams(
486 'GoogleNow', function(params) {
487 var optinPollPeriodSeconds =
488 parseInt(params && params.optinPollPeriodSeconds, 10) ||
489 DEFAULT_OPTIN_CHECK_PERIOD_SECONDS;
490 optInPollAttempts.start(optinPollPeriodSeconds);
495 * Combines notification groups into a set of Chrome notifications.
496 * @param {Object<string, StoredNotificationGroup>} notificationGroups Map from
497 * group name to group information.
498 * @return {Object<ChromeNotificationId, CombinedCard>} Cards to show.
500 function combineCardsFromGroups(notificationGroups) {
501 console.log('combineCardsFromGroups ' + JSON.stringify(notificationGroups));
502 /** @type {Object<ChromeNotificationId, CombinedCard>} */
503 var combinedCards = {};
505 for (var groupName in notificationGroups)
506 combineGroup(combinedCards, notificationGroups[groupName]);
508 return combinedCards;
512 * Processes a server response for consumption by showNotificationGroups.
513 * @param {ServerResponse} response Server response.
514 * @return {Promise} A promise to process the server response and provide
515 * updated groups. Rejects if the server response shouldn't be processed.
517 function processServerResponse(response) {
518 console.log('processServerResponse ' + JSON.stringify(response));
520 if (response.googleNowDisabled) {
521 chrome.storage.local.set({googleNowEnabled: false});
522 // Stop processing now. The state change will clear the cards.
523 return Promise.reject();
526 var receivedGroups = response.groups;
528 return fillFromChromeLocalStorage({
529 /** @type {Object<string, StoredNotificationGroup>} */
530 notificationGroups: {},
531 /** @type {Object<ServerNotificationId, number>} */
532 recentDismissals: {}
533 }).then(function(items) {
534 console.log('processServerResponse-get ' + JSON.stringify(items));
536 // Build a set of non-expired recent dismissals. It will be used for
537 // client-side filtering of cards.
538 /** @type {Object<ServerNotificationId, number>} */
539 var updatedRecentDismissals = {};
540 var now = Date.now();
541 for (var serverNotificationId in items.recentDismissals) {
542 var dismissalAge = now - items.recentDismissals[serverNotificationId];
543 if (dismissalAge < DISMISS_RETENTION_TIME_MS) {
544 updatedRecentDismissals[serverNotificationId] =
545 items.recentDismissals[serverNotificationId];
549 // Populate groups with corresponding cards.
550 if (response.notifications) {
551 for (var i = 0; i < response.notifications.length; ++i) {
552 /** @type {ReceivedNotification} */
553 var card = response.notifications[i];
554 if (!(card.notificationId in updatedRecentDismissals)) {
555 var group = receivedGroups[card.groupName];
556 group.cards = group.cards || [];
557 group.cards.push(card);
562 // Build updated set of groups.
563 var updatedGroups = {};
565 for (var groupName in receivedGroups) {
566 var receivedGroup = receivedGroups[groupName];
567 var storedGroup = items.notificationGroups[groupName] || {
568 cards: [],
569 cardsTimestamp: undefined,
570 nextPollTime: undefined,
571 rank: undefined
574 if (receivedGroup.requested)
575 receivedGroup.cards = receivedGroup.cards || [];
577 if (receivedGroup.cards) {
578 // If the group contains a cards update, all its fields will get new
579 // values.
580 storedGroup.cards = receivedGroup.cards;
581 storedGroup.cardsTimestamp = now;
582 storedGroup.rank = receivedGroup.rank;
583 storedGroup.nextPollTime = undefined;
584 // The code below assigns nextPollTime a defined value if
585 // nextPollSeconds is specified in the received group.
586 // If the group's cards are not updated, and nextPollSeconds is
587 // unspecified, this method doesn't change group's nextPollTime.
590 // 'nextPollSeconds' may be sent even for groups that don't contain
591 // cards updates.
592 if (receivedGroup.nextPollSeconds !== undefined) {
593 storedGroup.nextPollTime =
594 now + receivedGroup.nextPollSeconds * MS_IN_SECOND;
597 updatedGroups[groupName] = storedGroup;
600 scheduleNextCardsPoll(updatedGroups);
601 return {
602 updatedGroups: updatedGroups,
603 recentDismissals: updatedRecentDismissals
609 * Update the Explanatory Total Cards Shown Count.
611 function countExplanatoryCard() {
612 localStorage['explanatoryCardsShown']++;
616 * Determines if cards should have an explanation link.
617 * @return {boolean} true if an explanatory card should be shown.
619 function shouldShowExplanatoryCard() {
620 var isBelowThreshold =
621 localStorage['explanatoryCardsShown'] < EXPLANATORY_CARDS_LINK_THRESHOLD;
622 return isBelowThreshold;
626 * Requests notification cards from the server for specified groups.
627 * @param {Array<string>} groupNames Names of groups that need to be refreshed.
628 * @return {Promise} A promise to request the specified notification groups.
630 function requestNotificationGroupsFromServer(groupNames) {
631 console.log(
632 'requestNotificationGroupsFromServer from ' + NOTIFICATION_CARDS_URL +
633 ', groupNames=' + JSON.stringify(groupNames));
635 recordEvent(GoogleNowEvent.REQUEST_FOR_CARDS_TOTAL);
637 var requestParameters = '?timeZoneOffsetMs=' +
638 (-new Date().getTimezoneOffset() * MS_IN_MINUTE);
640 if (shouldShowExplanatoryCard()) {
641 requestParameters += '&cardExplanation=true';
644 groupNames.forEach(function(groupName) {
645 requestParameters += ('&requestTypes=' + groupName);
648 requestParameters += '&uiLocale=' + navigator.language;
650 console.log(
651 'requestNotificationGroupsFromServer: request=' + requestParameters);
653 return requestFromServer('GET', 'notifications' + requestParameters).then(
654 function(request) {
655 console.log(
656 'requestNotificationGroupsFromServer-received ' + request.status);
657 if (request.status == HTTP_OK) {
658 recordEvent(GoogleNowEvent.REQUEST_FOR_CARDS_SUCCESS);
659 return JSON.parse(request.responseText);
665 * Performs an opt-in poll without an immediate recheck.
666 * If the response is not opted-in, schedule an opt-in check poll.
668 function pollOptedInNoImmediateRecheck() {
669 requestAndUpdateOptedIn()
670 .then(function(optedIn) {
671 if (!optedIn) {
672 // Request a repoll if we're not opted in.
673 return Promise.reject();
676 .catch(function() {
677 scheduleOptInCheckPoll();
682 * Requests the account opted-in state from the server and updates any
683 * state as necessary.
684 * @return {Promise} A promise to request and update the opted-in state.
685 * The promise resolves with the opt-in state.
687 function requestAndUpdateOptedIn() {
688 console.log('requestOptedIn from ' + NOTIFICATION_CARDS_URL);
690 return requestFromServer('GET', 'settings/optin').then(function(request) {
691 console.log(
692 'requestOptedIn-received ' + request.status + ' ' + request.response);
693 if (request.status == HTTP_OK) {
694 var parsedResponse = JSON.parse(request.responseText);
695 return parsedResponse.value;
697 }).then(function(optedIn) {
698 chrome.storage.local.set({googleNowEnabled: optedIn});
699 return optedIn;
704 * Determines the groups that need to be requested right now.
705 * @return {Promise} A promise to determine the groups to request.
707 function getGroupsToRequest() {
708 return fillFromChromeLocalStorage({
709 /** @type {Object<string, StoredNotificationGroup>} */
710 notificationGroups: {}
711 }).then(function(items) {
712 console.log('getGroupsToRequest-storage-get ' + JSON.stringify(items));
713 var groupsToRequest = [];
714 var now = Date.now();
716 for (var groupName in items.notificationGroups) {
717 var group = items.notificationGroups[groupName];
718 if (group.nextPollTime !== undefined && group.nextPollTime <= now)
719 groupsToRequest.push(groupName);
721 return groupsToRequest;
726 * Requests notification cards from the server.
727 * @return {Promise} A promise to request the notification cards.
728 * Rejects if the cards won't be requested.
730 function requestNotificationCards() {
731 console.log('requestNotificationCards');
732 return getGroupsToRequest()
733 .then(requestNotificationGroupsFromServer)
734 .then(processServerResponse)
735 .then(function(processedResponse) {
736 var onCardShown =
737 shouldShowExplanatoryCard() ? countExplanatoryCard : undefined;
738 return showNotificationGroups(
739 processedResponse.updatedGroups, onCardShown).then(function() {
740 chrome.storage.local.set({
741 notificationGroups: processedResponse.updatedGroups,
742 recentDismissals: processedResponse.updatedRecentDismissals
744 recordEvent(GoogleNowEvent.CARDS_PARSE_SUCCESS);
751 * Determines if an immediate retry should occur based off of the given groups.
752 * The NOR group is expected most often and less latency sensitive, so we will
753 * simply wait MAXIMUM_POLLING_PERIOD_SECONDS before trying again.
754 * @param {Array<string>} groupNames Names of groups that need to be refreshed.
755 * @return {boolean} Whether a retry should occur.
757 function shouldScheduleRetryFromGroupList(groupNames) {
758 return (groupNames.length != 1) || (groupNames[0] !== 'NOR');
762 * Requests and shows notification cards.
764 function requestCards() {
765 console.log('requestCards @' + new Date());
766 // LOCATION_REQUEST is a legacy histogram value when we requested location.
767 // This corresponds to the extension attempting to request for cards.
768 // We're keeping the name the same to keep our histograms in order.
769 recordEvent(GoogleNowEvent.LOCATION_REQUEST);
770 tasks.add(UPDATE_CARDS_TASK_NAME, function() {
771 console.log('requestCards-task-begin');
772 updateCardsAttempts.isRunning(function(running) {
773 if (running) {
774 // The cards are requested only if there are no unsent dismissals.
775 processPendingDismissals()
776 .then(requestNotificationCards)
777 .catch(function() {
778 return getGroupsToRequest().then(function(groupsToRequest) {
779 if (shouldScheduleRetryFromGroupList(groupsToRequest)) {
780 updateCardsAttempts.scheduleRetry();
790 * Sends a server request to dismiss a card.
791 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
792 * the card.
793 * @param {number} dismissalTimeMs Time of the user's dismissal of the card in
794 * milliseconds since epoch.
795 * @param {DismissalData} dismissalData Data to build a dismissal request.
796 * @return {Promise} A promise to request the card dismissal, rejects on error.
798 function requestCardDismissal(
799 chromeNotificationId, dismissalTimeMs, dismissalData) {
800 console.log('requestDismissingCard ' + chromeNotificationId +
801 ' from ' + NOTIFICATION_CARDS_URL +
802 ', dismissalData=' + JSON.stringify(dismissalData));
804 var dismissalAge = Date.now() - dismissalTimeMs;
806 if (dismissalAge > MAXIMUM_DISMISSAL_AGE_MS) {
807 return Promise.resolve();
810 recordEvent(GoogleNowEvent.DISMISS_REQUEST_TOTAL);
812 var requestParameters = 'notifications/' + dismissalData.notificationId +
813 '?age=' + dismissalAge +
814 '&chromeNotificationId=' + chromeNotificationId;
816 for (var paramField in dismissalData.parameters)
817 requestParameters += ('&' + paramField +
818 '=' + dismissalData.parameters[paramField]);
820 console.log('requestCardDismissal: requestParameters=' + requestParameters);
822 return requestFromServer('DELETE', requestParameters).then(function(request) {
823 console.log('requestDismissingCard-onloadend ' + request.status);
824 if (request.status == HTTP_NOCONTENT)
825 recordEvent(GoogleNowEvent.DISMISS_REQUEST_SUCCESS);
827 // A dismissal doesn't require further retries if it was successful or
828 // doesn't have a chance for successful completion.
829 return (request.status == HTTP_NOCONTENT) ?
830 Promise.resolve() :
831 Promise.reject();
832 }).catch(function(request) {
833 request = (typeof request === 'object') ? request : {};
834 return (request.status == HTTP_BAD_REQUEST ||
835 request.status == HTTP_METHOD_NOT_ALLOWED) ?
836 Promise.resolve() :
837 Promise.reject();
842 * Tries to send dismiss requests for all pending dismissals.
843 * @return {Promise} A promise to process the pending dismissals.
844 * The promise is rejected if a problem was encountered.
846 function processPendingDismissals() {
847 return fillFromChromeLocalStorage({
848 /** @type {Array<PendingDismissal>} */
849 pendingDismissals: [],
850 /** @type {Object<ServerNotificationId, number>} */
851 recentDismissals: {}
852 }).then(function(items) {
853 console.log(
854 'processPendingDismissals-storage-get ' + JSON.stringify(items));
856 var dismissalsChanged = false;
858 function onFinish(success) {
859 if (dismissalsChanged) {
860 chrome.storage.local.set({
861 pendingDismissals: items.pendingDismissals,
862 recentDismissals: items.recentDismissals
865 return success ? Promise.resolve() : Promise.reject();
868 function doProcessDismissals() {
869 if (items.pendingDismissals.length == 0) {
870 dismissalAttempts.stop();
871 return onFinish(true);
874 // Send dismissal for the first card, and if successful, repeat
875 // recursively with the rest.
876 /** @type {PendingDismissal} */
877 var dismissal = items.pendingDismissals[0];
878 return requestCardDismissal(
879 dismissal.chromeNotificationId,
880 dismissal.time,
881 dismissal.dismissalData).then(function() {
882 dismissalsChanged = true;
883 items.pendingDismissals.splice(0, 1);
884 items.recentDismissals[dismissal.dismissalData.notificationId] =
885 Date.now();
886 return doProcessDismissals();
887 }).catch(function() {
888 return onFinish(false);
892 return doProcessDismissals();
897 * Submits a task to send pending dismissals.
899 function retryPendingDismissals() {
900 tasks.add(RETRY_DISMISS_TASK_NAME, function() {
901 processPendingDismissals().catch(dismissalAttempts.scheduleRetry);
906 * Opens a URL in a new tab.
907 * @param {string} url URL to open.
909 function openUrl(url) {
910 instrumented.tabs.create({url: url}, function(tab) {
911 if (tab)
912 chrome.windows.update(tab.windowId, {focused: true});
913 else
914 chrome.windows.create({url: url, focused: true});
919 * Opens URL corresponding to the clicked part of the notification.
920 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
921 * the card.
922 * @param {function(NotificationDataEntry): (string|undefined)} selector
923 * Function that extracts the url for the clicked area from the
924 * notification data entry.
926 function onNotificationClicked(chromeNotificationId, selector) {
927 fillFromChromeLocalStorage({
928 /** @type {Object<ChromeNotificationId, NotificationDataEntry>} */
929 notificationsData: {}
930 }).then(function(items) {
931 /** @type {(NotificationDataEntry|undefined)} */
932 var notificationDataEntry = items.notificationsData[chromeNotificationId];
933 if (!notificationDataEntry)
934 return;
936 var url = selector(notificationDataEntry);
937 if (!url)
938 return;
940 openUrl(url);
945 * Callback for chrome.notifications.onClosed event.
946 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
947 * the card.
948 * @param {boolean} byUser Whether the notification was closed by the user.
950 function onNotificationClosed(chromeNotificationId, byUser) {
951 if (!byUser)
952 return;
954 // At this point we are guaranteed that the notification is a now card.
955 chrome.metricsPrivate.recordUserAction('GoogleNow.Dismissed');
957 tasks.add(DISMISS_CARD_TASK_NAME, function() {
958 dismissalAttempts.start();
960 fillFromChromeLocalStorage({
961 /** @type {Array<PendingDismissal>} */
962 pendingDismissals: [],
963 /** @type {Object<ChromeNotificationId, NotificationDataEntry>} */
964 notificationsData: {},
965 /** @type {Object<string, StoredNotificationGroup>} */
966 notificationGroups: {}
967 }).then(function(items) {
968 /** @type {NotificationDataEntry} */
969 var notificationData =
970 items.notificationsData[chromeNotificationId] ||
972 timestamp: Date.now(),
973 combinedCard: []
976 var dismissalResult =
977 cardSet.onDismissal(
978 chromeNotificationId,
979 notificationData,
980 items.notificationGroups);
982 for (var i = 0; i < dismissalResult.dismissals.length; i++) {
983 /** @type {PendingDismissal} */
984 var dismissal = {
985 chromeNotificationId: chromeNotificationId,
986 time: Date.now(),
987 dismissalData: dismissalResult.dismissals[i]
989 items.pendingDismissals.push(dismissal);
992 items.notificationsData[chromeNotificationId] =
993 dismissalResult.notificationData;
995 chrome.storage.local.set(items);
997 processPendingDismissals();
1003 * Initializes the polling system to start fetching cards.
1005 function startPollingCards() {
1006 console.log('startPollingCards');
1007 // Create an update timer for a case when for some reason requesting
1008 // cards gets stuck.
1009 updateCardsAttempts.start(MAXIMUM_POLLING_PERIOD_SECONDS);
1010 requestCards();
1014 * Stops all machinery in the polling system.
1016 function stopPollingCards() {
1017 console.log('stopPollingCards');
1018 updateCardsAttempts.stop();
1019 // Since we're stopping everything, clear all runtime storage.
1020 // We don't clear localStorage since those values are still relevant
1021 // across Google Now start-stop events.
1022 chrome.storage.local.clear();
1026 * Initializes the event page on install or on browser startup.
1028 function initialize() {
1029 recordEvent(GoogleNowEvent.EXTENSION_START);
1030 registerForGcm();
1031 onStateChange();
1035 * Starts or stops the main pipeline for polling cards.
1036 * @param {boolean} shouldPollCardsRequest true to start and
1037 * false to stop polling cards.
1039 function setShouldPollCards(shouldPollCardsRequest) {
1040 updateCardsAttempts.isRunning(function(currentValue) {
1041 if (shouldPollCardsRequest != currentValue) {
1042 console.log('Action Taken setShouldPollCards=' + shouldPollCardsRequest);
1043 if (shouldPollCardsRequest)
1044 startPollingCards();
1045 else
1046 stopPollingCards();
1047 } else {
1048 console.log(
1049 'Action Ignored setShouldPollCards=' + shouldPollCardsRequest);
1055 * Starts or stops the optin check.
1056 * @param {boolean} shouldPollOptInStatus true to start and false to stop
1057 * polling the optin status.
1059 function setShouldPollOptInStatus(shouldPollOptInStatus) {
1060 optInPollAttempts.isRunning(function(currentValue) {
1061 if (shouldPollOptInStatus != currentValue) {
1062 console.log(
1063 'Action Taken setShouldPollOptInStatus=' + shouldPollOptInStatus);
1064 if (shouldPollOptInStatus) {
1065 pollOptedInNoImmediateRecheck();
1066 } else {
1067 optInPollAttempts.stop();
1069 } else {
1070 console.log(
1071 'Action Ignored setShouldPollOptInStatus=' + shouldPollOptInStatus);
1077 * Enables or disables the Google Now background permission.
1078 * @param {boolean} backgroundEnable true to run in the background.
1079 * false to not run in the background.
1081 function setBackgroundEnable(backgroundEnable) {
1082 instrumented.permissions.contains({permissions: ['background']},
1083 function(hasPermission) {
1084 if (backgroundEnable != hasPermission) {
1085 console.log('Action Taken setBackgroundEnable=' + backgroundEnable);
1086 if (backgroundEnable)
1087 chrome.permissions.request({permissions: ['background']});
1088 else
1089 chrome.permissions.remove({permissions: ['background']});
1090 } else {
1091 console.log('Action Ignored setBackgroundEnable=' + backgroundEnable);
1097 * Record why this extension would not poll for cards.
1098 * @param {boolean} signedIn true if the user is signed in.
1099 * @param {boolean} notificationEnabled true if
1100 * Google Now for Chrome is allowed to show notifications.
1101 * @param {boolean} googleNowEnabled true if
1102 * the Google Now is enabled for the user.
1104 function recordEventIfNoCards(signedIn, notificationEnabled, googleNowEnabled) {
1105 if (!signedIn) {
1106 recordEvent(GoogleNowEvent.SIGNED_OUT);
1107 } else if (!notificationEnabled) {
1108 recordEvent(GoogleNowEvent.NOTIFICATION_DISABLED);
1109 } else if (!googleNowEnabled) {
1110 recordEvent(GoogleNowEvent.GOOGLE_NOW_DISABLED);
1115 * Does the actual work of deciding what Google Now should do
1116 * based off of the current state of Chrome.
1117 * @param {boolean} signedIn true if the user is signed in.
1118 * @param {boolean} canEnableBackground true if
1119 * the background permission can be requested.
1120 * @param {boolean} notificationEnabled true if
1121 * Google Now for Chrome is allowed to show notifications.
1122 * @param {boolean} googleNowEnabled true if
1123 * the Google Now is enabled for the user.
1125 function updateRunningState(
1126 signedIn,
1127 canEnableBackground,
1128 notificationEnabled,
1129 googleNowEnabled) {
1130 console.log(
1131 'State Update signedIn=' + signedIn + ' ' +
1132 'canEnableBackground=' + canEnableBackground + ' ' +
1133 'notificationEnabled=' + notificationEnabled + ' ' +
1134 'googleNowEnabled=' + googleNowEnabled);
1136 var shouldPollCards = false;
1137 var shouldPollOptInStatus = false;
1138 var shouldSetBackground = false;
1140 if (signedIn && notificationEnabled) {
1141 shouldPollCards = googleNowEnabled;
1142 shouldPollOptInStatus = !googleNowEnabled;
1143 shouldSetBackground = canEnableBackground && googleNowEnabled;
1144 } else {
1145 recordEvent(GoogleNowEvent.STOPPED);
1148 recordEventIfNoCards(signedIn, notificationEnabled, googleNowEnabled);
1150 console.log(
1151 'Requested Actions shouldSetBackground=' + shouldSetBackground + ' ' +
1152 'setShouldPollCards=' + shouldPollCards + ' ' +
1153 'shouldPollOptInStatus=' + shouldPollOptInStatus);
1155 setBackgroundEnable(shouldSetBackground);
1156 setShouldPollCards(shouldPollCards);
1157 setShouldPollOptInStatus(shouldPollOptInStatus);
1158 if (!shouldPollCards) {
1159 removeAllCards();
1164 * Coordinates the behavior of Google Now for Chrome depending on
1165 * Chrome and extension state.
1167 function onStateChange() {
1168 tasks.add(STATE_CHANGED_TASK_NAME, function() {
1169 Promise.all([
1170 authenticationManager.isSignedIn(),
1171 canEnableBackground(),
1172 isNotificationsEnabled(),
1173 isGoogleNowEnabled()])
1174 .then(function(results) {
1175 updateRunningState.apply(null, results);
1181 * Determines if background mode should be requested.
1182 * @return {Promise} A promise to determine if background can be enabled.
1184 function canEnableBackground() {
1185 return new Promise(function(resolve) {
1186 instrumented.metricsPrivate.getVariationParams(
1187 'GoogleNow',
1188 function(response) {
1189 resolve(!response || (response.canEnableBackground != 'false'));
1195 * Checks if Google Now is enabled in the notifications center.
1196 * @return {Promise} A promise to determine if Google Now is enabled
1197 * in the notifications center.
1199 function isNotificationsEnabled() {
1200 return new Promise(function(resolve) {
1201 instrumented.notifications.getPermissionLevel(function(level) {
1202 resolve(level == 'granted');
1208 * Gets the previous Google Now opt-in state.
1209 * @return {Promise} A promise to determine the previous Google Now
1210 * opt-in state.
1212 function isGoogleNowEnabled() {
1213 return fillFromChromeLocalStorage({googleNowEnabled: false})
1214 .then(function(items) {
1215 return items.googleNowEnabled;
1220 * Ensures the extension is ready to listen for GCM messages.
1222 function registerForGcm() {
1223 // We don't need to use the key yet, just ensure the channel is set up.
1224 getGcmNotificationKey();
1228 * Returns a Promise resolving to either a cached or new GCM notification key.
1229 * Rejects if registration fails.
1230 * @return {Promise} A Promise that resolves to a potentially-cached GCM key.
1232 function getGcmNotificationKey() {
1233 return fillFromChromeLocalStorage({gcmNotificationKey: undefined})
1234 .then(function(items) {
1235 if (items.gcmNotificationKey) {
1236 console.log('Reused gcm key from storage.');
1237 return Promise.resolve(items.gcmNotificationKey);
1239 return requestNewGcmNotificationKey();
1244 * Returns a promise resolving to a GCM Notificaiton Key. May call
1245 * chrome.gcm.register() first if required. Rejects on registration failure.
1246 * @return {Promise} A Promise that resolves to a fresh GCM Notification key.
1248 function requestNewGcmNotificationKey() {
1249 return getGcmRegistrationId().then(function(gcmId) {
1250 authenticationManager.getAuthToken().then(function(token) {
1251 authenticationManager.getLogin().then(function(username) {
1252 return new Promise(function(resolve, reject) {
1253 var xhr = new XMLHttpRequest();
1254 xhr.responseType = 'application/json';
1255 xhr.open('POST', GCM_REGISTRATION_URL, true);
1256 xhr.setRequestHeader('Content-Type', 'application/json');
1257 xhr.setRequestHeader('Authorization', 'Bearer ' + token);
1258 xhr.setRequestHeader('project_id', GCM_PROJECT_ID);
1259 var payload = {
1260 'operation': 'add',
1261 'notification_key_name': username,
1262 'registration_ids': [gcmId]
1264 xhr.onloadend = function() {
1265 if (xhr.status != 200) {
1266 reject();
1268 var obj = JSON.parse(xhr.responseText);
1269 var key = obj && obj.notification_key;
1270 if (!key) {
1271 reject();
1273 console.log('gcm notification key POST: ' + key);
1274 chrome.storage.local.set({gcmNotificationKey: key});
1275 resolve(key);
1277 xhr.send(JSON.stringify(payload));
1280 }).catch(function() {
1281 // Couldn't obtain a GCM ID. Ignore and fallback to polling.
1287 * Returns a promise resolving to either a cached or new GCM registration ID.
1288 * Rejects if registration fails.
1289 * @return {Promise} A Promise that resolves to a GCM registration ID.
1291 function getGcmRegistrationId() {
1292 return fillFromChromeLocalStorage({gcmRegistrationId: undefined})
1293 .then(function(items) {
1294 if (items.gcmRegistrationId) {
1295 console.log('Reused gcm registration id from storage.');
1296 return Promise.resolve(items.gcmRegistrationId);
1299 return new Promise(function(resolve, reject) {
1300 instrumented.gcm.register([GCM_PROJECT_ID], function(registrationId) {
1301 console.log('gcm.register(): ' + registrationId);
1302 if (registrationId) {
1303 chrome.storage.local.set({gcmRegistrationId: registrationId});
1304 resolve(registrationId);
1305 } else {
1306 reject();
1314 * Polls the optin state.
1315 * Sometimes we get the response to the opted in result too soon during
1316 * push messaging. We'll recheck the optin state a few times before giving up.
1318 function pollOptedInWithRecheck() {
1320 * Cleans up any state used to recheck the opt-in poll.
1322 function clearPollingState() {
1323 localStorage.removeItem('optedInCheckCount');
1324 optInRecheckAttempts.stop();
1327 if (localStorage.optedInCheckCount === undefined) {
1328 localStorage.optedInCheckCount = 0;
1329 optInRecheckAttempts.start();
1332 console.log(new Date() +
1333 ' checkOptedIn Attempt ' + localStorage.optedInCheckCount);
1335 requestAndUpdateOptedIn().then(function(optedIn) {
1336 if (optedIn) {
1337 clearPollingState();
1338 return Promise.resolve();
1339 } else {
1340 // If we're not opted in, reject to retry.
1341 return Promise.reject();
1343 }).catch(function() {
1344 if (localStorage.optedInCheckCount < 5) {
1345 localStorage.optedInCheckCount++;
1346 optInRecheckAttempts.scheduleRetry();
1347 } else {
1348 clearPollingState();
1353 instrumented.runtime.onInstalled.addListener(function(details) {
1354 console.log('onInstalled ' + JSON.stringify(details));
1355 if (details.reason != 'chrome_update') {
1356 initialize();
1360 instrumented.runtime.onStartup.addListener(function() {
1361 console.log('onStartup');
1363 // Show notifications received by earlier polls. Doing this as early as
1364 // possible to reduce latency of showing first notifications. This mimics how
1365 // persistent notifications will work.
1366 tasks.add(SHOW_ON_START_TASK_NAME, function() {
1367 fillFromChromeLocalStorage({
1368 /** @type {Object<string, StoredNotificationGroup>} */
1369 notificationGroups: {}
1370 }).then(function(items) {
1371 console.log('onStartup-get ' + JSON.stringify(items));
1373 showNotificationGroups(items.notificationGroups).then(function() {
1374 chrome.storage.local.set(items);
1379 initialize();
1382 authenticationManager.addListener(function() {
1383 console.log('signIn State Change');
1384 onStateChange();
1387 instrumented.notifications.onClicked.addListener(
1388 function(chromeNotificationId) {
1389 chrome.metricsPrivate.recordUserAction('GoogleNow.MessageClicked');
1390 onNotificationClicked(chromeNotificationId,
1391 function(notificationDataEntry) {
1392 var actionUrls = notificationDataEntry.actionUrls;
1393 var url = actionUrls && actionUrls.messageUrl;
1394 if (url) {
1395 recordNotificationClick(notificationDataEntry.cardTypeId);
1397 return url;
1401 instrumented.notifications.onButtonClicked.addListener(
1402 function(chromeNotificationId, buttonIndex) {
1403 chrome.metricsPrivate.recordUserAction(
1404 'GoogleNow.ButtonClicked' + buttonIndex);
1405 onNotificationClicked(chromeNotificationId,
1406 function(notificationDataEntry) {
1407 var actionUrls = notificationDataEntry.actionUrls;
1408 var url = actionUrls.buttonUrls[buttonIndex];
1409 if (url) {
1410 recordButtonClick(notificationDataEntry.cardTypeId, buttonIndex);
1411 } else {
1412 verify(false, 'onButtonClicked: no url for a button');
1413 console.log(
1414 'buttonIndex=' + buttonIndex + ' ' +
1415 'chromeNotificationId=' + chromeNotificationId + ' ' +
1416 'notificationDataEntry=' +
1417 JSON.stringify(notificationDataEntry));
1419 return url;
1423 instrumented.notifications.onClosed.addListener(onNotificationClosed);
1425 instrumented.notifications.onPermissionLevelChanged.addListener(
1426 function(permissionLevel) {
1427 console.log('Notifications permissionLevel Change');
1428 onStateChange();
1431 instrumented.notifications.onShowSettings.addListener(function() {
1432 openUrl(SETTINGS_URL);
1435 // Handles state change notifications for the Google Now enabled bit.
1436 instrumented.storage.onChanged.addListener(function(changes, areaName) {
1437 if (areaName === 'local') {
1438 if ('googleNowEnabled' in changes) {
1439 onStateChange();
1444 instrumented.gcm.onMessage.addListener(function(message) {
1445 console.log('gcm.onMessage ' + JSON.stringify(message));
1446 if (!message || !message.data) {
1447 return;
1450 var payload = message.data.payload;
1451 var tag = message.data.tag;
1452 if (payload.indexOf('REQUEST_CARDS') == 0) {
1453 tasks.add(ON_PUSH_MESSAGE_START_TASK_NAME, function() {
1454 // Accept promise rejection on failure since it's safer to do nothing,
1455 // preventing polling the server when the payload really didn't change.
1456 fillFromChromeLocalStorage({
1457 lastPollNowPayloads: {},
1458 /** @type {Object<string, StoredNotificationGroup>} */
1459 notificationGroups: {}
1460 }, PromiseRejection.ALLOW).then(function(items) {
1461 if (items.lastPollNowPayloads[tag] != payload) {
1462 items.lastPollNowPayloads[tag] = payload;
1464 items.notificationGroups['PUSH' + tag] = {
1465 cards: [],
1466 nextPollTime: Date.now()
1469 chrome.storage.local.set({
1470 lastPollNowPayloads: items.lastPollNowPayloads,
1471 notificationGroups: items.notificationGroups
1474 pollOptedInWithRecheck();