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. Processing requests for cards dismissals that are not yet sent to the
15 * 2. Making a server request.
16 * 3. Showing the received cards as notifications.
19 // TODO(robliao): Decide what to do in incognito mode.
22 * Standard response code for successful HTTP requests. This is the only success
23 * code the server will send.
26 var HTTP_NOCONTENT
= 204;
28 var HTTP_BAD_REQUEST
= 400;
29 var HTTP_UNAUTHORIZED
= 401;
30 var HTTP_FORBIDDEN
= 403;
31 var HTTP_METHOD_NOT_ALLOWED
= 405;
33 var MS_IN_SECOND
= 1000;
34 var MS_IN_MINUTE
= 60 * 1000;
37 * Initial period for polling for Google Now Notifications cards to use when the
38 * period from the server is not available.
40 var INITIAL_POLLING_PERIOD_SECONDS
= 5 * 60; // 5 minutes
43 * Mininal period for polling for Google Now Notifications cards.
45 var MINIMUM_POLLING_PERIOD_SECONDS
= 5 * 60; // 5 minutes
48 * Maximal period for polling for Google Now Notifications cards to use when the
49 * period from the server is not available.
51 var MAXIMUM_POLLING_PERIOD_SECONDS
= 60 * 60; // 1 hour
54 * Initial period for polling for Google Now optin notification after push
55 * messaging indicates Google Now is enabled.
57 var INITIAL_OPTIN_RECHECK_PERIOD_SECONDS
= 60; // 1 minute
60 * Maximum period for polling for Google Now optin notification after push
61 * messaging indicates Google Now is enabled. It is expected that the alarm
62 * will be stopped after this.
64 var MAXIMUM_OPTIN_RECHECK_PERIOD_SECONDS
= 16 * 60; // 16 minutes
67 * Initial period for retrying the server request for dismissing cards.
69 var INITIAL_RETRY_DISMISS_PERIOD_SECONDS
= 60; // 1 minute
72 * Maximum period for retrying the server request for dismissing cards.
74 var MAXIMUM_RETRY_DISMISS_PERIOD_SECONDS
= 60 * 60; // 1 hour
77 * Time we keep retrying dismissals.
79 var MAXIMUM_DISMISSAL_AGE_MS
= 24 * 60 * 60 * 1000; // 1 day
82 * Time we keep dismissals after successful server dismiss requests.
84 var DISMISS_RETENTION_TIME_MS
= 20 * 60 * 1000; // 20 minutes
87 * Default period for checking whether the user is opted in to Google Now.
89 var DEFAULT_OPTIN_CHECK_PERIOD_SECONDS
= 60 * 60 * 24 * 7; // 1 week
92 * URL to open when the user clicked on a link for the our notification
95 var SETTINGS_URL
= 'https://support.google.com/chrome/?p=ib_google_now_welcome';
98 * Number of cards that need an explanatory link.
100 var EXPLANATORY_CARDS_LINK_THRESHOLD
= 4;
103 * Names for tasks that can be created by the extension.
105 var UPDATE_CARDS_TASK_NAME
= 'update-cards';
106 var DISMISS_CARD_TASK_NAME
= 'dismiss-card';
107 var RETRY_DISMISS_TASK_NAME
= 'retry-dismiss';
108 var STATE_CHANGED_TASK_NAME
= 'state-changed';
109 var SHOW_ON_START_TASK_NAME
= 'show-cards-on-start';
110 var ON_PUSH_MESSAGE_START_TASK_NAME
= 'on-push-message';
113 * Group as received from the server.
116 * nextPollSeconds: (string|undefined),
117 * rank: (number|undefined),
118 * requested: (boolean|undefined)
124 * Server response with notifications and groups.
127 * googleNowDisabled: (boolean|undefined),
128 * groups: Object.<string, ReceivedGroup>,
129 * notifications: Array.<ReceivedNotification>
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.
142 * cards: Array.<ReceivedNotification>,
143 * cardsTimestamp: (number|undefined),
144 * nextPollTime: (number|undefined),
145 * rank: (number|undefined)
148 var StoredNotificationGroup
;
151 * Pending (not yet successfully sent) dismissal for a received notification.
152 * |time| is the moment when the user requested dismissal.
155 * chromeNotificationId: ChromeNotificationId,
157 * dismissalData: DismissalData
160 var PendingDismissal
;
163 * Checks if a new task can't be scheduled when another task is already
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.
169 function areTasksConflicting(newTaskName
, scheduledTaskName
) {
170 if (newTaskName
== UPDATE_CARDS_TASK_NAME
&&
171 scheduledTaskName
== UPDATE_CARDS_TASK_NAME
) {
172 // If a card update is requested while an old update is still scheduled, we
173 // don't need the new update.
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.
189 var tasks
= buildTaskManager(areTasksConflicting
);
191 // Add error processing to API calls.
192 wrapper
.instrumentChromeApiFunction('metricsPrivate.getVariationParams', 1);
193 wrapper
.instrumentChromeApiFunction('notifications.clear', 1);
194 wrapper
.instrumentChromeApiFunction('notifications.create', 2);
195 wrapper
.instrumentChromeApiFunction('notifications.getPermissionLevel', 0);
196 wrapper
.instrumentChromeApiFunction('notifications.update', 2);
197 wrapper
.instrumentChromeApiFunction('notifications.getAll', 0);
198 wrapper
.instrumentChromeApiFunction(
199 'notifications.onButtonClicked.addListener', 0);
200 wrapper
.instrumentChromeApiFunction('notifications.onClicked.addListener', 0);
201 wrapper
.instrumentChromeApiFunction('notifications.onClosed.addListener', 0);
202 wrapper
.instrumentChromeApiFunction(
203 'notifications.onPermissionLevelChanged.addListener', 0);
204 wrapper
.instrumentChromeApiFunction(
205 'notifications.onShowSettings.addListener', 0);
206 wrapper
.instrumentChromeApiFunction('permissions.contains', 1);
207 wrapper
.instrumentChromeApiFunction('pushMessaging.onMessage.addListener', 0);
208 wrapper
.instrumentChromeApiFunction('storage.onChanged.addListener', 0);
209 wrapper
.instrumentChromeApiFunction('runtime.onInstalled.addListener', 0);
210 wrapper
.instrumentChromeApiFunction('runtime.onStartup.addListener', 0);
211 wrapper
.instrumentChromeApiFunction('tabs.create', 1);
213 var updateCardsAttempts
= buildAttemptManager(
216 INITIAL_POLLING_PERIOD_SECONDS
,
217 MAXIMUM_POLLING_PERIOD_SECONDS
);
218 var optInPollAttempts
= buildAttemptManager(
220 pollOptedInNoImmediateRecheck
,
221 INITIAL_POLLING_PERIOD_SECONDS
,
222 MAXIMUM_POLLING_PERIOD_SECONDS
);
223 var optInRecheckAttempts
= buildAttemptManager(
225 pollOptedInWithRecheck
,
226 INITIAL_OPTIN_RECHECK_PERIOD_SECONDS
,
227 MAXIMUM_OPTIN_RECHECK_PERIOD_SECONDS
);
228 var dismissalAttempts
= buildAttemptManager(
230 retryPendingDismissals
,
231 INITIAL_RETRY_DISMISS_PERIOD_SECONDS
,
232 MAXIMUM_RETRY_DISMISS_PERIOD_SECONDS
);
233 var cardSet
= buildCardSet();
235 var authenticationManager
= buildAuthenticationManager();
238 * Google Now UMA event identifier.
241 var GoogleNowEvent
= {
242 REQUEST_FOR_CARDS_TOTAL
: 0,
243 REQUEST_FOR_CARDS_SUCCESS
: 1,
244 CARDS_PARSE_SUCCESS
: 2,
245 DISMISS_REQUEST_TOTAL
: 3,
246 DISMISS_REQUEST_SUCCESS
: 4,
248 DELETED_LOCATION_UPDATE
: 6,
250 DELETED_SHOW_WELCOME_TOAST
: 8,
252 DELETED_USER_SUPPRESSED
: 10,
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
261 * Records a Google Now Event.
262 * @param {GoogleNowEvent} event Event identifier.
264 function recordEvent(event
) {
265 var metricDescription
= {
266 metricName
: 'GoogleNow.Event',
267 type
: 'histogram-linear',
269 max
: GoogleNowEvent
.EVENTS_TOTAL
,
270 buckets
: GoogleNowEvent
.EVENTS_TOTAL
+ 1
273 chrome
.metricsPrivate
.recordValue(metricDescription
, event
);
277 * Records a notification clicked event.
278 * @param {number|undefined} cardTypeId Card type ID.
280 function recordNotificationClick(cardTypeId
) {
281 if (cardTypeId
!== undefined) {
282 chrome
.metricsPrivate
.recordSparseValue(
283 'GoogleNow.Card.Clicked', cardTypeId
);
288 * Records a button clicked event.
289 * @param {number|undefined} cardTypeId Card type ID.
290 * @param {number} buttonIndex Button Index
292 function recordButtonClick(cardTypeId
, buttonIndex
) {
293 if (cardTypeId
!== undefined) {
294 chrome
.metricsPrivate
.recordSparseValue(
295 'GoogleNow.Card.Button.Clicked' + buttonIndex
, cardTypeId
);
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
304 * @return {function(XMLHttpRequest)} Function that validates the token with the
305 * supplied XMLHttpRequest.
307 function checkAuthenticationStatus(token
) {
308 return function(request
) {
309 if (request
.status
== HTTP_FORBIDDEN
||
310 request
.status
== HTTP_UNAUTHORIZED
) {
311 authenticationManager
.removeToken(token
);
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.
324 function requestFromServer(method
, handlerName
, opt_contentType
) {
325 return authenticationManager
.getAuthToken().then(function(token
) {
326 var request
= buildServerRequest(method
, handlerName
, opt_contentType
);
327 request
.setRequestHeader('Authorization', 'Bearer ' + token
);
328 var requestPromise
= new Promise(function(resolve
, reject
) {
329 request
.addEventListener('loadend', function() {
330 if ((200 <= request
.status
) && (request
.status
< 300)) {
338 requestPromise
.catch(checkAuthenticationStatus(token
));
339 return requestPromise
;
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.
351 function showNotificationGroups(notificationGroups
, opt_onCardShown
) {
352 /** @type {Object.<ChromeNotificationId, CombinedCard>} */
353 var cards
= combineCardsFromGroups(notificationGroups
);
354 console
.log('showNotificationGroups ' + JSON
.stringify(cards
));
356 return new Promise(function(resolve
) {
357 instrumented
.notifications
.getAll(function(notifications
) {
358 console
.log('showNotificationGroups-getAll ' +
359 JSON
.stringify(notifications
));
360 notifications
= notifications
|| {};
362 // Mark notifications that didn't receive an update as having received
364 for (var chromeNotificationId
in notifications
) {
365 cards
[chromeNotificationId
] = cards
[chromeNotificationId
] || [];
368 /** @type {Object.<ChromeNotificationId, NotificationDataEntry>} */
369 var notificationsData
= {};
371 // Create/update/delete notifications.
372 for (var chromeNotificationId
in cards
) {
373 notificationsData
[chromeNotificationId
] = cardSet
.update(
374 chromeNotificationId
,
375 cards
[chromeNotificationId
],
379 chrome
.storage
.local
.set({notificationsData
: notificationsData
});
386 * Removes all cards and card state on Google Now close down.
388 function removeAllCards() {
389 console
.log('removeAllCards');
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() {});
399 chrome
.storage
.local
.remove(['notificationsData', 'notificationGroups']);
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
411 function combineGroup(combinedCards
, storedGroup
) {
412 for (var i
= 0; i
< storedGroup
.cards
.length
; i
++) {
413 /** @type {ReceivedNotification} */
414 var receivedNotification
= storedGroup
.cards
[i
];
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
427 combinedCards
[receivedNotification
.chromeNotificationId
] || [];
428 combinedCard
.push(uncombinedNotification
);
429 combinedCards
[receivedNotification
.chromeNotificationId
] = combinedCard
;
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.
439 function calculateNextPollTimeMilliseconds(groups
) {
440 var nextPollTime
= null;
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
);
450 // At least one of the groups must have nextPollTime.
451 verify(nextPollTime
!= null, 'calculateNextPollTime: nextPollTime is null');
456 * Schedules next cards poll.
457 * @param {Object.<string, StoredNotificationGroup>} groups Map from group name
458 * to group information.
460 function scheduleNextCardsPoll(groups
) {
461 var nextPollTimeMs
= calculateNextPollTimeMilliseconds(groups
);
463 var nextPollDelaySeconds
= Math
.max(
464 (nextPollTimeMs
- Date
.now()) / MS_IN_SECOND
,
465 MINIMUM_POLLING_PERIOD_SECONDS
);
466 updateCardsAttempts
.start(nextPollDelaySeconds
);
470 * Schedules the next opt-in check poll.
472 function scheduleOptInCheckPoll() {
473 instrumented
.metricsPrivate
.getVariationParams(
474 'GoogleNow', function(params
) {
475 var optinPollPeriodSeconds
=
476 parseInt(params
&& params
.optinPollPeriodSeconds
, 10) ||
477 DEFAULT_OPTIN_CHECK_PERIOD_SECONDS
;
478 optInPollAttempts
.start(optinPollPeriodSeconds
);
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.
488 function combineCardsFromGroups(notificationGroups
) {
489 console
.log('combineCardsFromGroups ' + JSON
.stringify(notificationGroups
));
490 /** @type {Object.<ChromeNotificationId, CombinedCard>} */
491 var combinedCards
= {};
493 for (var groupName
in notificationGroups
)
494 combineGroup(combinedCards
, notificationGroups
[groupName
]);
496 return combinedCards
;
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.
505 function processServerResponse(response
) {
506 console
.log('processServerResponse ' + JSON
.stringify(response
));
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();
514 var receivedGroups
= response
.groups
;
516 return fillFromChromeLocalStorage({
517 /** @type {Object.<string, StoredNotificationGroup>} */
518 notificationGroups
: {},
519 /** @type {Object.<ServerNotificationId, number>} */
521 }).then(function(items
) {
522 console
.log('processServerResponse-get ' + JSON
.stringify(items
));
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
];
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
);
550 // Build updated set of groups.
551 var updatedGroups
= {};
553 for (var groupName
in receivedGroups
) {
554 var receivedGroup
= receivedGroups
[groupName
];
555 var storedGroup
= items
.notificationGroups
[groupName
] || {
557 cardsTimestamp
: undefined,
558 nextPollTime
: undefined,
562 if (receivedGroup
.requested
)
563 receivedGroup
.cards
= receivedGroup
.cards
|| [];
565 if (receivedGroup
.cards
) {
566 // If the group contains a cards update, all its fields will get new
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.
578 // 'nextPollSeconds' may be sent even for groups that don't contain
580 if (receivedGroup
.nextPollSeconds
!== undefined) {
581 storedGroup
.nextPollTime
=
582 now
+ receivedGroup
.nextPollSeconds
* MS_IN_SECOND
;
585 updatedGroups
[groupName
] = storedGroup
;
588 scheduleNextCardsPoll(updatedGroups
);
590 updatedGroups
: updatedGroups
,
591 recentDismissals
: updatedRecentDismissals
597 * Update the Explanatory Total Cards Shown Count.
599 function countExplanatoryCard() {
600 localStorage
['explanatoryCardsShown']++;
604 * Determines if cards should have an explanation link.
605 * @return {boolean} true if an explanatory card should be shown.
607 function shouldShowExplanatoryCard() {
608 var isBelowThreshold
=
609 localStorage
['explanatoryCardsShown'] < EXPLANATORY_CARDS_LINK_THRESHOLD
;
610 return isBelowThreshold
;
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.
618 function requestNotificationGroupsFromServer(groupNames
) {
620 'requestNotificationGroupsFromServer from ' + NOTIFICATION_CARDS_URL
+
621 ', groupNames=' + JSON
.stringify(groupNames
));
623 recordEvent(GoogleNowEvent
.REQUEST_FOR_CARDS_TOTAL
);
625 var requestParameters
= '?timeZoneOffsetMs=' +
626 (-new Date().getTimezoneOffset() * MS_IN_MINUTE
);
628 if (shouldShowExplanatoryCard()) {
629 requestParameters
+= '&cardExplanation=true';
632 groupNames
.forEach(function(groupName
) {
633 requestParameters
+= ('&requestTypes=' + groupName
);
636 requestParameters
+= '&uiLocale=' + navigator
.language
;
639 'requestNotificationGroupsFromServer: request=' + requestParameters
);
641 return requestFromServer('GET', 'notifications' + requestParameters
).then(
644 'requestNotificationGroupsFromServer-received ' + request
.status
);
645 if (request
.status
== HTTP_OK
) {
646 recordEvent(GoogleNowEvent
.REQUEST_FOR_CARDS_SUCCESS
);
647 return JSON
.parse(request
.responseText
);
653 * Performs an opt-in poll without an immediate recheck.
654 * If the response is not opted-in, schedule an opt-in check poll.
656 function pollOptedInNoImmediateRecheck() {
657 requestAndUpdateOptedIn()
658 .then(function(optedIn
) {
660 // Request a repoll if we're not opted in.
661 return Promise
.reject();
665 scheduleOptInCheckPoll();
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.
675 function requestAndUpdateOptedIn() {
676 console
.log('requestOptedIn from ' + NOTIFICATION_CARDS_URL
);
678 return requestFromServer('GET', 'settings/optin').then(function(request
) {
680 'requestOptedIn-received ' + request
.status
+ ' ' + request
.response
);
681 if (request
.status
== HTTP_OK
) {
682 var parsedResponse
= JSON
.parse(request
.responseText
);
683 return parsedResponse
.value
;
685 }).then(function(optedIn
) {
686 chrome
.storage
.local
.set({googleNowEnabled
: optedIn
});
692 * Determines the groups that need to be requested right now.
693 * @return {Promise} A promise to determine the groups to request.
695 function getGroupsToRequest() {
696 return fillFromChromeLocalStorage({
697 /** @type {Object.<string, StoredNotificationGroup>} */
698 notificationGroups
: {}
699 }).then(function(items
) {
700 console
.log('getGroupsToRequest-storage-get ' + JSON
.stringify(items
));
701 var groupsToRequest
= [];
702 var now
= Date
.now();
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
);
709 return groupsToRequest
;
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.
718 function requestNotificationCards() {
719 console
.log('requestNotificationCards');
720 return getGroupsToRequest()
721 .then(requestNotificationGroupsFromServer
)
722 .then(processServerResponse
)
723 .then(function(processedResponse
) {
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
732 recordEvent(GoogleNowEvent
.CARDS_PARSE_SUCCESS
);
739 * Requests and shows notification cards.
741 function requestCards() {
742 console
.log('requestCards @' + new Date());
743 // LOCATION_REQUEST is a legacy histogram value when we requested location.
744 // This corresponds to the extension attempting to request for cards.
745 // We're keeping the name the same to keep our histograms in order.
746 recordEvent(GoogleNowEvent
.LOCATION_REQUEST
);
747 tasks
.add(UPDATE_CARDS_TASK_NAME
, function() {
748 console
.log('requestCards-task-begin');
749 updateCardsAttempts
.isRunning(function(running
) {
751 // The cards are requested only if there are no unsent dismissals.
752 processPendingDismissals()
753 .then(requestNotificationCards
)
754 .catch(updateCardsAttempts
.scheduleRetry
);
761 * Sends a server request to dismiss a card.
762 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
764 * @param {number} dismissalTimeMs Time of the user's dismissal of the card in
765 * milliseconds since epoch.
766 * @param {DismissalData} dismissalData Data to build a dismissal request.
767 * @return {Promise} A promise to request the card dismissal, rejects on error.
769 function requestCardDismissal(
770 chromeNotificationId
, dismissalTimeMs
, dismissalData
) {
771 console
.log('requestDismissingCard ' + chromeNotificationId
+
772 ' from ' + NOTIFICATION_CARDS_URL
+
773 ', dismissalData=' + JSON
.stringify(dismissalData
));
775 var dismissalAge
= Date
.now() - dismissalTimeMs
;
777 if (dismissalAge
> MAXIMUM_DISMISSAL_AGE_MS
) {
778 return Promise
.resolve();
781 recordEvent(GoogleNowEvent
.DISMISS_REQUEST_TOTAL
);
783 var requestParameters
= 'notifications/' + dismissalData
.notificationId
+
784 '?age=' + dismissalAge
+
785 '&chromeNotificationId=' + chromeNotificationId
;
787 for (var paramField
in dismissalData
.parameters
)
788 requestParameters
+= ('&' + paramField
+
789 '=' + dismissalData
.parameters
[paramField
]);
791 console
.log('requestCardDismissal: requestParameters=' + requestParameters
);
793 return requestFromServer('DELETE', requestParameters
).then(function(request
) {
794 console
.log('requestDismissingCard-onloadend ' + request
.status
);
795 if (request
.status
== HTTP_NOCONTENT
)
796 recordEvent(GoogleNowEvent
.DISMISS_REQUEST_SUCCESS
);
798 // A dismissal doesn't require further retries if it was successful or
799 // doesn't have a chance for successful completion.
800 return (request
.status
== HTTP_NOCONTENT
) ?
803 }).catch(function(request
) {
804 return (request
.status
== HTTP_BAD_REQUEST
||
805 request
.status
== HTTP_METHOD_NOT_ALLOWED
) ?
812 * Tries to send dismiss requests for all pending dismissals.
813 * @return {Promise} A promise to process the pending dismissals.
814 * The promise is rejected if a problem was encountered.
816 function processPendingDismissals() {
817 return fillFromChromeLocalStorage({
818 /** @type {Array.<PendingDismissal>} */
819 pendingDismissals
: [],
820 /** @type {Object.<ServerNotificationId, number>} */
822 }).then(function(items
) {
824 'processPendingDismissals-storage-get ' + JSON
.stringify(items
));
826 var dismissalsChanged
= false;
828 function onFinish(success
) {
829 if (dismissalsChanged
) {
830 chrome
.storage
.local
.set({
831 pendingDismissals
: items
.pendingDismissals
,
832 recentDismissals
: items
.recentDismissals
835 return success
? Promise
.resolve() : Promise
.reject();
838 function doProcessDismissals() {
839 if (items
.pendingDismissals
.length
== 0) {
840 dismissalAttempts
.stop();
841 return onFinish(true);
844 // Send dismissal for the first card, and if successful, repeat
845 // recursively with the rest.
846 /** @type {PendingDismissal} */
847 var dismissal
= items
.pendingDismissals
[0];
848 return requestCardDismissal(
849 dismissal
.chromeNotificationId
,
851 dismissal
.dismissalData
).then(function() {
852 dismissalsChanged
= true;
853 items
.pendingDismissals
.splice(0, 1);
854 items
.recentDismissals
[dismissal
.dismissalData
.notificationId
] =
856 return doProcessDismissals();
857 }).catch(function() {
858 return onFinish(false);
862 return doProcessDismissals();
867 * Submits a task to send pending dismissals.
869 function retryPendingDismissals() {
870 tasks
.add(RETRY_DISMISS_TASK_NAME
, function() {
871 processPendingDismissals().catch(dismissalAttempts
.scheduleRetry
);
876 * Opens a URL in a new tab.
877 * @param {string} url URL to open.
879 function openUrl(url
) {
880 instrumented
.tabs
.create({url
: url
}, function(tab
) {
882 chrome
.windows
.update(tab
.windowId
, {focused
: true});
884 chrome
.windows
.create({url
: url
, focused
: true});
889 * Opens URL corresponding to the clicked part of the notification.
890 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
892 * @param {function(NotificationDataEntry): (string|undefined)} selector
893 * Function that extracts the url for the clicked area from the
894 * notification data entry.
896 function onNotificationClicked(chromeNotificationId
, selector
) {
897 fillFromChromeLocalStorage({
898 /** @type {Object.<ChromeNotificationId, NotificationDataEntry>} */
899 notificationsData
: {}
900 }).then(function(items
) {
901 /** @type {(NotificationDataEntry|undefined)} */
902 var notificationDataEntry
= items
.notificationsData
[chromeNotificationId
];
903 if (!notificationDataEntry
)
906 var url
= selector(notificationDataEntry
);
915 * Callback for chrome.notifications.onClosed event.
916 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of
918 * @param {boolean} byUser Whether the notification was closed by the user.
920 function onNotificationClosed(chromeNotificationId
, byUser
) {
924 // At this point we are guaranteed that the notification is a now card.
925 chrome
.metricsPrivate
.recordUserAction('GoogleNow.Dismissed');
927 tasks
.add(DISMISS_CARD_TASK_NAME
, function() {
928 dismissalAttempts
.start();
930 fillFromChromeLocalStorage({
931 /** @type {Array.<PendingDismissal>} */
932 pendingDismissals
: [],
933 /** @type {Object.<ChromeNotificationId, NotificationDataEntry>} */
934 notificationsData
: {},
935 /** @type {Object.<string, StoredNotificationGroup>} */
936 notificationGroups
: {}
937 }).then(function(items
) {
938 /** @type {NotificationDataEntry} */
939 var notificationData
=
940 items
.notificationsData
[chromeNotificationId
] ||
942 timestamp
: Date
.now(),
946 var dismissalResult
=
948 chromeNotificationId
,
950 items
.notificationGroups
);
952 for (var i
= 0; i
< dismissalResult
.dismissals
.length
; i
++) {
953 /** @type {PendingDismissal} */
955 chromeNotificationId
: chromeNotificationId
,
957 dismissalData
: dismissalResult
.dismissals
[i
]
959 items
.pendingDismissals
.push(dismissal
);
962 items
.notificationsData
[chromeNotificationId
] =
963 dismissalResult
.notificationData
;
965 chrome
.storage
.local
.set(items
);
967 processPendingDismissals();
973 * Initializes the polling system to start fetching cards.
975 function startPollingCards() {
976 console
.log('startPollingCards');
977 // Create an update timer for a case when for some reason requesting
979 updateCardsAttempts
.start(MAXIMUM_POLLING_PERIOD_SECONDS
);
984 * Stops all machinery in the polling system.
986 function stopPollingCards() {
987 console
.log('stopPollingCards');
988 updateCardsAttempts
.stop();
989 // Since we're stopping everything, clear all runtime storage.
990 // We don't clear localStorage since those values are still relevant
991 // across Google Now start-stop events.
992 chrome
.storage
.local
.clear();
996 * Initializes the event page on install or on browser startup.
998 function initialize() {
999 recordEvent(GoogleNowEvent
.EXTENSION_START
);
1004 * Starts or stops the main pipeline for polling cards.
1005 * @param {boolean} shouldPollCardsRequest true to start and
1006 * false to stop polling cards.
1008 function setShouldPollCards(shouldPollCardsRequest
) {
1009 updateCardsAttempts
.isRunning(function(currentValue
) {
1010 if (shouldPollCardsRequest
!= currentValue
) {
1011 console
.log('Action Taken setShouldPollCards=' + shouldPollCardsRequest
);
1012 if (shouldPollCardsRequest
)
1013 startPollingCards();
1018 'Action Ignored setShouldPollCards=' + shouldPollCardsRequest
);
1024 * Starts or stops the optin check.
1025 * @param {boolean} shouldPollOptInStatus true to start and false to stop
1026 * polling the optin status.
1028 function setShouldPollOptInStatus(shouldPollOptInStatus
) {
1029 optInPollAttempts
.isRunning(function(currentValue
) {
1030 if (shouldPollOptInStatus
!= currentValue
) {
1032 'Action Taken setShouldPollOptInStatus=' + shouldPollOptInStatus
);
1033 if (shouldPollOptInStatus
) {
1034 pollOptedInNoImmediateRecheck();
1036 optInPollAttempts
.stop();
1040 'Action Ignored setShouldPollOptInStatus=' + shouldPollOptInStatus
);
1046 * Enables or disables the Google Now background permission.
1047 * @param {boolean} backgroundEnable true to run in the background.
1048 * false to not run in the background.
1050 function setBackgroundEnable(backgroundEnable
) {
1051 instrumented
.permissions
.contains({permissions
: ['background']},
1052 function(hasPermission
) {
1053 if (backgroundEnable
!= hasPermission
) {
1054 console
.log('Action Taken setBackgroundEnable=' + backgroundEnable
);
1055 if (backgroundEnable
)
1056 chrome
.permissions
.request({permissions
: ['background']});
1058 chrome
.permissions
.remove({permissions
: ['background']});
1060 console
.log('Action Ignored setBackgroundEnable=' + backgroundEnable
);
1066 * Record why this extension would not poll for cards.
1067 * @param {boolean} signedIn true if the user is signed in.
1068 * @param {boolean} notificationEnabled true if
1069 * Google Now for Chrome is allowed to show notifications.
1070 * @param {boolean} googleNowEnabled true if
1071 * the Google Now is enabled for the user.
1073 function recordEventIfNoCards(signedIn
, notificationEnabled
, googleNowEnabled
) {
1075 recordEvent(GoogleNowEvent
.SIGNED_OUT
);
1076 } else if (!notificationEnabled
) {
1077 recordEvent(GoogleNowEvent
.NOTIFICATION_DISABLED
);
1078 } else if (!googleNowEnabled
) {
1079 recordEvent(GoogleNowEvent
.GOOGLE_NOW_DISABLED
);
1084 * Does the actual work of deciding what Google Now should do
1085 * based off of the current state of Chrome.
1086 * @param {boolean} signedIn true if the user is signed in.
1087 * @param {boolean} canEnableBackground true if
1088 * the background permission can be requested.
1089 * @param {boolean} notificationEnabled true if
1090 * Google Now for Chrome is allowed to show notifications.
1091 * @param {boolean} googleNowEnabled true if
1092 * the Google Now is enabled for the user.
1094 function updateRunningState(
1096 canEnableBackground
,
1097 notificationEnabled
,
1100 'State Update signedIn=' + signedIn
+ ' ' +
1101 'canEnableBackground=' + canEnableBackground
+ ' ' +
1102 'notificationEnabled=' + notificationEnabled
+ ' ' +
1103 'googleNowEnabled=' + googleNowEnabled
);
1105 var shouldPollCards
= false;
1106 var shouldPollOptInStatus
= false;
1107 var shouldSetBackground
= false;
1109 if (signedIn
&& notificationEnabled
) {
1110 shouldPollCards
= googleNowEnabled
;
1111 shouldPollOptInStatus
= !googleNowEnabled
;
1112 shouldSetBackground
= canEnableBackground
&& googleNowEnabled
;
1114 recordEvent(GoogleNowEvent
.STOPPED
);
1117 recordEventIfNoCards(signedIn
, notificationEnabled
, googleNowEnabled
);
1120 'Requested Actions shouldSetBackground=' + shouldSetBackground
+ ' ' +
1121 'setShouldPollCards=' + shouldPollCards
+ ' ' +
1122 'shouldPollOptInStatus=' + shouldPollOptInStatus
);
1124 setBackgroundEnable(shouldSetBackground
);
1125 setShouldPollCards(shouldPollCards
);
1126 setShouldPollOptInStatus(shouldPollOptInStatus
);
1127 if (!shouldPollCards
) {
1133 * Coordinates the behavior of Google Now for Chrome depending on
1134 * Chrome and extension state.
1136 function onStateChange() {
1137 tasks
.add(STATE_CHANGED_TASK_NAME
, function() {
1139 authenticationManager
.isSignedIn(),
1140 canEnableBackground(),
1141 isNotificationsEnabled(),
1142 isGoogleNowEnabled()])
1143 .then(function(results
) {
1144 updateRunningState
.apply(null, results
);
1150 * Determines if background mode should be requested.
1151 * @return {Promise} A promise to determine if background can be enabled.
1153 function canEnableBackground() {
1154 return new Promise(function(resolve
) {
1155 instrumented
.metricsPrivate
.getVariationParams(
1157 function(response
) {
1158 resolve(!response
|| (response
.canEnableBackground
!= 'false'));
1164 * Checks if Google Now is enabled in the notifications center.
1165 * @return {Promise} A promise to determine if Google Now is enabled
1166 * in the notifications center.
1168 function isNotificationsEnabled() {
1169 return new Promise(function(resolve
) {
1170 instrumented
.notifications
.getPermissionLevel(function(level
) {
1171 resolve(level
== 'granted');
1177 * Gets the previous Google Now opt-in state.
1178 * @return {Promise} A promise to determine the previous Google Now
1181 function isGoogleNowEnabled() {
1182 return fillFromChromeLocalStorage({googleNowEnabled
: false})
1183 .then(function(items
) {
1184 return items
.googleNowEnabled
;
1189 * Polls the optin state.
1190 * Sometimes we get the response to the opted in result too soon during
1191 * push messaging. We'll recheck the optin state a few times before giving up.
1193 function pollOptedInWithRecheck() {
1195 * Cleans up any state used to recheck the opt-in poll.
1197 function clearPollingState() {
1198 localStorage
.removeItem('optedInCheckCount');
1199 optInRecheckAttempts
.stop();
1202 if (localStorage
.optedInCheckCount
=== undefined) {
1203 localStorage
.optedInCheckCount
= 0;
1204 optInRecheckAttempts
.start();
1207 console
.log(new Date() +
1208 ' checkOptedIn Attempt ' + localStorage
.optedInCheckCount
);
1210 requestAndUpdateOptedIn().then(function(optedIn
) {
1212 clearPollingState();
1213 return Promise
.resolve();
1215 // If we're not opted in, reject to retry.
1216 return Promise
.reject();
1218 }).catch(function() {
1219 if (localStorage
.optedInCheckCount
< 5) {
1220 localStorage
.optedInCheckCount
++;
1221 optInRecheckAttempts
.scheduleRetry();
1223 clearPollingState();
1228 instrumented
.runtime
.onInstalled
.addListener(function(details
) {
1229 console
.log('onInstalled ' + JSON
.stringify(details
));
1230 if (details
.reason
!= 'chrome_update') {
1235 instrumented
.runtime
.onStartup
.addListener(function() {
1236 console
.log('onStartup');
1238 // Show notifications received by earlier polls. Doing this as early as
1239 // possible to reduce latency of showing first notifications. This mimics how
1240 // persistent notifications will work.
1241 tasks
.add(SHOW_ON_START_TASK_NAME
, function() {
1242 fillFromChromeLocalStorage({
1243 /** @type {Object.<string, StoredNotificationGroup>} */
1244 notificationGroups
: {}
1245 }).then(function(items
) {
1246 console
.log('onStartup-get ' + JSON
.stringify(items
));
1248 showNotificationGroups(items
.notificationGroups
).then(function() {
1249 chrome
.storage
.local
.set(items
);
1257 authenticationManager
.addListener(function() {
1258 console
.log('signIn State Change');
1262 instrumented
.notifications
.onClicked
.addListener(
1263 function(chromeNotificationId
) {
1264 chrome
.metricsPrivate
.recordUserAction('GoogleNow.MessageClicked');
1265 onNotificationClicked(chromeNotificationId
,
1266 function(notificationDataEntry
) {
1267 var actionUrls
= notificationDataEntry
.actionUrls
;
1268 var url
= actionUrls
&& actionUrls
.messageUrl
;
1270 recordNotificationClick(notificationDataEntry
.cardTypeId
);
1276 instrumented
.notifications
.onButtonClicked
.addListener(
1277 function(chromeNotificationId
, buttonIndex
) {
1278 chrome
.metricsPrivate
.recordUserAction(
1279 'GoogleNow.ButtonClicked' + buttonIndex
);
1280 onNotificationClicked(chromeNotificationId
,
1281 function(notificationDataEntry
) {
1282 var actionUrls
= notificationDataEntry
.actionUrls
;
1283 var url
= actionUrls
.buttonUrls
[buttonIndex
];
1285 recordButtonClick(notificationDataEntry
.cardTypeId
, buttonIndex
);
1287 verify(false, 'onButtonClicked: no url for a button');
1293 instrumented
.notifications
.onClosed
.addListener(onNotificationClosed
);
1295 instrumented
.notifications
.onPermissionLevelChanged
.addListener(
1296 function(permissionLevel
) {
1297 console
.log('Notifications permissionLevel Change');
1301 instrumented
.notifications
.onShowSettings
.addListener(function() {
1302 openUrl(SETTINGS_URL
);
1305 // Handles state change notifications for the Google Now enabled bit.
1306 instrumented
.storage
.onChanged
.addListener(function(changes
, areaName
) {
1307 if (areaName
=== 'local') {
1308 if ('googleNowEnabled' in changes
) {
1314 instrumented
.pushMessaging
.onMessage
.addListener(function(message
) {
1315 // message.payload will be '' when the extension first starts.
1316 // Each time after signing in, we'll get latest payload for all channels.
1317 // So, we need to poll the server only when the payload is non-empty and has
1319 console
.log('pushMessaging.onMessage ' + JSON
.stringify(message
));
1320 if (message
.payload
.indexOf('REQUEST_CARDS') == 0) {
1321 tasks
.add(ON_PUSH_MESSAGE_START_TASK_NAME
, function() {
1322 // Accept promise rejection on failure since it's safer to do nothing,
1323 // preventing polling the server when the payload really didn't change.
1324 fillFromChromeLocalStorage({
1325 lastPollNowPayloads
: {},
1326 /** @type {Object.<string, StoredNotificationGroup>} */
1327 notificationGroups
: {}
1328 }, PromiseRejection
.ALLOW
).then(function(items
) {
1329 if (items
.lastPollNowPayloads
[message
.subchannelId
] !=
1331 items
.lastPollNowPayloads
[message
.subchannelId
] = message
.payload
;
1333 items
.notificationGroups
['PUSH' + message
.subchannelId
] = {
1335 nextPollTime
: Date
.now()
1338 chrome
.storage
.local
.set({
1339 lastPollNowPayloads
: items
.lastPollNowPayloads
,
1340 notificationGroups
: items
.notificationGroups
1343 pollOptedInWithRecheck();