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.
25 * Data to build a dismissal request for a card from a specific group.
28 * notificationId: NotificationId,
35 * Urls that need to be opened when clicking a notification or its buttons.
38 * messageUrl: (string|undefined),
39 * buttonUrls: (Array.<string>|undefined)
45 * ID of a combined notification. This is the ID used with chrome.notifications
50 var ChromeNotificationId;
53 * Notification as sent by the server.
56 * notificationId: NotificationId,
57 * chromeNotificationId: ChromeNotificationId,
59 * chromeNotificationOptions: Object,
60 * actionUrls: (ActionUrls|undefined),
62 * locationBased: (boolean|undefined),
66 var ReceivedNotification;
69 * Received notification in a self-sufficient form that doesn't require group's
70 * timestamp to calculate show and hide times.
73 * receivedNotification: ReceivedNotification,
74 * showTime: (number|undefined),
78 var UncombinedNotification;
81 * Card combined from potentially multiple groups.
83 * @typedef {Array.<UncombinedNotification>}
88 * Data entry that we store for every Chrome notification.
89 * |timestamp| is the time when corresponding Chrome notification was created or
90 * updated last time by cardSet.update().
93 * actionUrls: (ActionUrls|undefined),
95 * combinedCard: CombinedCard
99 var NotificationDataEntry;
102 * Names for tasks that can be created by the this file.
104 var UPDATE_CARD_TASK_NAME = 'update-card';
107 * Builds an object to manage notification card set.
108 * @return {Object} Card set interface.
110 function buildCardSet() {
111 var alarmPrefix = 'card-';
114 * Creates/updates/deletes a Chrome notification.
115 * @param {ChromeNotificationId} cardId Card ID.
116 * @param {(ReceivedNotification|undefined)} receivedNotification Google Now
117 * card represented as a set of parameters for showing a Chrome
118 * notification, or null if the notification needs to be deleted.
119 * @param {function(ReceivedNotification)=} onCardShown Optional parameter
120 * called when each card is shown.
122 function updateNotification(cardId, receivedNotification, onCardShown) {
123 console.log('cardManager.updateNotification ' + cardId + ' ' +
124 JSON.stringify(receivedNotification));
126 if (!receivedNotification) {
127 instrumented.notifications.clear(cardId, function() {});
131 // Try updating the notification.
132 instrumented.notifications.update(
134 receivedNotification.chromeNotificationOptions,
135 function(wasUpdated) {
137 // If the notification wasn't updated, it probably didn't exist.
139 console.log('cardManager.updateNotification ' + cardId +
140 ' failed to update, creating');
141 instrumented.notifications.create(
143 receivedNotification.chromeNotificationOptions,
144 function(newNotificationId) {
145 if (!newNotificationId || chrome.runtime.lastError) {
146 var errorMessage = chrome.runtime.lastError &&
147 chrome.runtime.lastError.message;
148 console.error('notifications.create: ID=' +
149 newNotificationId + ', ERROR=' + errorMessage);
153 if (onCardShown !== undefined)
154 onCardShown(receivedNotification);
161 * Iterates uncombined notifications in a combined card, determining for
162 * each whether it's visible at the specified moment.
163 * @param {CombinedCard} combinedCard The combined card in question.
164 * @param {number} timestamp Time for which to calculate visibility.
165 * @param {function(UncombinedNotification, boolean)} callback Function
166 * invoked for every uncombined notification in |combinedCard|.
167 * The boolean parameter indicates whether the uncombined notification is
168 * visible at |timestamp|.
170 function iterateUncombinedNotifications(combinedCard, timestamp, callback) {
171 for (var i = 0; i != combinedCard.length; ++i) {
172 var uncombinedNotification = combinedCard[i];
173 var shouldShow = !uncombinedNotification.showTime ||
174 uncombinedNotification.showTime <= timestamp;
175 var shouldHide = uncombinedNotification.hideTime <= timestamp;
177 callback(uncombinedNotification, shouldShow && !shouldHide);
182 * Refreshes (shows/hides) the notification corresponding to the combined card
183 * based on the current time and show-hide intervals in the combined card.
184 * @param {ChromeNotificationId} cardId Card ID.
185 * @param {CombinedCard} combinedCard Combined cards with |cardId|.
186 * @param {Object.<string, StoredNotificationGroup>} notificationGroups
187 * Map from group name to group information.
188 * @param {function(ReceivedNotification)=} onCardShown Optional parameter
189 * called when each card is shown.
190 * @return {(NotificationDataEntry|undefined)} Notification data entry for
191 * this card. It's 'undefined' if the card's life is over.
193 function update(cardId, combinedCard, notificationGroups, onCardShown) {
194 console.log('cardManager.update ' + JSON.stringify(combinedCard));
196 chrome.alarms.clear(alarmPrefix + cardId);
197 var now = Date.now();
198 /** @type {(UncombinedNotification|undefined)} */
199 var winningCard = undefined;
200 // Next moment of time when winning notification selection algotithm can
201 // potentially return a different notification.
202 /** @type {?number} */
203 var nextEventTime = null;
205 // Find a winning uncombined notification: a highest-priority notification
206 // that needs to be shown now.
207 iterateUncombinedNotifications(
210 function(uncombinedCard, visible) {
211 // If the uncombined notification is visible now and set the winning
212 // card to it if its priority is higher.
215 uncombinedCard.receivedNotification.chromeNotificationOptions.
217 winningCard.receivedNotification.chromeNotificationOptions.
219 winningCard = uncombinedCard;
223 // Next event time is the closest hide or show event.
224 if (uncombinedCard.showTime && uncombinedCard.showTime > now) {
225 if (!nextEventTime || nextEventTime > uncombinedCard.showTime)
226 nextEventTime = uncombinedCard.showTime;
228 if (uncombinedCard.hideTime > now) {
229 if (!nextEventTime || nextEventTime > uncombinedCard.hideTime)
230 nextEventTime = uncombinedCard.hideTime;
234 // Show/hide the winning card.
236 cardId, winningCard && winningCard.receivedNotification, onCardShown);
239 // If we expect more events, create an alarm for the next one.
240 chrome.alarms.create(alarmPrefix + cardId, {when: nextEventTime});
242 // The trick with stringify/parse is to create a copy of action URLs,
243 // otherwise notifications data with 2 pointers to the same object won't
244 // be stored correctly to chrome.storage.
245 var winningActionUrls = winningCard &&
246 winningCard.receivedNotification.actionUrls &&
247 JSON.parse(JSON.stringify(
248 winningCard.receivedNotification.actionUrls));
251 actionUrls: winningActionUrls,
253 combinedCard: combinedCard
256 // If there are no more events, we are done with this card. Note that all
257 // received notifications have hideTime.
258 verify(!winningCard, 'No events left, but card is shown.');
259 clearCardFromGroups(cardId, notificationGroups);
265 * Removes dismissed part of a card and refreshes the card. Returns remaining
266 * dismissals for the combined card and updated notification data.
267 * @param {ChromeNotificationId} cardId Card ID.
268 * @param {NotificationDataEntry} notificationData Stored notification entry
270 * @param {Object.<string, StoredNotificationGroup>} notificationGroups
271 * Map from group name to group information.
273 * dismissals: Array.<DismissalData>,
274 * notificationData: (NotificationDataEntry|undefined)
277 function onDismissal(cardId, notificationData, notificationGroups) {
279 var newCombinedCard = [];
281 // Determine which parts of the combined card need to be dismissed or to be
282 // preserved. We dismiss parts that were visible at the moment when the card
284 iterateUncombinedNotifications(
285 notificationData.combinedCard,
286 notificationData.timestamp,
287 function(uncombinedCard, visible) {
290 notificationId: uncombinedCard.receivedNotification.notificationId,
291 parameters: uncombinedCard.receivedNotification.dismissal
294 newCombinedCard.push(uncombinedCard);
299 dismissals: dismissals,
300 notificationData: update(cardId, newCombinedCard, notificationGroups)
305 * Removes card information from |notificationGroups|.
306 * @param {ChromeNotificationId} cardId Card ID.
307 * @param {Object.<string, StoredNotificationGroup>} notificationGroups
308 * Map from group name to group information.
310 function clearCardFromGroups(cardId, notificationGroups) {
311 console.log('cardManager.clearCardFromGroups ' + cardId);
312 for (var groupName in notificationGroups) {
313 var group = notificationGroups[groupName];
314 for (var i = 0; i != group.cards.length; ++i) {
315 if (group.cards[i].chromeNotificationId == cardId) {
316 group.cards.splice(i, 1);
323 instrumented.alarms.onAlarm.addListener(function(alarm) {
324 console.log('cardManager.onAlarm ' + JSON.stringify(alarm));
326 if (alarm.name.indexOf(alarmPrefix) == 0) {
327 // Alarm to show the card.
328 tasks.add(UPDATE_CARD_TASK_NAME, function() {
329 var cardId = alarm.name.substring(alarmPrefix.length);
330 instrumented.storage.local.get(
331 ['notificationsData', 'notificationGroups'],
333 console.log('cardManager.onAlarm.get ' + JSON.stringify(items));
335 /** @type {Object.<string, NotificationDataEntry>} */
336 items.notificationsData = items.notificationsData || {};
337 /** @type {Object.<string, StoredNotificationGroup>} */
338 items.notificationGroups = items.notificationGroups || {};
341 (items.notificationsData[cardId] &&
342 items.notificationsData[cardId].combinedCard) || [];
344 var cardShownCallback = undefined;
345 if (localStorage['locationCardsShown'] <
346 LOCATION_CARDS_LINK_THRESHOLD) {
347 cardShownCallback = countLocationCard;
350 items.notificationsData[cardId] =
354 items.notificationGroups,
357 chrome.storage.local.set(items);
365 onDismissal: onDismissal