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.
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. Obtaining the location of the machine;
14 * 2. Processing requests for cards dismissals that are not yet sent to the
16 * 3. Making a server request based on that location;
17 * 4. Showing the received cards as notifications.
20 // TODO(vadimt): Decide what to do in incognito mode.
21 // TODO(vadimt): Figure out the final values of the constants.
22 // TODO(vadimt): Remove 'console' calls.
25 * Standard response code for successful HTTP requests. This is the only success
26 * code the server will send.
29 var HTTP_NOCONTENT = 204;
31 var HTTP_BAD_REQUEST = 400;
32 var HTTP_UNAUTHORIZED = 401;
33 var HTTP_FORBIDDEN = 403;
34 var HTTP_METHOD_NOT_ALLOWED = 405;
36 var MS_IN_SECOND = 1000;
37 var MS_IN_MINUTE = 60 * 1000;
40 * Initial period for polling for Google Now Notifications cards to use when the
41 * period from the server is not available.
43 var INITIAL_POLLING_PERIOD_SECONDS = 5 * 60; // 5 minutes
46 * Mininal period for polling for Google Now Notifications cards.
48 var MINIMUM_POLLING_PERIOD_SECONDS = 5 * 60; // 5 minutes
51 * Maximal period for polling for Google Now Notifications cards to use when the
52 * period from the server is not available.
54 var MAXIMUM_POLLING_PERIOD_SECONDS = 60 * 60; // 1 hour
57 * Initial period for retrying the server request for dismissing cards.
59 var INITIAL_RETRY_DISMISS_PERIOD_SECONDS = 60; // 1 minute
62 * Maximum period for retrying the server request for dismissing cards.
64 var MAXIMUM_RETRY_DISMISS_PERIOD_SECONDS = 60 * 60; // 1 hour
67 * Time we keep retrying dismissals.
69 var MAXIMUM_DISMISSAL_AGE_MS = 24 * 60 * 60 * 1000; // 1 day
72 * Time we keep dismissals after successful server dismiss requests.
74 var DISMISS_RETENTION_TIME_MS = 20 * 60 * 1000; // 20 minutes
77 * Default period for checking whether the user is opted in to Google Now.
79 var DEFAULT_OPTIN_CHECK_PERIOD_SECONDS = 60 * 60 * 24 * 7; // 1 week
82 * URL to open when the user clicked on a link for the our notification
85 var SETTINGS_URL = 'https://support.google.com/chrome/?p=ib_google_now_welcome';
88 * Number of location cards that need an explanatory link.
90 var LOCATION_CARDS_LINK_THRESHOLD = 10;
93 * Names for tasks that can be created by the extension.
95 var UPDATE_CARDS_TASK_NAME = 'update-cards';
96 var DISMISS_CARD_TASK_NAME = 'dismiss-card';
97 var RETRY_DISMISS_TASK_NAME = 'retry-dismiss';
98 var STATE_CHANGED_TASK_NAME = 'state-changed';
99 var SHOW_ON_START_TASK_NAME = 'show-cards-on-start';
100 var ON_PUSH_MESSAGE_START_TASK_NAME = 'on-push-message';
102 var LOCATION_WATCH_NAME = 'location-watch';
105 * Group as received from the server.
108 * nextPollSeconds: (string|undefined),
109 * rank: (number|undefined),
110 * requested: (boolean|undefined)
116 * Server response with notifications and groups.
119 * googleNowDisabled: (boolean|undefined),
120 * groups: Object.<string, ReceivedGroup>,
121 * notifications: Array.<ReceivedNotification>
127 * Notification group as the client stores it. |cardsTimestamp| and |rank| are
128 * defined if |cards| is non-empty. |nextPollTime| is undefined if the server
129 * (1) never sent 'nextPollSeconds' for the group or
130 * (2) didn't send 'nextPollSeconds' with the last group update containing a
131 * cards update and all the times after that.
134 * cards: Array.<ReceivedNotification>,
135 * cardsTimestamp: (number|undefined),
136 * nextPollTime: (number|undefined),
137 * rank: (number|undefined)
140 var StoredNotificationGroup;
143 * Pending (not yet successfully sent) dismissal for a received notification.
144 * |time| is the moment when the user requested dismissal.
147 * chromeNotificationId: ChromeNotificationId,
149 * dismissalData: DismissalData
152 var PendingDismissal;
155 * Checks if a new task can't be scheduled when another task is already
157 * @param {string} newTaskName Name of the new task.
158 * @param {string} scheduledTaskName Name of the scheduled task.
159 * @return {boolean} Whether the new task conflicts with the existing task.
161 function areTasksConflicting(newTaskName, scheduledTaskName) {
162 if (newTaskName == UPDATE_CARDS_TASK_NAME &&
163 scheduledTaskName == UPDATE_CARDS_TASK_NAME) {
164 // If a card update is requested while an old update is still scheduled, we
165 // don't need the new update.
169 if (newTaskName == RETRY_DISMISS_TASK_NAME &&
170 (scheduledTaskName == UPDATE_CARDS_TASK_NAME ||
171 scheduledTaskName == DISMISS_CARD_TASK_NAME ||
172 scheduledTaskName == RETRY_DISMISS_TASK_NAME)) {
173 // No need to schedule retry-dismiss action if another action that tries to
174 // send dismissals is scheduled.
181 var tasks = buildTaskManager(areTasksConflicting);
183 // Add error processing to API calls.
184 wrapper.instrumentChromeApiFunction('location.onLocationUpdate.addListener', 0);
185 wrapper.instrumentChromeApiFunction('metricsPrivate.getVariationParams', 1);
186 wrapper.instrumentChromeApiFunction('notifications.clear', 1);
187 wrapper.instrumentChromeApiFunction('notifications.create', 2);
188 wrapper.instrumentChromeApiFunction('notifications.getPermissionLevel', 0);
189 wrapper.instrumentChromeApiFunction('notifications.update', 2);
190 wrapper.instrumentChromeApiFunction('notifications.getAll', 0);
191 wrapper.instrumentChromeApiFunction(
192 'notifications.onButtonClicked.addListener', 0);
193 wrapper.instrumentChromeApiFunction('notifications.onClicked.addListener', 0);
194 wrapper.instrumentChromeApiFunction('notifications.onClosed.addListener', 0);
195 wrapper.instrumentChromeApiFunction(
196 'notifications.onPermissionLevelChanged.addListener', 0);
197 wrapper.instrumentChromeApiFunction(
198 'notifications.onShowSettings.addListener', 0);
199 wrapper.instrumentChromeApiFunction(
200 'preferencesPrivate.googleGeolocationAccessEnabled.get',
202 wrapper.instrumentChromeApiFunction(
203 'preferencesPrivate.googleGeolocationAccessEnabled.onChange.addListener',
205 wrapper.instrumentChromeApiFunction('permissions.contains', 1);
206 wrapper.instrumentChromeApiFunction('pushMessaging.onMessage.addListener', 0);
207 wrapper.instrumentChromeApiFunction('runtime.onInstalled.addListener', 0);
208 wrapper.instrumentChromeApiFunction('runtime.onStartup.addListener', 0);
209 wrapper.instrumentChromeApiFunction('tabs.create', 1);
210 wrapper.instrumentChromeApiFunction('storage.local.get', 1);
212 var updateCardsAttempts = buildAttemptManager(
215 INITIAL_POLLING_PERIOD_SECONDS,
216 MAXIMUM_POLLING_PERIOD_SECONDS);
217 var dismissalAttempts = buildAttemptManager(
219 retryPendingDismissals,
220 INITIAL_RETRY_DISMISS_PERIOD_SECONDS,
221 MAXIMUM_RETRY_DISMISS_PERIOD_SECONDS);
222 var cardSet = buildCardSet();
224 var authenticationManager = buildAuthenticationManager();
227 * Google Now UMA event identifier.
230 var GoogleNowEvent = {
231 REQUEST_FOR_CARDS_TOTAL: 0,
232 REQUEST_FOR_CARDS_SUCCESS: 1,
233 CARDS_PARSE_SUCCESS: 2,
234 DISMISS_REQUEST_TOTAL: 3,
235 DISMISS_REQUEST_SUCCESS: 4,
239 DELETED_SHOW_WELCOME_TOAST: 8,
241 DELETED_USER_SUPPRESSED: 10,
242 EVENTS_TOTAL: 11 // EVENTS_TOTAL is not an event; all new events need to be
247 * Records a Google Now Event.
248 * @param {GoogleNowEvent} event Event identifier.
250 function recordEvent(event) {
251 var metricDescription = {
252 metricName: 'GoogleNow.Event',
253 type: 'histogram-linear',
255 max: GoogleNowEvent.EVENTS_TOTAL,
256 buckets: GoogleNowEvent.EVENTS_TOTAL + 1
259 chrome.metricsPrivate.recordValue(metricDescription, event);
263 * Adds authorization behavior to the request.
264 * @param {XMLHttpRequest} request Server request.
265 * @param {function(boolean)} callbackBoolean Completion callback with 'success'
268 function setAuthorization(request, callbackBoolean) {
269 authenticationManager.getAuthToken(function(token) {
271 callbackBoolean(false);
275 request.setRequestHeader('Authorization', 'Bearer ' + token);
277 // Instrument onloadend to remove stale auth tokens.
278 var originalOnLoadEnd = request.onloadend;
279 request.onloadend = wrapper.wrapCallback(function(event) {
280 if (request.status == HTTP_FORBIDDEN ||
281 request.status == HTTP_UNAUTHORIZED) {
282 authenticationManager.removeToken(token, function() {
283 originalOnLoadEnd(event);
286 originalOnLoadEnd(event);
290 callbackBoolean(true);
295 * Shows parsed and combined cards as notifications.
296 * @param {Object.<string, StoredNotificationGroup>} notificationGroups Map from
297 * group name to group information.
298 * @param {Object.<ChromeNotificationId, CombinedCard>} cards Map from
299 * chromeNotificationId to the combined card, containing cards to show.
300 * @param {function()} onSuccess Called on success.
301 * @param {function(ReceivedNotification)=} onCardShown Optional parameter
302 * called when each card is shown.
304 function showNotificationCards(
305 notificationGroups, cards, onSuccess, onCardShown) {
306 console.log('showNotificationCards ' + JSON.stringify(cards));
308 instrumented.notifications.getAll(function(notifications) {
309 console.log('showNotificationCards-getAll ' +
310 JSON.stringify(notifications));
311 notifications = notifications || {};
313 // Mark notifications that didn't receive an update as having received
315 for (var chromeNotificationId in notifications) {
316 cards[chromeNotificationId] = cards[chromeNotificationId] || [];
319 /** @type {Object.<string, NotificationDataEntry>} */
320 var notificationsData = {};
322 // Create/update/delete notifications.
323 for (var chromeNotificationId in cards) {
324 notificationsData[chromeNotificationId] = cardSet.update(
325 chromeNotificationId,
326 cards[chromeNotificationId],
330 chrome.storage.local.set({notificationsData: notificationsData});
336 * Removes all cards and card state on Google Now close down.
337 * For example, this occurs when the geolocation preference is unchecked in the
340 function removeAllCards() {
341 console.log('removeAllCards');
343 // TODO(robliao): Once Google Now clears its own checkbox in the
344 // notifications center and bug 260376 is fixed, the below clearing
345 // code is no longer necessary.
346 instrumented.notifications.getAll(function(notifications) {
347 notifications = notifications || {};
348 for (var chromeNotificationId in notifications) {
349 instrumented.notifications.clear(chromeNotificationId, function() {});
351 chrome.storage.local.remove(['notificationsData', 'notificationGroups']);
356 * Adds a card group into a set of combined cards.
357 * @param {Object.<ChromeNotificationId, CombinedCard>} combinedCards Map from
358 * chromeNotificationId to a combined card.
359 * This is an input/output parameter.
360 * @param {StoredNotificationGroup} storedGroup Group to combine into the
363 function combineGroup(combinedCards, storedGroup) {
364 for (var i = 0; i < storedGroup.cards.length; i++) {
365 /** @type {ReceivedNotification} */
366 var receivedNotification = storedGroup.cards[i];
368 /** @type {UncombinedNotification} */
369 var uncombinedNotification = {
370 receivedNotification: receivedNotification,
371 showTime: receivedNotification.trigger.showTimeSec &&
372 (storedGroup.cardsTimestamp +
373 receivedNotification.trigger.showTimeSec * MS_IN_SECOND),
374 hideTime: storedGroup.cardsTimestamp +
375 receivedNotification.trigger.hideTimeSec * MS_IN_SECOND
379 combinedCards[receivedNotification.chromeNotificationId] || [];
380 combinedCard.push(uncombinedNotification);
381 combinedCards[receivedNotification.chromeNotificationId] = combinedCard;
386 * Schedules next cards poll.
387 * @param {Object.<string, StoredNotificationGroup>} groups Map from group name
388 * to group information.
389 * @param {boolean} isOptedIn True if the user is opted in to Google Now.
391 function scheduleNextPoll(groups, isOptedIn) {
393 var nextPollTime = null;
395 for (var groupName in groups) {
396 var group = groups[groupName];
397 if (group.nextPollTime !== undefined) {
398 nextPollTime = nextPollTime == null ?
399 group.nextPollTime : Math.min(group.nextPollTime, nextPollTime);
403 // At least one of the groups must have nextPollTime.
404 verify(nextPollTime != null, 'scheduleNextPoll: nextPollTime is null');
406 var nextPollDelaySeconds = Math.max(
407 (nextPollTime - Date.now()) / MS_IN_SECOND,
408 MINIMUM_POLLING_PERIOD_SECONDS);
409 updateCardsAttempts.start(nextPollDelaySeconds);
411 instrumented.metricsPrivate.getVariationParams(
412 'GoogleNow', function(params) {
413 var optinPollPeriodSeconds =
414 parseInt(params && params.optinPollPeriodSeconds, 10) ||
415 DEFAULT_OPTIN_CHECK_PERIOD_SECONDS;
416 updateCardsAttempts.start(optinPollPeriodSeconds);
422 * Combines notification groups into a set of Chrome notifications and shows
424 * @param {Object.<string, StoredNotificationGroup>} notificationGroups Map from
425 * group name to group information.
426 * @param {function()} onSuccess Called on success.
427 * @param {function(ReceivedNotification)=} onCardShown Optional parameter
428 * called when each card is shown.
430 function combineAndShowNotificationCards(
431 notificationGroups, onSuccess, onCardShown) {
432 console.log('combineAndShowNotificationCards ' +
433 JSON.stringify(notificationGroups));
434 /** @type {Object.<ChromeNotificationId, CombinedCard>} */
435 var combinedCards = {};
437 for (var groupName in notificationGroups)
438 combineGroup(combinedCards, notificationGroups[groupName]);
440 showNotificationCards(
441 notificationGroups, combinedCards, onSuccess, onCardShown);
445 * Based on a response from the notification server, shows notifications and
446 * schedules next update.
447 * @param {ServerResponse} response Server response.
448 * @param {function(ReceivedNotification)=} onCardShown Optional parameter
449 * called when each card is shown.
451 function processServerResponse(response, onCardShown) {
452 console.log('processServerResponse ' + JSON.stringify(response));
454 if (response.googleNowDisabled) {
455 chrome.storage.local.set({googleNowEnabled: false});
456 // TODO(vadimt): Remove the line below once the server stops sending groups
457 // with 'googleNowDisabled' responses.
458 response.groups = {};
459 // Google Now was enabled; now it's disabled. This is a state change.
463 var receivedGroups = response.groups;
465 instrumented.storage.local.get(
466 ['notificationGroups', 'recentDismissals'],
469 'processServerResponse-get ' + JSON.stringify(items));
471 /** @type {Object.<string, StoredNotificationGroup>} */
472 items.notificationGroups = items.notificationGroups || {};
473 /** @type {Object.<NotificationId, number>} */
474 items.recentDismissals = items.recentDismissals || {};
476 // Build a set of non-expired recent dismissals. It will be used for
477 // client-side filtering of cards.
478 /** @type {Object.<NotificationId, number>} */
479 var updatedRecentDismissals = {};
480 var now = Date.now();
481 for (var notificationId in items.recentDismissals) {
482 var dismissalAge = now - items.recentDismissals[notificationId];
483 if (dismissalAge < DISMISS_RETENTION_TIME_MS) {
484 updatedRecentDismissals[notificationId] =
485 items.recentDismissals[notificationId];
489 // Populate groups with corresponding cards.
490 if (response.notifications) {
491 for (var i = 0; i < response.notifications.length; ++i) {
492 /** @type {ReceivedNotification} */
493 var card = response.notifications[i];
494 if (!(card.notificationId in updatedRecentDismissals)) {
495 var group = receivedGroups[card.groupName];
496 group.cards = group.cards || [];
497 group.cards.push(card);
502 // Build updated set of groups.
503 var updatedGroups = {};
505 for (var groupName in receivedGroups) {
506 var receivedGroup = receivedGroups[groupName];
507 var storedGroup = items.notificationGroups[groupName] || {
509 cardsTimestamp: undefined,
510 nextPollTime: undefined,
514 if (receivedGroup.requested)
515 receivedGroup.cards = receivedGroup.cards || [];
517 if (receivedGroup.cards) {
518 // If the group contains a cards update, all its fields will get new
520 storedGroup.cards = receivedGroup.cards;
521 storedGroup.cardsTimestamp = now;
522 storedGroup.rank = receivedGroup.rank;
523 storedGroup.nextPollTime = undefined;
524 // The code below assigns nextPollTime a defined value if
525 // nextPollSeconds is specified in the received group.
526 // If the group's cards are not updated, and nextPollSeconds is
527 // unspecified, this method doesn't change group's nextPollTime.
530 // 'nextPollSeconds' may be sent even for groups that don't contain
532 if (receivedGroup.nextPollSeconds !== undefined) {
533 storedGroup.nextPollTime =
534 now + receivedGroup.nextPollSeconds * MS_IN_SECOND;
537 updatedGroups[groupName] = storedGroup;
540 scheduleNextPoll(updatedGroups, !response.googleNowDisabled);
541 combineAndShowNotificationCards(
544 chrome.storage.local.set({
545 notificationGroups: updatedGroups,
546 recentDismissals: updatedRecentDismissals
548 recordEvent(GoogleNowEvent.CARDS_PARSE_SUCCESS);
555 * Update Location Cards Shown Count.
556 * @param {ReceivedNotification} receivedNotification Notification as it was
557 * received from the server.
559 function countLocationCard(receivedNotification) {
560 if (receivedNotification.locationBased) {
561 localStorage['locationCardsShown']++;
566 * Requests notification cards from the server for specified groups.
567 * @param {Array.<string>} groupNames Names of groups that need to be refreshed.
569 function requestNotificationGroups(groupNames) {
570 console.log('requestNotificationGroups from ' + NOTIFICATION_CARDS_URL +
571 ', groupNames=' + JSON.stringify(groupNames));
573 recordEvent(GoogleNowEvent.REQUEST_FOR_CARDS_TOTAL);
575 var requestParameters = '?timeZoneOffsetMs=' +
576 (-new Date().getTimezoneOffset() * MS_IN_MINUTE);
578 var cardShownCallback = undefined;
579 if (localStorage['locationCardsShown'] < LOCATION_CARDS_LINK_THRESHOLD) {
580 requestParameters += '&locationExplanation=true';
581 cardShownCallback = countLocationCard;
584 groupNames.forEach(function(groupName) {
585 requestParameters += ('&requestTypes=' + groupName);
588 console.log('requestNotificationGroups: request=' + requestParameters);
590 var request = buildServerRequest('GET', 'notifications' + requestParameters);
592 request.onloadend = function(event) {
593 console.log('requestNotificationGroups-onloadend ' + request.status);
594 if (request.status == HTTP_OK) {
595 recordEvent(GoogleNowEvent.REQUEST_FOR_CARDS_SUCCESS);
596 processServerResponse(
597 JSON.parse(request.responseText), cardShownCallback);
601 setAuthorization(request, function(success) {
608 * Requests the account opted-in state from the server.
609 * @param {function()} optedInCallback Function that will be called if
610 * opted-in state is 'true'.
612 function requestOptedIn(optedInCallback) {
613 console.log('requestOptedIn from ' + NOTIFICATION_CARDS_URL);
615 var request = buildServerRequest('GET', 'settings/optin');
617 request.onloadend = function(event) {
619 'requestOptedIn-onloadend ' + request.status + ' ' + request.response);
620 if (request.status == HTTP_OK) {
621 var parsedResponse = JSON.parse(request.responseText);
622 if (parsedResponse.value) {
623 chrome.storage.local.set({googleNowEnabled: true});
625 // Google Now was disabled, now it's enabled. This is a state change.
628 scheduleNextPoll({}, false);
633 setAuthorization(request, function(success) {
640 * Requests notification cards from the server.
641 * @param {Location=} position Location of this computer.
643 function requestNotificationCards(position) {
644 console.log('requestNotificationCards ' + JSON.stringify(position));
646 instrumented.storage.local.get(
647 ['notificationGroups', 'googleNowEnabled'], function(items) {
648 console.log('requestNotificationCards-storage-get ' +
649 JSON.stringify(items));
651 /** @type {Object.<string, StoredNotificationGroup>} */
652 items.notificationGroups = items.notificationGroups || {};
654 var groupsToRequest = [];
656 var now = Date.now();
658 for (var groupName in items.notificationGroups) {
659 var group = items.notificationGroups[groupName];
660 if (group.nextPollTime !== undefined && group.nextPollTime <= now)
661 groupsToRequest.push(groupName);
664 if (items.googleNowEnabled) {
665 requestNotificationGroups(groupsToRequest);
667 requestOptedIn(function() {
668 requestNotificationGroups(groupsToRequest);
675 * Starts getting location for a cards update.
677 function requestLocation() {
678 console.log('requestLocation');
679 recordEvent(GoogleNowEvent.LOCATION_REQUEST);
680 // TODO(vadimt): Figure out location request options.
681 instrumented.metricsPrivate.getVariationParams('GoogleNow', function(params) {
682 var minDistanceInMeters =
683 parseInt(params && params.minDistanceInMeters, 10) ||
685 var minTimeInMilliseconds =
686 parseInt(params && params.minTimeInMilliseconds, 10) ||
687 180000; // 3 minutes.
689 // TODO(vadimt): Uncomment/remove watchLocation and remove invoking
690 // updateNotificationsCards once state machine design is finalized.
691 // chrome.location.watchLocation(LOCATION_WATCH_NAME, {
692 // minDistanceInMeters: minDistanceInMeters,
693 // minTimeInMilliseconds: minTimeInMilliseconds
695 // We need setTimeout to avoid recursive task creation. This is a temporary
696 // code, and it will be removed once we finally decide to send or not send
697 // client location to the server.
698 setTimeout(wrapper.wrapCallback(updateNotificationsCards, true), 0);
703 * Stops getting the location.
705 function stopRequestLocation() {
706 console.log('stopRequestLocation');
707 chrome.location.clearWatch(LOCATION_WATCH_NAME);
711 * Obtains new location; requests and shows notification cards based on this
713 * @param {Location=} position Location of this computer.
715 function updateNotificationsCards(position) {
716 console.log('updateNotificationsCards ' + JSON.stringify(position) +
718 tasks.add(UPDATE_CARDS_TASK_NAME, function() {
719 console.log('updateNotificationsCards-task-begin');
720 updateCardsAttempts.isRunning(function(running) {
722 updateCardsAttempts.planForNext(function() {
723 processPendingDismissals(function(success) {
725 // The cards are requested only if there are no unsent dismissals.
726 requestNotificationCards(position);
736 * Sends a server request to dismiss a card.
737 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
739 * @param {number} dismissalTimeMs Time of the user's dismissal of the card in
740 * milliseconds since epoch.
741 * @param {DismissalData} dismissalData Data to build a dismissal request.
742 * @param {function(boolean)} callbackBoolean Completion callback with 'done'
745 function requestCardDismissal(
746 chromeNotificationId, dismissalTimeMs, dismissalData, callbackBoolean) {
747 console.log('requestDismissingCard ' + chromeNotificationId +
748 ' from ' + NOTIFICATION_CARDS_URL +
749 ', dismissalData=' + JSON.stringify(dismissalData));
751 var dismissalAge = Date.now() - dismissalTimeMs;
753 if (dismissalAge > MAXIMUM_DISMISSAL_AGE_MS) {
754 callbackBoolean(true);
758 recordEvent(GoogleNowEvent.DISMISS_REQUEST_TOTAL);
760 var requestParameters = 'notifications/' + dismissalData.notificationId +
761 '?age=' + dismissalAge +
762 '&chromeNotificationId=' + chromeNotificationId;
764 for (var paramField in dismissalData.parameters)
765 requestParameters += ('&' + paramField +
766 '=' + dismissalData.parameters[paramField]);
768 console.log('requestCardDismissal: requestParameters=' + requestParameters);
770 var request = buildServerRequest('DELETE', requestParameters);
771 request.onloadend = function(event) {
772 console.log('requestDismissingCard-onloadend ' + request.status);
773 if (request.status == HTTP_NOCONTENT)
774 recordEvent(GoogleNowEvent.DISMISS_REQUEST_SUCCESS);
776 // A dismissal doesn't require further retries if it was successful or
777 // doesn't have a chance for successful completion.
778 var done = request.status == HTTP_NOCONTENT ||
779 request.status == HTTP_BAD_REQUEST ||
780 request.status == HTTP_METHOD_NOT_ALLOWED;
781 callbackBoolean(done);
784 setAuthorization(request, function(success) {
788 callbackBoolean(false);
793 * Tries to send dismiss requests for all pending dismissals.
794 * @param {function(boolean)} callbackBoolean Completion callback with 'success'
795 * parameter. Success means that no pending dismissals are left.
797 function processPendingDismissals(callbackBoolean) {
798 instrumented.storage.local.get(['pendingDismissals', 'recentDismissals'],
800 console.log('processPendingDismissals-storage-get ' +
801 JSON.stringify(items));
803 /** @type {Array.<PendingDismissal>} */
804 items.pendingDismissals = items.pendingDismissals || [];
805 /** @type {Object.<NotificationId, number>} */
806 items.recentDismissals = items.recentDismissals || {};
808 var dismissalsChanged = false;
810 function onFinish(success) {
811 if (dismissalsChanged) {
812 chrome.storage.local.set({
813 pendingDismissals: items.pendingDismissals,
814 recentDismissals: items.recentDismissals
817 callbackBoolean(success);
820 function doProcessDismissals() {
821 if (items.pendingDismissals.length == 0) {
822 dismissalAttempts.stop();
827 // Send dismissal for the first card, and if successful, repeat
828 // recursively with the rest.
829 /** @type {PendingDismissal} */
830 var dismissal = items.pendingDismissals[0];
831 requestCardDismissal(
832 dismissal.chromeNotificationId,
834 dismissal.dismissalData,
837 dismissalsChanged = true;
838 items.pendingDismissals.splice(0, 1);
839 items.recentDismissals[
840 dismissal.dismissalData.notificationId] =
842 doProcessDismissals();
849 doProcessDismissals();
854 * Submits a task to send pending dismissals.
856 function retryPendingDismissals() {
857 tasks.add(RETRY_DISMISS_TASK_NAME, function() {
858 dismissalAttempts.planForNext(function() {
859 processPendingDismissals(function(success) {});
865 * Opens a URL in a new tab.
866 * @param {string} url URL to open.
868 function openUrl(url) {
869 instrumented.tabs.create({url: url}, function(tab) {
871 chrome.windows.update(tab.windowId, {focused: true});
873 chrome.windows.create({url: url, focused: true});
878 * Opens URL corresponding to the clicked part of the notification.
879 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
881 * @param {function((ActionUrls|undefined)): (string|undefined)} selector
882 * Function that extracts the url for the clicked area from the button
885 function onNotificationClicked(chromeNotificationId, selector) {
886 instrumented.storage.local.get('notificationsData', function(items) {
887 /** @type {(NotificationDataEntry|undefined)} */
888 var notificationData = items &&
889 items.notificationsData &&
890 items.notificationsData[chromeNotificationId];
892 if (!notificationData)
895 var url = selector(notificationData.actionUrls);
904 * Callback for chrome.notifications.onClosed event.
905 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
907 * @param {boolean} byUser Whether the notification was closed by the user.
909 function onNotificationClosed(chromeNotificationId, byUser) {
913 // At this point we are guaranteed that the notification is a now card.
914 chrome.metricsPrivate.recordUserAction('GoogleNow.Dismissed');
916 tasks.add(DISMISS_CARD_TASK_NAME, function() {
917 dismissalAttempts.start();
919 instrumented.storage.local.get(
920 ['pendingDismissals', 'notificationsData', 'notificationGroups'],
923 /** @type {Array.<PendingDismissal>} */
924 items.pendingDismissals = items.pendingDismissals || [];
925 /** @type {Object.<string, NotificationDataEntry>} */
926 items.notificationsData = items.notificationsData || {};
927 /** @type {Object.<string, StoredNotificationGroup>} */
928 items.notificationGroups = items.notificationGroups || {};
930 /** @type {NotificationDataEntry} */
931 var notificationData =
932 items.notificationsData[chromeNotificationId] ||
934 timestamp: Date.now(),
938 var dismissalResult =
940 chromeNotificationId,
942 items.notificationGroups);
944 for (var i = 0; i < dismissalResult.dismissals.length; i++) {
945 /** @type {PendingDismissal} */
947 chromeNotificationId: chromeNotificationId,
949 dismissalData: dismissalResult.dismissals[i]
951 items.pendingDismissals.push(dismissal);
954 items.notificationsData[chromeNotificationId] =
955 dismissalResult.notificationData;
957 chrome.storage.local.set(items);
959 processPendingDismissals(function(success) {});
965 * Initializes the polling system to start monitoring location and fetching
968 function startPollingCards() {
969 // Create an update timer for a case when for some reason location request
971 updateCardsAttempts.start(MAXIMUM_POLLING_PERIOD_SECONDS);
977 * Stops all machinery in the polling system.
979 function stopPollingCards() {
980 stopRequestLocation();
981 updateCardsAttempts.stop();
983 // Mark the Google Now as disabled to start with checking the opt-in state
984 // next time startPollingCards() is called.
985 chrome.storage.local.set({googleNowEnabled: false});
989 * Initializes the event page on install or on browser startup.
991 function initialize() {
992 recordEvent(GoogleNowEvent.EXTENSION_START);
997 * Starts or stops the polling of cards.
998 * @param {boolean} shouldPollCardsRequest true to start and
999 * false to stop polling cards.
1001 function setShouldPollCards(shouldPollCardsRequest) {
1002 updateCardsAttempts.isRunning(function(currentValue) {
1003 if (shouldPollCardsRequest != currentValue) {
1004 console.log('Action Taken setShouldPollCards=' + shouldPollCardsRequest);
1005 if (shouldPollCardsRequest)
1006 startPollingCards();
1011 'Action Ignored setShouldPollCards=' + shouldPollCardsRequest);
1017 * Enables or disables the Google Now background permission.
1018 * @param {boolean} backgroundEnable true to run in the background.
1019 * false to not run in the background.
1021 function setBackgroundEnable(backgroundEnable) {
1022 instrumented.permissions.contains({permissions: ['background']},
1023 function(hasPermission) {
1024 if (backgroundEnable != hasPermission) {
1025 console.log('Action Taken setBackgroundEnable=' + backgroundEnable);
1026 if (backgroundEnable)
1027 chrome.permissions.request({permissions: ['background']});
1029 chrome.permissions.remove({permissions: ['background']});
1031 console.log('Action Ignored setBackgroundEnable=' + backgroundEnable);
1037 * Does the actual work of deciding what Google Now should do
1038 * based off of the current state of Chrome.
1039 * @param {boolean} signedIn true if the user is signed in.
1040 * @param {boolean} geolocationEnabled true if
1041 * the geolocation option is enabled.
1042 * @param {boolean} canEnableBackground true if
1043 * the background permission can be requested.
1044 * @param {boolean} notificationEnabled true if
1045 * Google Now for Chrome is allowed to show notifications.
1046 * @param {boolean} googleNowEnabled true if
1047 * the Google Now is enabled for the user.
1049 function updateRunningState(
1052 canEnableBackground,
1053 notificationEnabled,
1056 'State Update signedIn=' + signedIn + ' ' +
1057 'geolocationEnabled=' + geolocationEnabled + ' ' +
1058 'canEnableBackground=' + canEnableBackground + ' ' +
1059 'notificationEnabled=' + notificationEnabled + ' ' +
1060 'googleNowEnabled=' + googleNowEnabled);
1062 // TODO(vadimt): Remove this line once state machine design is finalized.
1063 geolocationEnabled = true;
1065 var shouldPollCards = false;
1066 var shouldSetBackground = false;
1068 if (signedIn && notificationEnabled) {
1069 if (geolocationEnabled) {
1070 if (canEnableBackground && googleNowEnabled)
1071 shouldSetBackground = true;
1073 shouldPollCards = true;
1076 recordEvent(GoogleNowEvent.STOPPED);
1080 'Requested Actions shouldSetBackground=' + shouldSetBackground + ' ' +
1081 'setShouldPollCards=' + shouldPollCards);
1083 setBackgroundEnable(shouldSetBackground);
1084 setShouldPollCards(shouldPollCards);
1088 * Coordinates the behavior of Google Now for Chrome depending on
1089 * Chrome and extension state.
1091 function onStateChange() {
1092 tasks.add(STATE_CHANGED_TASK_NAME, function() {
1093 authenticationManager.isSignedIn(function(signedIn) {
1094 instrumented.metricsPrivate.getVariationParams(
1096 function(response) {
1097 var canEnableBackground =
1098 (!response || (response.canEnableBackground != 'false'));
1099 instrumented.notifications.getPermissionLevel(function(level) {
1100 var notificationEnabled = (level == 'granted');
1103 googleGeolocationAccessEnabled.
1104 get({}, function(prefValue) {
1105 var geolocationEnabled = !!prefValue.value;
1106 instrumented.storage.local.get(
1109 var googleNowEnabled =
1110 items && !!items.googleNowEnabled;
1114 canEnableBackground,
1115 notificationEnabled,
1125 instrumented.runtime.onInstalled.addListener(function(details) {
1126 console.log('onInstalled ' + JSON.stringify(details));
1127 if (details.reason != 'chrome_update') {
1132 instrumented.runtime.onStartup.addListener(function() {
1133 console.log('onStartup');
1135 // Show notifications received by earlier polls. Doing this as early as
1136 // possible to reduce latency of showing first notifications. This mimics how
1137 // persistent notifications will work.
1138 tasks.add(SHOW_ON_START_TASK_NAME, function() {
1139 instrumented.storage.local.get('notificationGroups', function(items) {
1140 console.log('onStartup-get ' + JSON.stringify(items));
1141 items = items || {};
1142 /** @type {Object.<string, StoredNotificationGroup>} */
1143 items.notificationGroups = items.notificationGroups || {};
1145 combineAndShowNotificationCards(items.notificationGroups, function() {
1146 chrome.storage.local.set(items);
1156 googleGeolocationAccessEnabled.
1158 addListener(function(prefValue) {
1159 console.log('googleGeolocationAccessEnabled Pref onChange ' +
1164 authenticationManager.addListener(function() {
1165 console.log('signIn State Change');
1169 instrumented.notifications.onClicked.addListener(
1170 function(chromeNotificationId) {
1171 chrome.metricsPrivate.recordUserAction('GoogleNow.MessageClicked');
1172 onNotificationClicked(chromeNotificationId, function(actionUrls) {
1173 return actionUrls && actionUrls.messageUrl;
1177 instrumented.notifications.onButtonClicked.addListener(
1178 function(chromeNotificationId, buttonIndex) {
1179 chrome.metricsPrivate.recordUserAction(
1180 'GoogleNow.ButtonClicked' + buttonIndex);
1181 onNotificationClicked(chromeNotificationId, function(actionUrls) {
1182 var url = actionUrls.buttonUrls[buttonIndex];
1183 verify(url !== undefined, 'onButtonClicked: no url for a button');
1188 instrumented.notifications.onClosed.addListener(onNotificationClosed);
1190 instrumented.notifications.onPermissionLevelChanged.addListener(
1191 function(permissionLevel) {
1192 console.log('Notifications permissionLevel Change');
1196 instrumented.notifications.onShowSettings.addListener(function() {
1197 openUrl(SETTINGS_URL);
1200 instrumented.location.onLocationUpdate.addListener(function(position) {
1201 recordEvent(GoogleNowEvent.LOCATION_UPDATE);
1202 updateNotificationsCards(position);
1205 instrumented.pushMessaging.onMessage.addListener(function(message) {
1206 // message.payload will be '' when the extension first starts.
1207 // Each time after signing in, we'll get latest payload for all channels.
1208 // So, we need to poll the server only when the payload is non-empty and has
1210 console.log('pushMessaging.onMessage ' + JSON.stringify(message));
1211 if (message.payload.indexOf('REQUEST_CARDS') == 0) {
1212 tasks.add(ON_PUSH_MESSAGE_START_TASK_NAME, function() {
1213 instrumented.storage.local.get(
1214 ['lastPollNowPayloads', 'notificationGroups'], function(items) {
1215 // If storage.get fails, it's safer to do nothing, preventing polling
1216 // the server when the payload really didn't change.
1220 // If this is the first time we get lastPollNowPayloads, initialize it.
1221 items.lastPollNowPayloads = items.lastPollNowPayloads || {};
1223 if (items.lastPollNowPayloads[message.subchannelId] !=
1225 items.lastPollNowPayloads[message.subchannelId] = message.payload;
1227 /** @type {Object.<string, StoredNotificationGroup>} */
1228 items.notificationGroups = items.notificationGroups || {};
1229 items.notificationGroups['PUSH' + message.subchannelId] = {
1231 nextPollTime: Date.now()
1234 chrome.storage.local.set({
1235 lastPollNowPayloads: items.lastPollNowPayloads,
1236 notificationGroups: items.notificationGroups
1239 updateNotificationsCards();