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