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 * Notification as it's sent by the server.
108 * notificationId: string,
109 * chromeNotificationId: string,
112 * chromeNotificationOptions: Object,
113 * actionUrls: Object=,
117 var UnmergedNotification;
120 * Notification group as the client stores it. |cardsTimestamp| and |rank| are
121 * defined if |cards| is non-empty. |nextPollTime| is undefined if the server
122 * (1) never sent 'nextPollSeconds' for the group or
123 * (2) didn't send 'nextPollSeconds' with the last group update containing a
124 * cards update and all the times after that.
127 * cards: Array.<UnmergedNotification>,
128 * cardsTimestamp: number=,
129 * nextPollTime: number=,
136 * Checks if a new task can't be scheduled when another task is already
138 * @param {string} newTaskName Name of the new task.
139 * @param {string} scheduledTaskName Name of the scheduled task.
140 * @return {boolean} Whether the new task conflicts with the existing task.
142 function areTasksConflicting(newTaskName, scheduledTaskName) {
143 if (newTaskName == UPDATE_CARDS_TASK_NAME &&
144 scheduledTaskName == UPDATE_CARDS_TASK_NAME) {
145 // If a card update is requested while an old update is still scheduled, we
146 // don't need the new update.
150 if (newTaskName == RETRY_DISMISS_TASK_NAME &&
151 (scheduledTaskName == UPDATE_CARDS_TASK_NAME ||
152 scheduledTaskName == DISMISS_CARD_TASK_NAME ||
153 scheduledTaskName == RETRY_DISMISS_TASK_NAME)) {
154 // No need to schedule retry-dismiss action if another action that tries to
155 // send dismissals is scheduled.
162 var tasks = buildTaskManager(areTasksConflicting);
164 // Add error processing to API calls.
165 wrapper.instrumentChromeApiFunction('location.onLocationUpdate.addListener', 0);
166 wrapper.instrumentChromeApiFunction('metricsPrivate.getVariationParams', 1);
167 wrapper.instrumentChromeApiFunction('notifications.clear', 1);
168 wrapper.instrumentChromeApiFunction('notifications.create', 2);
169 wrapper.instrumentChromeApiFunction('notifications.getPermissionLevel', 0);
170 wrapper.instrumentChromeApiFunction('notifications.update', 2);
171 wrapper.instrumentChromeApiFunction('notifications.getAll', 0);
172 wrapper.instrumentChromeApiFunction(
173 'notifications.onButtonClicked.addListener', 0);
174 wrapper.instrumentChromeApiFunction('notifications.onClicked.addListener', 0);
175 wrapper.instrumentChromeApiFunction('notifications.onClosed.addListener', 0);
176 wrapper.instrumentChromeApiFunction(
177 'notifications.onPermissionLevelChanged.addListener', 0);
178 wrapper.instrumentChromeApiFunction(
179 'notifications.onShowSettings.addListener', 0);
180 wrapper.instrumentChromeApiFunction(
181 'preferencesPrivate.googleGeolocationAccessEnabled.get',
183 wrapper.instrumentChromeApiFunction(
184 'preferencesPrivate.googleGeolocationAccessEnabled.onChange.addListener',
186 wrapper.instrumentChromeApiFunction('permissions.contains', 1);
187 wrapper.instrumentChromeApiFunction('pushMessaging.onMessage.addListener', 0);
188 wrapper.instrumentChromeApiFunction('runtime.onInstalled.addListener', 0);
189 wrapper.instrumentChromeApiFunction('runtime.onStartup.addListener', 0);
190 wrapper.instrumentChromeApiFunction('tabs.create', 1);
191 wrapper.instrumentChromeApiFunction('storage.local.get', 1);
193 var updateCardsAttempts = buildAttemptManager(
196 INITIAL_POLLING_PERIOD_SECONDS,
197 MAXIMUM_POLLING_PERIOD_SECONDS);
198 var dismissalAttempts = buildAttemptManager(
200 retryPendingDismissals,
201 INITIAL_RETRY_DISMISS_PERIOD_SECONDS,
202 MAXIMUM_RETRY_DISMISS_PERIOD_SECONDS);
203 var cardSet = buildCardSet();
205 var authenticationManager = buildAuthenticationManager();
208 * Google Now UMA event identifier.
211 var GoogleNowEvent = {
212 REQUEST_FOR_CARDS_TOTAL: 0,
213 REQUEST_FOR_CARDS_SUCCESS: 1,
214 CARDS_PARSE_SUCCESS: 2,
215 DISMISS_REQUEST_TOTAL: 3,
216 DISMISS_REQUEST_SUCCESS: 4,
220 DELETED_SHOW_WELCOME_TOAST: 8,
222 DELETED_USER_SUPPRESSED: 10,
223 EVENTS_TOTAL: 11 // EVENTS_TOTAL is not an event; all new events need to be
228 * Records a Google Now Event.
229 * @param {GoogleNowEvent} event Event identifier.
231 function recordEvent(event) {
232 var metricDescription = {
233 metricName: 'GoogleNow.Event',
234 type: 'histogram-linear',
236 max: GoogleNowEvent.EVENTS_TOTAL,
237 buckets: GoogleNowEvent.EVENTS_TOTAL + 1
240 chrome.metricsPrivate.recordValue(metricDescription, event);
244 * Adds authorization behavior to the request.
245 * @param {XMLHttpRequest} request Server request.
246 * @param {function(boolean)} callbackBoolean Completion callback with 'success'
249 function setAuthorization(request, callbackBoolean) {
250 authenticationManager.getAuthToken(function(token) {
252 callbackBoolean(false);
256 request.setRequestHeader('Authorization', 'Bearer ' + token);
258 // Instrument onloadend to remove stale auth tokens.
259 var originalOnLoadEnd = request.onloadend;
260 request.onloadend = wrapper.wrapCallback(function(event) {
261 if (request.status == HTTP_FORBIDDEN ||
262 request.status == HTTP_UNAUTHORIZED) {
263 authenticationManager.removeToken(token, function() {
264 originalOnLoadEnd(event);
267 originalOnLoadEnd(event);
271 callbackBoolean(true);
276 * Shows parsed and merged cards as notifications.
277 * @param {Object.<string, MergedCard>} cards Map from chromeNotificationId to
278 * the merged card, containing cards to show.
279 * @param {function(CardCreateInfo)=} onCardShown Optional parameter called when
280 * each card is shown.
282 function showNotificationCards(cards, onCardShown) {
283 console.log('showNotificationCards ' + JSON.stringify(cards));
285 instrumented.storage.local.get(['notificationsData', 'recentDismissals'],
287 console.log('showNotificationCards-get ' +
288 JSON.stringify(items));
290 items.notificationsData = items.notificationsData || {};
291 items.recentDismissals = items.recentDismissals || {};
293 instrumented.notifications.getAll(function(notifications) {
294 console.log('showNotificationCards-getAll ' +
295 JSON.stringify(notifications));
296 notifications = notifications || {};
298 // Build a set of non-expired recent dismissals. It will be used for
299 // client-side filtering of cards.
300 var updatedRecentDismissals = {};
301 var currentTimeMs = Date.now();
302 for (var chromeNotificationId in items.recentDismissals) {
303 if (currentTimeMs - items.recentDismissals[chromeNotificationId] <
304 DISMISS_RETENTION_TIME_MS) {
305 updatedRecentDismissals[chromeNotificationId] =
306 items.recentDismissals[chromeNotificationId];
307 delete cards[chromeNotificationId];
311 // Delete notifications that didn't receive an update.
312 for (var chromeNotificationId in notifications) {
313 console.log('showNotificationCards-delete-check ' +
314 chromeNotificationId);
315 if (!(chromeNotificationId in cards)) {
317 'showNotificationCards-delete ' + chromeNotificationId);
318 cardSet.clear(chromeNotificationId, false);
322 // Create/update notifications and store their new properties.
323 var newNotificationsData = {};
324 for (var chromeNotificationId in cards) {
325 var notificationData =
326 items.notificationsData[chromeNotificationId];
327 var previousVersion = notifications[chromeNotificationId] &&
329 notificationData.cardCreateInfo &&
330 notificationData.cardCreateInfo.version;
331 newNotificationsData[chromeNotificationId] = cardSet.update(
332 chromeNotificationId,
333 cards[chromeNotificationId],
338 chrome.storage.local.set({
339 notificationsData: newNotificationsData,
340 recentDismissals: updatedRecentDismissals
347 * Removes all cards and card state on Google Now close down.
348 * For example, this occurs when the geolocation preference is unchecked in the
351 function removeAllCards() {
352 console.log('removeAllCards');
354 // TODO(robliao): Once Google Now clears its own checkbox in the
355 // notifications center and bug 260376 is fixed, the below clearing
356 // code is no longer necessary.
357 instrumented.notifications.getAll(function(notifications) {
358 notifications = notifications || {};
359 for (var chromeNotificationId in notifications) {
360 instrumented.notifications.clear(chromeNotificationId, function() {});
362 chrome.storage.local.remove(['notificationsData', 'notificationGroups']);
367 * Merges an unmerged notification into a merged card with same ID.
368 * @param {MergedCard=} mergedCard Existing merged card or undefined if a merged
369 * card doesn't exist (i.e. we see this ID for the first time while
371 * @param {UnmergedNotification} unmergedNotification Notification as it was
372 * received from the server.
373 * @param {number} notificationTimestamp The moment the unmerged card was
375 * @param {number} notificationGroupRank Rank of the group of the unmerged card.
376 * @return {MergedCard} Result of merging |unmergedNotification| into
381 unmergedNotification,
382 notificationTimestamp,
383 notificationGroupRank) {
384 var result = mergedCard || {dismissals: []};
386 var priority = mergedCard ?
388 mergedCard.notification.priority,
389 unmergedNotification.chromeNotificationOptions.priority) :
390 unmergedNotification.chromeNotificationOptions.priority;
392 if (!mergedCard || notificationGroupRank > mergedCard.groupRank) {
393 result.groupRank = notificationGroupRank;
394 var showTime = unmergedNotification.trigger &&
395 unmergedNotification.trigger.showTimeSec &&
396 (notificationTimestamp +
397 unmergedNotification.trigger.showTimeSec * MS_IN_SECOND);
398 var hideTime = unmergedNotification.trigger &&
399 unmergedNotification.trigger.hideTimeSec &&
400 (notificationTimestamp +
401 unmergedNotification.trigger.hideTimeSec * MS_IN_SECOND);
408 if (!mergedCard || notificationTimestamp > mergedCard.timestamp) {
409 result.timestamp = notificationTimestamp;
410 result.notification = unmergedNotification.chromeNotificationOptions;
411 result.actionUrls = unmergedNotification.actionUrls;
412 result.version = unmergedNotification.version;
415 result.locationBased =
416 result.locationBased || unmergedNotification.locationBased;
418 result.notification.priority = priority;
419 var dismissalData = {
420 notificationId: unmergedNotification.notificationId,
421 parameters: unmergedNotification.dismissal
423 result.dismissals.push(dismissalData);
429 * Merges a card group into a set of merged cards.
430 * @param {Object.<string, MergedCard>} mergedCards Map from
431 * chromeNotificationId to a merged card.
432 * This is an input/output parameter.
433 * @param {StorageGroup} storageGroup Group to merge into the merged card set.
435 function mergeGroup(mergedCards, storageGroup) {
436 for (var i = 0; i < storageGroup.cards.length; i++) {
437 var card = storageGroup.cards[i];
438 mergedCards[card.chromeNotificationId] = mergeCards(
439 mergedCards[card.chromeNotificationId],
441 storageGroup.cardsTimestamp,
447 * Schedules next cards poll.
448 * @param {Object.<string, StorageGroup>} groups Map from group name to group
450 * @param {boolean} isOptedIn True if the user is opted in to Google Now.
452 function scheduleNextPoll(groups, isOptedIn) {
454 var nextPollTime = null;
456 for (var groupName in groups) {
457 var group = groups[groupName];
458 if (group.nextPollTime !== undefined) {
459 nextPollTime = nextPollTime == null ?
460 group.nextPollTime : Math.min(group.nextPollTime, nextPollTime);
464 // At least one of the groups must have nextPollTime.
465 verify(nextPollTime != null, 'scheduleNextPoll: nextPollTime is null');
467 var nextPollDelaySeconds = Math.max(
468 (nextPollTime - Date.now()) / MS_IN_SECOND,
469 MINIMUM_POLLING_PERIOD_SECONDS);
470 updateCardsAttempts.start(nextPollDelaySeconds);
472 instrumented.metricsPrivate.getVariationParams(
473 'GoogleNow', function(params) {
474 var optinPollPeriodSeconds =
475 parseInt(params && params.optinPollPeriodSeconds, 10) ||
476 DEFAULT_OPTIN_CHECK_PERIOD_SECONDS;
477 updateCardsAttempts.start(optinPollPeriodSeconds);
483 * Merges notification groups into a set of Chrome notifications and shows them.
484 * @param {Object.<string, StorageGroup>} notificationGroups Map from group name
485 * to group information.
486 * @param {function(CardCreateInfo)=} onCardShown Optional parameter called when
487 * each card is shown.
489 function mergeAndShowNotificationCards(notificationGroups, onCardShown) {
490 var mergedCards = {};
492 for (var groupName in notificationGroups)
493 mergeGroup(mergedCards, notificationGroups[groupName]);
495 showNotificationCards(mergedCards, onCardShown);
499 * Parses JSON response from the notification server, shows notifications and
500 * schedules next update.
501 * @param {string} response Server response.
502 * @param {function(CardCreateInfo)=} onCardShown Optional parameter called when
503 * each card is shown.
505 function parseAndShowNotificationCards(response, onCardShown) {
506 console.log('parseAndShowNotificationCards ' + response);
507 var parsedResponse = JSON.parse(response);
509 if (parsedResponse.googleNowDisabled) {
510 chrome.storage.local.set({googleNowEnabled: false});
511 // TODO(vadimt): Remove the line below once the server stops sending groups
512 // with 'googleNowDisabled' responses.
513 parsedResponse.groups = {};
514 // Google Now was enabled; now it's disabled. This is a state change.
518 var receivedGroups = parsedResponse.groups;
520 // Populate groups with corresponding cards.
521 if (parsedResponse.notifications) {
522 for (var i = 0; i != parsedResponse.notifications.length; ++i) {
523 var card = parsedResponse.notifications[i];
524 var group = receivedGroups[card.groupName];
525 group.cards = group.cards || [];
526 group.cards.push(card);
530 instrumented.storage.local.get('notificationGroups', function(items) {
531 console.log('parseAndShowNotificationCards-get ' + JSON.stringify(items));
533 items.notificationGroups = items.notificationGroups || {};
535 var now = Date.now();
537 // Build updated set of groups.
538 var updatedGroups = {};
540 for (var groupName in receivedGroups) {
541 var receivedGroup = receivedGroups[groupName];
542 var storageGroup = items.notificationGroups[groupName] || {
544 cardsTimestamp: undefined,
545 nextPollTime: undefined,
549 if (receivedGroup.requested)
550 receivedGroup.cards = receivedGroup.cards || [];
552 if (receivedGroup.cards) {
553 // If the group contains a cards update, all its fields will get new
555 storageGroup.cards = receivedGroup.cards;
556 storageGroup.cardsTimestamp = now;
557 storageGroup.rank = receivedGroup.rank;
558 storageGroup.nextPollTime = undefined;
559 // The code below assigns nextPollTime a defined value if
560 // nextPollSeconds is specified in the received group.
561 // If the group's cards are not updated, and nextPollSeconds is
562 // unspecified, this method doesn't change group's nextPollTime.
565 // 'nextPollSeconds' may be sent even for groups that don't contain cards
567 if (receivedGroup.nextPollSeconds !== undefined) {
568 storageGroup.nextPollTime =
569 now + receivedGroup.nextPollSeconds * MS_IN_SECOND;
572 updatedGroups[groupName] = storageGroup;
575 scheduleNextPoll(updatedGroups, !parsedResponse.googleNowDisabled);
576 chrome.storage.local.set({notificationGroups: updatedGroups});
577 mergeAndShowNotificationCards(updatedGroups, onCardShown);
578 recordEvent(GoogleNowEvent.CARDS_PARSE_SUCCESS);
583 * Update Location Cards Shown Count.
584 * @param {Object} cardCreateInfo Card Create Info
586 function countLocationCard(cardCreateInfo) {
587 if (cardCreateInfo.locationBased) {
588 localStorage['locationCardsShown']++;
593 * Requests notification cards from the server for specified groups.
594 * @param {Array.<string>} groupNames Names of groups that need to be refreshed.
596 function requestNotificationGroups(groupNames) {
597 console.log('requestNotificationGroups from ' + NOTIFICATION_CARDS_URL +
598 ', groupNames=' + JSON.stringify(groupNames));
600 recordEvent(GoogleNowEvent.REQUEST_FOR_CARDS_TOTAL);
602 var requestParameters = '?timeZoneOffsetMs=' +
603 (-new Date().getTimezoneOffset() * MS_IN_MINUTE);
605 var cardShownCallback = undefined;
606 if (localStorage['locationCardsShown'] < LOCATION_CARDS_LINK_THRESHOLD) {
607 requestParameters += '&locationExplanation=true';
608 cardShownCallback = countLocationCard;
611 groupNames.forEach(function(groupName) {
612 requestParameters += ('&requestTypes=' + groupName);
615 console.log('requestNotificationGroups: request=' + requestParameters);
617 var request = buildServerRequest('GET', 'notifications' + requestParameters);
619 request.onloadend = function(event) {
620 console.log('requestNotificationGroups-onloadend ' + request.status);
621 if (request.status == HTTP_OK) {
622 recordEvent(GoogleNowEvent.REQUEST_FOR_CARDS_SUCCESS);
623 parseAndShowNotificationCards(request.response, cardShownCallback);
627 setAuthorization(request, function(success) {
634 * Requests the account opted-in state from the server.
635 * @param {function()} optedInCallback Function that will be called if
636 * opted-in state is 'true'.
638 function requestOptedIn(optedInCallback) {
639 console.log('requestOptedIn from ' + NOTIFICATION_CARDS_URL);
641 var request = buildServerRequest('GET', 'settings/optin');
643 request.onloadend = function(event) {
645 'requestOptedIn-onloadend ' + request.status + ' ' + request.response);
646 if (request.status == HTTP_OK) {
647 var parsedResponse = JSON.parse(request.response);
648 if (parsedResponse.value) {
649 chrome.storage.local.set({googleNowEnabled: true});
651 // Google Now was disabled, now it's enabled. This is a state change.
654 scheduleNextPoll({}, false);
659 setAuthorization(request, function(success) {
666 * Requests notification cards from the server.
667 * @param {Location} position Location of this computer.
669 function requestNotificationCards(position) {
670 console.log('requestNotificationCards ' + JSON.stringify(position));
672 instrumented.storage.local.get(
673 ['notificationGroups', 'googleNowEnabled'], function(items) {
674 console.log('requestNotificationCards-storage-get ' +
675 JSON.stringify(items));
678 var groupsToRequest = [];
680 if (items.notificationGroups) {
681 var now = Date.now();
683 for (var groupName in items.notificationGroups) {
684 var group = items.notificationGroups[groupName];
685 if (group.nextPollTime !== undefined && group.nextPollTime <= now)
686 groupsToRequest.push(groupName);
690 if (items.googleNowEnabled) {
691 requestNotificationGroups(groupsToRequest);
693 requestOptedIn(function() {
694 requestNotificationGroups(groupsToRequest);
701 * Starts getting location for a cards update.
703 function requestLocation() {
704 console.log('requestLocation');
705 recordEvent(GoogleNowEvent.LOCATION_REQUEST);
706 // TODO(vadimt): Figure out location request options.
707 instrumented.metricsPrivate.getVariationParams('GoogleNow', function(params) {
708 var minDistanceInMeters =
709 parseInt(params && params.minDistanceInMeters, 10) ||
711 var minTimeInMilliseconds =
712 parseInt(params && params.minTimeInMilliseconds, 10) ||
713 180000; // 3 minutes.
715 // TODO(vadimt): Uncomment/remove watchLocation and remove invoking
716 // updateNotificationsCards once state machine design is finalized.
717 // chrome.location.watchLocation(LOCATION_WATCH_NAME, {
718 // minDistanceInMeters: minDistanceInMeters,
719 // minTimeInMilliseconds: minTimeInMilliseconds
721 // We need setTimeout to avoid recursive task creation. This is a temporary
722 // code, and it will be removed once we finally decide to send or not send
723 // client location to the server.
724 setTimeout(wrapper.wrapCallback(updateNotificationsCards, true), 0);
729 * Stops getting the location.
731 function stopRequestLocation() {
732 console.log('stopRequestLocation');
733 chrome.location.clearWatch(LOCATION_WATCH_NAME);
737 * Obtains new location; requests and shows notification cards based on this
739 * @param {Location} position Location of this computer.
741 function updateNotificationsCards(position) {
742 console.log('updateNotificationsCards ' + JSON.stringify(position) +
744 tasks.add(UPDATE_CARDS_TASK_NAME, function() {
745 console.log('updateNotificationsCards-task-begin');
746 updateCardsAttempts.isRunning(function(running) {
748 updateCardsAttempts.planForNext(function() {
749 processPendingDismissals(function(success) {
751 // The cards are requested only if there are no unsent dismissals.
752 requestNotificationCards(position);
762 * Sends a server request to dismiss a card.
763 * @param {string} chromeNotificationId chrome.notifications ID of 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 * @param {function(boolean)} callbackBoolean Completion callback with 'done'
770 function requestCardDismissal(
771 chromeNotificationId, dismissalTimeMs, dismissalData, callbackBoolean) {
772 console.log('requestDismissingCard ' + chromeNotificationId + ' from ' +
773 NOTIFICATION_CARDS_URL);
775 var dismissalAge = Date.now() - dismissalTimeMs;
777 if (dismissalAge > MAXIMUM_DISMISSAL_AGE_MS) {
778 callbackBoolean(true);
782 recordEvent(GoogleNowEvent.DISMISS_REQUEST_TOTAL);
784 var request = 'notifications/' + dismissalData.notificationId +
785 '?age=' + dismissalAge +
786 '&chromeNotificationId=' + chromeNotificationId;
788 for (var paramField in dismissalData.parameters)
789 request += ('&' + paramField + '=' + dismissalData.parameters[paramField]);
791 console.log('requestCardDismissal: request=' + request);
793 var request = buildServerRequest('DELETE', request);
794 request.onloadend = function(event) {
795 console.log('requestDismissingCard-onloadend ' + request.status);
796 if (request.status == HTTP_NOCONTENT)
797 recordEvent(GoogleNowEvent.DISMISS_REQUEST_SUCCESS);
799 // A dismissal doesn't require further retries if it was successful or
800 // doesn't have a chance for successful completion.
801 var done = request.status == HTTP_NOCONTENT ||
802 request.status == HTTP_BAD_REQUEST ||
803 request.status == HTTP_METHOD_NOT_ALLOWED;
804 callbackBoolean(done);
807 setAuthorization(request, function(success) {
811 callbackBoolean(false);
816 * Tries to send dismiss requests for all pending dismissals.
817 * @param {function(boolean)} callbackBoolean Completion callback with 'success'
818 * parameter. Success means that no pending dismissals are left.
820 function processPendingDismissals(callbackBoolean) {
821 instrumented.storage.local.get(['pendingDismissals', 'recentDismissals'],
823 console.log('processPendingDismissals-storage-get ' +
824 JSON.stringify(items));
826 items.pendingDismissals = items.pendingDismissals || [];
827 items.recentDismissals = items.recentDismissals || {};
829 var dismissalsChanged = false;
831 function onFinish(success) {
832 if (dismissalsChanged) {
833 chrome.storage.local.set({
834 pendingDismissals: items.pendingDismissals,
835 recentDismissals: items.recentDismissals
838 callbackBoolean(success);
841 function doProcessDismissals() {
842 if (items.pendingDismissals.length == 0) {
843 dismissalAttempts.stop();
848 // Send dismissal for the first card, and if successful, repeat
849 // recursively with the rest.
850 var dismissal = items.pendingDismissals[0];
851 requestCardDismissal(
852 dismissal.chromeNotificationId,
854 dismissal.dismissalData,
857 dismissalsChanged = true;
858 items.pendingDismissals.splice(0, 1);
859 items.recentDismissals[dismissal.chromeNotificationId] =
861 doProcessDismissals();
868 doProcessDismissals();
873 * Submits a task to send pending dismissals.
875 function retryPendingDismissals() {
876 tasks.add(RETRY_DISMISS_TASK_NAME, function() {
877 dismissalAttempts.planForNext(function() {
878 processPendingDismissals(function(success) {});
884 * Opens a URL in a new tab.
885 * @param {string} url URL to open.
887 function openUrl(url) {
888 instrumented.tabs.create({url: url}, function(tab) {
890 chrome.windows.update(tab.windowId, {focused: true});
892 chrome.windows.create({url: url, focused: true});
897 * Opens URL corresponding to the clicked part of the notification.
898 * @param {string} chromeNotificationId chrome.notifications ID of the card.
899 * @param {function(Object): string} selector Function that extracts the url for
900 * the clicked area from the button action URLs info.
902 function onNotificationClicked(chromeNotificationId, selector) {
903 instrumented.storage.local.get('notificationsData', function(items) {
904 var notificationData = items &&
905 items.notificationsData &&
906 items.notificationsData[chromeNotificationId];
908 if (!notificationData)
911 var url = selector(notificationData.actionUrls);
920 * Callback for chrome.notifications.onClosed event.
921 * @param {string} chromeNotificationId chrome.notifications ID of the card.
922 * @param {boolean} byUser Whether the notification was closed by the user.
924 function onNotificationClosed(chromeNotificationId, byUser) {
928 // At this point we are guaranteed that the notification is a now card.
929 chrome.metricsPrivate.recordUserAction('GoogleNow.Dismissed');
931 tasks.add(DISMISS_CARD_TASK_NAME, function() {
932 dismissalAttempts.start();
934 instrumented.storage.local.get(
935 ['pendingDismissals', 'notificationsData'], function(items) {
937 items.pendingDismissals = items.pendingDismissals || [];
938 items.notificationsData = items.notificationsData || {};
940 // Deleting the notification in case it was re-added while this task was
941 // scheduled, waiting for execution; also cleaning notification's data
943 cardSet.clear(chromeNotificationId, true);
945 var notificationData = items.notificationsData[chromeNotificationId];
947 if (notificationData && notificationData.dismissals) {
948 for (var i = 0; i < notificationData.dismissals.length; i++) {
950 chromeNotificationId: chromeNotificationId,
952 dismissalData: notificationData.dismissals[i]
954 items.pendingDismissals.push(dismissal);
957 chrome.storage.local.set({pendingDismissals: items.pendingDismissals});
960 processPendingDismissals(function(success) {});
966 * Initializes the polling system to start monitoring location and fetching
969 function startPollingCards() {
970 // Create an update timer for a case when for some reason location request
972 updateCardsAttempts.start(MAXIMUM_POLLING_PERIOD_SECONDS);
978 * Stops all machinery in the polling system.
980 function stopPollingCards() {
981 stopRequestLocation();
982 updateCardsAttempts.stop();
984 // Mark the Google Now as disabled to start with checking the opt-in state
985 // next time startPollingCards() is called.
986 chrome.storage.local.set({googleNowEnabled: false});
990 * Initializes the event page on install or on browser startup.
992 function initialize() {
993 recordEvent(GoogleNowEvent.EXTENSION_START);
998 * Starts or stops the polling of cards.
999 * @param {boolean} shouldPollCardsRequest true to start and
1000 * false to stop polling cards.
1002 function setShouldPollCards(shouldPollCardsRequest) {
1003 updateCardsAttempts.isRunning(function(currentValue) {
1004 if (shouldPollCardsRequest != currentValue) {
1005 console.log('Action Taken setShouldPollCards=' + shouldPollCardsRequest);
1006 if (shouldPollCardsRequest)
1007 startPollingCards();
1012 'Action Ignored setShouldPollCards=' + shouldPollCardsRequest);
1018 * Enables or disables the Google Now background permission.
1019 * @param {boolean} backgroundEnable true to run in the background.
1020 * false to not run in the background.
1022 function setBackgroundEnable(backgroundEnable) {
1023 instrumented.permissions.contains({permissions: ['background']},
1024 function(hasPermission) {
1025 if (backgroundEnable != hasPermission) {
1026 console.log('Action Taken setBackgroundEnable=' + backgroundEnable);
1027 if (backgroundEnable)
1028 chrome.permissions.request({permissions: ['background']});
1030 chrome.permissions.remove({permissions: ['background']});
1032 console.log('Action Ignored setBackgroundEnable=' + backgroundEnable);
1038 * Does the actual work of deciding what Google Now should do
1039 * based off of the current state of Chrome.
1040 * @param {boolean} signedIn true if the user is signed in.
1041 * @param {boolean} geolocationEnabled true if
1042 * the geolocation option is enabled.
1043 * @param {boolean} canEnableBackground true if
1044 * the background permission can be requested.
1045 * @param {boolean} notificationEnabled true if
1046 * Google Now for Chrome is allowed to show notifications.
1047 * @param {boolean} googleNowEnabled true if
1048 * the Google Now is enabled for the user.
1050 function updateRunningState(
1053 canEnableBackground,
1054 notificationEnabled,
1057 'State Update signedIn=' + signedIn + ' ' +
1058 'geolocationEnabled=' + geolocationEnabled + ' ' +
1059 'canEnableBackground=' + canEnableBackground + ' ' +
1060 'notificationEnabled=' + notificationEnabled + ' ' +
1061 'googleNowEnabled=' + googleNowEnabled);
1063 // TODO(vadimt): Remove this line once state machine design is finalized.
1064 geolocationEnabled = true;
1066 var shouldPollCards = false;
1067 var shouldSetBackground = false;
1069 if (signedIn && notificationEnabled) {
1070 if (geolocationEnabled) {
1071 if (canEnableBackground && googleNowEnabled)
1072 shouldSetBackground = true;
1074 shouldPollCards = true;
1077 recordEvent(GoogleNowEvent.STOPPED);
1081 'Requested Actions shouldSetBackground=' + shouldSetBackground + ' ' +
1082 'setShouldPollCards=' + shouldPollCards);
1084 setBackgroundEnable(shouldSetBackground);
1085 setShouldPollCards(shouldPollCards);
1089 * Coordinates the behavior of Google Now for Chrome depending on
1090 * Chrome and extension state.
1092 function onStateChange() {
1093 tasks.add(STATE_CHANGED_TASK_NAME, function() {
1094 authenticationManager.isSignedIn(function(signedIn) {
1095 instrumented.metricsPrivate.getVariationParams(
1097 function(response) {
1098 var canEnableBackground =
1099 (!response || (response.canEnableBackground != 'false'));
1100 instrumented.notifications.getPermissionLevel(function(level) {
1101 var notificationEnabled = (level == 'granted');
1104 googleGeolocationAccessEnabled.
1105 get({}, function(prefValue) {
1106 var geolocationEnabled = !!prefValue.value;
1107 instrumented.storage.local.get(
1110 var googleNowEnabled =
1111 items && !!items.googleNowEnabled;
1115 canEnableBackground,
1116 notificationEnabled,
1126 instrumented.runtime.onInstalled.addListener(function(details) {
1127 console.log('onInstalled ' + JSON.stringify(details));
1128 if (details.reason != 'chrome_update') {
1133 instrumented.runtime.onStartup.addListener(function() {
1134 console.log('onStartup');
1136 // Show notifications received by earlier polls. Doing this as early as
1137 // possible to reduce latency of showing first notifications. This mimics how
1138 // persistent notifications will work.
1139 tasks.add(SHOW_ON_START_TASK_NAME, function() {
1140 instrumented.storage.local.get('notificationGroups', function(items) {
1141 console.log('onStartup-get ' + JSON.stringify(items));
1142 items = items || {};
1143 items.notificationGroups = items.notificationGroups || {};
1145 mergeAndShowNotificationCards(items.notificationGroups);
1154 googleGeolocationAccessEnabled.
1156 addListener(function(prefValue) {
1157 console.log('googleGeolocationAccessEnabled Pref onChange ' +
1162 authenticationManager.addListener(function() {
1163 console.log('signIn State Change');
1167 instrumented.notifications.onClicked.addListener(
1168 function(chromeNotificationId) {
1169 chrome.metricsPrivate.recordUserAction('GoogleNow.MessageClicked');
1170 onNotificationClicked(chromeNotificationId, function(actionUrls) {
1171 return actionUrls && actionUrls.messageUrl;
1175 instrumented.notifications.onButtonClicked.addListener(
1176 function(chromeNotificationId, buttonIndex) {
1177 chrome.metricsPrivate.recordUserAction(
1178 'GoogleNow.ButtonClicked' + buttonIndex);
1179 onNotificationClicked(chromeNotificationId, function(actionUrls) {
1180 var url = actionUrls.buttonUrls[buttonIndex];
1181 verify(url, 'onButtonClicked: no url for a button');
1186 instrumented.notifications.onClosed.addListener(onNotificationClosed);
1188 instrumented.notifications.onPermissionLevelChanged.addListener(
1189 function(permissionLevel) {
1190 console.log('Notifications permissionLevel Change');
1194 instrumented.notifications.onShowSettings.addListener(function() {
1195 openUrl(SETTINGS_URL);
1198 instrumented.location.onLocationUpdate.addListener(function(position) {
1199 recordEvent(GoogleNowEvent.LOCATION_UPDATE);
1200 updateNotificationsCards(position);
1203 instrumented.pushMessaging.onMessage.addListener(function(message) {
1204 // message.payload will be '' when the extension first starts.
1205 // Each time after signing in, we'll get latest payload for all channels.
1206 // So, we need to poll the server only when the payload is non-empty and has
1208 console.log('pushMessaging.onMessage ' + JSON.stringify(message));
1209 if (message.payload.indexOf('REQUEST_CARDS') == 0) {
1210 tasks.add(ON_PUSH_MESSAGE_START_TASK_NAME, function() {
1211 instrumented.storage.local.get(
1212 ['lastPollNowPayloads', 'notificationGroups'], function(items) {
1213 // If storage.get fails, it's safer to do nothing, preventing polling
1214 // the server when the payload really didn't change.
1218 // If this is the first time we get lastPollNowPayloads, initialize it.
1219 items.lastPollNowPayloads = items.lastPollNowPayloads || {};
1221 if (items.lastPollNowPayloads[message.subchannelId] !=
1223 items.lastPollNowPayloads[message.subchannelId] = message.payload;
1225 items.notificationGroups = items.notificationGroups || {};
1226 items.notificationGroups['PUSH' + message.subchannelId] = {
1228 nextPollTime: Date.now()
1231 chrome.storage.local.set({
1232 lastPollNowPayloads: items.lastPollNowPayloads,
1233 notificationGroups: items.notificationGroups
1236 updateNotificationsCards();