1 // Copyright 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 * Show/hide trigger in a card.
11 * showTimeSec: (string|undefined),
18 * ID of an individual (uncombined) notification.
19 * This ID comes directly from the server.
23 var ServerNotificationId;
26 * Data to build a dismissal request for a card from a specific group.
29 * notificationId: ServerNotificationId,
36 * Urls that need to be opened when clicking a notification or its buttons.
39 * messageUrl: (string|undefined),
40 * buttonUrls: (Array<string>|undefined)
46 * ID of a combined notification.
47 * This is the ID used with chrome.notifications API.
51 var ChromeNotificationId;
54 * Notification as sent by the server.
57 * notificationId: ServerNotificationId,
58 * chromeNotificationId: ChromeNotificationId,
60 * chromeNotificationOptions: Object,
61 * actionUrls: (ActionUrls|undefined),
63 * locationBased: (boolean|undefined),
65 * cardTypeId: (number|undefined)
68 var ReceivedNotification;
71 * Received notification in a self-sufficient form that doesn't require group's
72 * timestamp to calculate show and hide times.
75 * receivedNotification: ReceivedNotification,
76 * showTime: (number|undefined),
80 var UncombinedNotification;
83 * Card combined from potentially multiple groups.
85 * @typedef {Array<UncombinedNotification>}
90 * Data entry that we store for every Chrome notification.
91 * |timestamp| is the time when corresponding Chrome notification was created or
92 * updated last time by cardSet.update().
95 * actionUrls: (ActionUrls|undefined),
96 * cardTypeId: (number|undefined),
98 * combinedCard: CombinedCard
102 var NotificationDataEntry;
105 * Names for tasks that can be created by the this file.
107 var UPDATE_CARD_TASK_NAME = 'update-card';
110 * Builds an object to manage notification card set.
111 * @return {Object} Card set interface.
113 function buildCardSet() {
114 var alarmPrefix = 'card-';
117 * Creates/updates/deletes a Chrome notification.
118 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID
120 * @param {(ReceivedNotification|undefined)} receivedNotification Google Now
121 * card represented as a set of parameters for showing a Chrome
122 * notification, or null if the notification needs to be deleted.
123 * @param {function(ReceivedNotification)=} onCardShown Optional parameter
124 * called when each card is shown.
126 function updateNotification(
127 chromeNotificationId, receivedNotification, onCardShown) {
129 'cardManager.updateNotification ' + chromeNotificationId + ' ' +
130 JSON.stringify(receivedNotification));
132 if (!receivedNotification) {
133 instrumented.notifications.clear(chromeNotificationId, function() {});
137 // Try updating the notification.
138 instrumented.notifications.update(
139 chromeNotificationId,
140 receivedNotification.chromeNotificationOptions,
141 function(wasUpdated) {
143 // If the notification wasn't updated, it probably didn't exist.
146 'cardManager.updateNotification ' + chromeNotificationId +
147 ' not updated, creating instead');
148 instrumented.notifications.create(
149 chromeNotificationId,
150 receivedNotification.chromeNotificationOptions,
151 function(newChromeNotificationId) {
152 if (!newChromeNotificationId || chrome.runtime.lastError) {
153 var errorMessage = chrome.runtime.lastError &&
154 chrome.runtime.lastError.message;
155 console.error('notifications.create: ID=' +
156 newChromeNotificationId + ', ERROR=' + errorMessage);
160 if (onCardShown !== undefined)
161 onCardShown(receivedNotification);
168 * Iterates uncombined notifications in a combined card, determining for
169 * each whether it's visible at the specified moment.
170 * @param {CombinedCard} combinedCard The combined card in question.
171 * @param {number} timestamp Time for which to calculate visibility.
172 * @param {function(UncombinedNotification, boolean)} callback Function
173 * invoked for every uncombined notification in |combinedCard|.
174 * The boolean parameter indicates whether the uncombined notification is
175 * visible at |timestamp|.
177 function iterateUncombinedNotifications(combinedCard, timestamp, callback) {
178 for (var i = 0; i != combinedCard.length; ++i) {
179 var uncombinedNotification = combinedCard[i];
180 var shouldShow = !uncombinedNotification.showTime ||
181 uncombinedNotification.showTime <= timestamp;
182 var shouldHide = uncombinedNotification.hideTime <= timestamp;
184 callback(uncombinedNotification, shouldShow && !shouldHide);
189 * Refreshes (shows/hides) the notification corresponding to the combined card
190 * based on the current time and show-hide intervals in the combined card.
191 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID
193 * @param {CombinedCard} combinedCard Combined cards with
194 * |chromeNotificationId|.
195 * @param {Object<StoredNotificationGroup>} notificationGroups Map from group
196 * name to group information.
197 * @param {function(ReceivedNotification)=} onCardShown Optional parameter
198 * called when each card is shown.
199 * @return {(NotificationDataEntry|undefined)} Notification data entry for
200 * this card. It's 'undefined' if the card's life is over.
203 chromeNotificationId, combinedCard, notificationGroups, onCardShown) {
204 console.log('cardManager.update ' + JSON.stringify(combinedCard));
206 chrome.alarms.clear(alarmPrefix + chromeNotificationId);
207 var now = Date.now();
208 /** @type {(UncombinedNotification|undefined)} */
209 var winningCard = undefined;
210 // Next moment of time when winning notification selection algotithm can
211 // potentially return a different notification.
212 /** @type {?number} */
213 var nextEventTime = null;
215 // Find a winning uncombined notification: a highest-priority notification
216 // that needs to be shown now.
217 iterateUncombinedNotifications(
220 function(uncombinedCard, visible) {
221 // If the uncombined notification is visible now and set the winning
222 // card to it if its priority is higher.
225 uncombinedCard.receivedNotification.chromeNotificationOptions.
227 winningCard.receivedNotification.chromeNotificationOptions.
229 winningCard = uncombinedCard;
233 // Next event time is the closest hide or show event.
234 if (uncombinedCard.showTime && uncombinedCard.showTime > now) {
235 if (!nextEventTime || nextEventTime > uncombinedCard.showTime)
236 nextEventTime = uncombinedCard.showTime;
238 if (uncombinedCard.hideTime > now) {
239 if (!nextEventTime || nextEventTime > uncombinedCard.hideTime)
240 nextEventTime = uncombinedCard.hideTime;
244 // Show/hide the winning card.
246 chromeNotificationId,
247 winningCard && winningCard.receivedNotification,
251 // If we expect more events, create an alarm for the next one.
252 chrome.alarms.create(
253 alarmPrefix + chromeNotificationId, {when: nextEventTime});
255 // The trick with stringify/parse is to create a copy of action URLs,
256 // otherwise notifications data with 2 pointers to the same object won't
257 // be stored correctly to chrome.storage.
258 var winningActionUrls = winningCard &&
259 winningCard.receivedNotification.actionUrls &&
260 JSON.parse(JSON.stringify(
261 winningCard.receivedNotification.actionUrls));
262 var winningCardTypeId = winningCard &&
263 winningCard.receivedNotification.cardTypeId;
265 actionUrls: winningActionUrls,
266 cardTypeId: winningCardTypeId,
268 combinedCard: combinedCard
271 // If there are no more events, we are done with this card. Note that all
272 // received notifications have hideTime.
273 verify(!winningCard, 'No events left, but card is shown.');
274 clearCardFromGroups(chromeNotificationId, notificationGroups);
280 * Removes dismissed part of a card and refreshes the card. Returns remaining
281 * dismissals for the combined card and updated notification data.
282 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID
284 * @param {NotificationDataEntry} notificationData Stored notification entry
286 * @param {Object<StoredNotificationGroup>} notificationGroups Map from group
287 * name to group information.
289 * dismissals: Array<DismissalData>,
290 * notificationData: (NotificationDataEntry|undefined)
293 function onDismissal(
294 chromeNotificationId, notificationData, notificationGroups) {
295 /** @type {Array<DismissalData>} */
297 /** @type {Array<UncombinedNotification>} */
298 var newCombinedCard = [];
300 // Determine which parts of the combined card need to be dismissed or to be
301 // preserved. We dismiss parts that were visible at the moment when the card
303 iterateUncombinedNotifications(
304 notificationData.combinedCard,
305 notificationData.timestamp,
306 function(uncombinedCard, visible) {
309 notificationId: uncombinedCard.receivedNotification.notificationId,
310 parameters: uncombinedCard.receivedNotification.dismissal
313 newCombinedCard.push(uncombinedCard);
318 dismissals: dismissals,
319 notificationData: update(
320 chromeNotificationId, newCombinedCard, notificationGroups)
325 * Removes card information from |notificationGroups|.
326 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID
328 * @param {Object<StoredNotificationGroup>} notificationGroups Map from group
329 * name to group information.
331 function clearCardFromGroups(chromeNotificationId, notificationGroups) {
332 console.log('cardManager.clearCardFromGroups ' + chromeNotificationId);
333 for (var groupName in notificationGroups) {
334 var group = notificationGroups[groupName];
335 for (var i = 0; i != group.cards.length; ++i) {
336 if (group.cards[i].chromeNotificationId == chromeNotificationId) {
337 group.cards.splice(i, 1);
344 instrumented.alarms.onAlarm.addListener(function(alarm) {
345 console.log('cardManager.onAlarm ' + JSON.stringify(alarm));
347 if (alarm.name.indexOf(alarmPrefix) == 0) {
348 // Alarm to show the card.
349 tasks.add(UPDATE_CARD_TASK_NAME, function() {
350 /** @type {ChromeNotificationId} */
351 var chromeNotificationId = alarm.name.substring(alarmPrefix.length);
352 fillFromChromeLocalStorage({
353 /** @type {Object<ChromeNotificationId, NotificationDataEntry>} */
354 notificationsData: {},
355 /** @type {Object<StoredNotificationGroup>} */
356 notificationGroups: {}
357 }).then(function(items) {
358 console.log('cardManager.onAlarm.get ' + JSON.stringify(items));
361 (items.notificationsData[chromeNotificationId] &&
362 items.notificationsData[chromeNotificationId].combinedCard) || [];
364 var cardShownCallback = undefined;
365 if (localStorage['explanatoryCardsShown'] <
366 EXPLANATORY_CARDS_LINK_THRESHOLD) {
367 cardShownCallback = countExplanatoryCard;
370 items.notificationsData[chromeNotificationId] =
372 chromeNotificationId,
374 items.notificationGroups,
377 chrome.storage.local.set(items);
385 onDismissal: onDismissal