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