OLD | NEW |
---|---|
1 // Copyright 2013 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 'use strict'; | 5 'use strict'; |
6 | 6 |
7 /** | 7 /** |
8 * Show/hide trigger in a card. | 8 * Show/hide trigger in a card. |
9 * | 9 * |
10 * @typedef {{ | 10 * @typedef {{ |
11 * showTime: number=, | 11 * showTimeSec: (string|undefined), |
12 * hideTime: number= | 12 * hideTimeSec: string |
13 * }} | 13 * }} |
14 */ | 14 */ |
15 var Trigger; | 15 var Trigger; |
16 | 16 |
17 /** | 17 /** |
18 * ID of an individual (uncombined) notification. | |
19 * | |
20 * @typedef {string} | |
21 */ | |
22 var NotificationId; | |
23 | |
24 /** | |
18 * Data to build a dismissal request for a card from a specific group. | 25 * Data to build a dismissal request for a card from a specific group. |
19 * | 26 * |
20 * @typedef {{ | 27 * @typedef {{ |
21 * notificationId: string, | 28 * notificationId: NotificationId, |
22 * parameters: Object | 29 * parameters: Object |
23 * }} | 30 * }} |
24 */ | 31 */ |
25 var DismissalData; | 32 var DismissalData; |
26 | 33 |
27 /** | 34 /** |
28 * Card merged from potentially multiple groups. | 35 * Urls that need to be opened when clicking a notification or its buttons. |
29 * | 36 * |
30 * @typedef {{ | 37 * @typedef {{ |
38 * messageUrl: (string|undefined), | |
39 * buttonUrls: (Array.<string>|undefined) | |
40 * }} | |
41 */ | |
42 var ActionUrls; | |
43 | |
44 /** | |
45 * ID of a combined notification. This is the ID used with chrome.notifications | |
46 * API. | |
47 * | |
48 * @typedef {string} | |
49 */ | |
50 var ChromeNotificationId; | |
51 | |
52 /** | |
53 * Notification as sent by the server. | |
54 * | |
55 * @typedef {{ | |
56 * notificationId: NotificationId, | |
57 * chromeNotificationId: ChromeNotificationId, | |
31 * trigger: Trigger, | 58 * trigger: Trigger, |
32 * version: number, | 59 * version: number, |
60 * chromeNotificationOptions: Object, | |
61 * actionUrls: (ActionUrls|undefined), | |
62 * dismissal: Object, | |
63 * locationBased: (boolean|undefined), | |
64 * groupName: string | |
65 * }} | |
66 */ | |
67 var ReceivedNotification; | |
68 | |
69 /** | |
70 * Received notification in a self-sufficient form that doesn't require group's | |
71 * timestamp to calculate show and hide times. | |
72 * | |
73 * @typedef {{ | |
74 * receivedNotification: ReceivedNotification, | |
75 * showTime: (number|undefined), | |
76 * hideTime: number | |
77 * }} | |
78 */ | |
79 var UncombinedNotification; | |
80 | |
81 /** | |
82 * Card combined from potentially multiple groups. | |
83 * | |
84 * @typedef {Array.<UncombinedNotification>} | |
85 */ | |
86 var CombinedCard; | |
87 | |
88 /** | |
89 * Data entry that we store for every Chrome notification. | |
90 * |timestamp| is the time when corresponding Chrome notification was created or | |
91 * updated last time by cardSet.update(). | |
92 * | |
93 * @typedef {{ | |
94 * actionUrls: (ActionUrls|undefined), | |
33 * timestamp: number, | 95 * timestamp: number, |
34 * notification: Object, | 96 * combinedCard: CombinedCard |
35 * actionUrls: Object=, | 97 * }} |
36 * groupRank: number, | 98 * |
37 * dismissals: Array.<DismissalData>, | 99 */ |
38 * locationBased: boolean= | 100 var NotificationDataEntry; |
39 * }} | |
40 */ | |
41 var MergedCard; | |
42 | |
43 /** | |
44 * Set of parameters for creating card notification. | |
45 * | |
46 * @typedef {{ | |
47 * notification: Object, | |
48 * hideTime: number=, | |
49 * version: number, | |
50 * previousVersion: number=, | |
51 * locationBased: boolean= | |
52 * }} | |
53 */ | |
54 var CardCreateInfo; | |
55 | 101 |
56 /** | 102 /** |
57 * Names for tasks that can be created by the this file. | 103 * Names for tasks that can be created by the this file. |
58 */ | 104 */ |
59 var SHOW_CARD_TASK_NAME = 'show-card'; | 105 var UPDATE_CARD_TASK_NAME = 'update-card'; |
60 var CLEAR_CARD_TASK_NAME = 'clear-card'; | |
61 | 106 |
62 /** | 107 /** |
63 * Builds an object to manage notification card set. | 108 * Builds an object to manage notification card set. |
64 * @return {Object} Card set interface. | 109 * @return {Object} Card set interface. |
65 */ | 110 */ |
66 function buildCardSet() { | 111 function buildCardSet() { |
67 var cardShowPrefix = 'card-show-'; | 112 var alarmPrefix = 'card-'; |
68 var cardHidePrefix = 'card-hide-'; | 113 |
69 | 114 /** |
70 /** | 115 * Creates/updates/deletes a Chrome notification. |
71 * Schedules hiding a notification. | 116 * @param {ChromeNotificationId} cardId Card ID. |
72 * @param {string} cardId Card ID. | 117 * @param {?ReceivedNotification} receivedNotification Google Now card |
73 * @param {number=} opt_timeHide If specified, epoch time to hide the card. If | 118 * represented as a set of parameters for showing a Chrome notification, |
74 * undefined, the card will be kept shown at least until next update. | 119 * or null if the notification needs to be deleted. |
75 */ | 120 * @param {function(ReceivedNotification)=} onCardShown Optional parameter |
76 function scheduleHiding(cardId, opt_timeHide) { | 121 * called when each card is shown. |
77 if (opt_timeHide === undefined) | 122 */ |
78 return; | 123 function updateNotification(cardId, receivedNotification, onCardShown) { |
79 | 124 console.log('cardManager.updateNotification ' + cardId + ' ' + |
80 var alarmName = cardHidePrefix + cardId; | 125 JSON.stringify(receivedNotification)); |
81 var alarmInfo = {when: opt_timeHide}; | 126 |
82 chrome.alarms.create(alarmName, alarmInfo); | 127 if (!receivedNotification) { |
83 } | 128 instrumented.notifications.clear(cardId, function() {}); |
84 | |
85 /** | |
86 * Shows a notification. | |
87 * @param {string} cardId Card ID. | |
88 * @param {CardCreateInfo} cardCreateInfo Google Now card represented as a set | |
89 * of parameters for showing a Chrome notification. | |
90 * @param {function(CardCreateInfo)=} onCardShown Optional parameter called | |
91 * when each card is shown. | |
92 */ | |
93 function showNotification(cardId, cardCreateInfo, onCardShown) { | |
94 console.log('cardManager.showNotification ' + cardId + ' ' + | |
95 JSON.stringify(cardCreateInfo)); | |
96 | |
97 if (cardCreateInfo.hideTime <= Date.now()) { | |
98 console.log('cardManager.showNotification ' + cardId + ': expired'); | |
99 // Card has expired. Schedule hiding to delete asociated information. | |
100 scheduleHiding(cardId, cardCreateInfo.hideTime); | |
101 return; | 129 return; |
102 } | 130 } |
103 | 131 |
104 if (cardCreateInfo.previousVersion !== cardCreateInfo.version) { | 132 // Try updating the notification. |
rgustafson
2013/12/06 23:35:16
So the only way to make a notification retoast now
vadimt
2013/12/06 23:52:58
--- Correct
| |
105 // Delete a notification with the specified id if it already exists, and | 133 instrumented.notifications.update( |
106 // then create a notification. | 134 cardId, |
107 instrumented.notifications.create( | 135 receivedNotification.chromeNotificationOptions, |
108 cardId, | 136 function(wasUpdated) { |
109 cardCreateInfo.notification, | 137 if (!wasUpdated) { |
110 function(newNotificationId) { | 138 // If the notification wasn't updated, it probably didn't exist. |
111 if (!newNotificationId || chrome.runtime.lastError) { | 139 // Create it. |
112 var errorMessage = chrome.runtime.lastError && | 140 console.log('cardManager.updateNotification ' + cardId + |
113 chrome.runtime.lastError.message; | 141 ' failed to update, creating'); |
114 console.error('notifications.create: ID=' + newNotificationId + | 142 instrumented.notifications.create( |
115 ', ERROR=' + errorMessage); | 143 cardId, |
116 return; | 144 receivedNotification.chromeNotificationOptions, |
117 } | 145 function(newNotificationId) { |
118 | 146 if (!newNotificationId || chrome.runtime.lastError) { |
119 if (onCardShown !== undefined) | 147 var errorMessage = chrome.runtime.lastError && |
120 onCardShown(cardCreateInfo); | 148 chrome.runtime.lastError.message; |
121 | 149 console.error('notifications.create: ID=' + |
122 scheduleHiding(cardId, cardCreateInfo.hideTime); | 150 newNotificationId + ', ERROR=' + errorMessage); |
151 return; | |
152 } | |
153 | |
154 if (onCardShown !== undefined) | |
155 onCardShown(receivedNotification); | |
156 }); | |
157 } | |
158 }); | |
159 } | |
160 | |
161 /** | |
162 * Enumerates uncombined notifications in a combined card, determining for | |
rgustafson
2013/12/06 23:35:16
Enumerating implies listing everything. A list is
vadimt
2013/12/06 23:52:58
Done.
| |
163 * each whether it's visible at the specified moment. | |
164 * @param {CombinedCard} combinedCard The combined card in question. | |
165 * @param {number} timestamp Time for which to calculate visibility. | |
166 * @param {function(UncombinedNotification, boolean)} callback Function | |
167 * invoked for every uncombined notification in |combinedCard|. | |
168 * The boolean parameter indicates whether the uncombined notification is | |
169 * visible at |timestamp|. | |
170 */ | |
171 function enumerateUncombinedNotifications(combinedCard, timestamp, callback) { | |
172 for (var i = 0; i != combinedCard.length; ++i) { | |
173 var uncombinedNotification = combinedCard[i]; | |
174 var shouldShow = !uncombinedNotification.showTime || | |
175 uncombinedNotification.showTime <= timestamp; | |
176 var shouldHide = uncombinedNotification.hideTime <= timestamp; | |
177 | |
178 callback(uncombinedNotification, shouldShow && !shouldHide); | |
179 } | |
180 } | |
181 | |
182 /** | |
183 * Refreshes (shows/hides) the notification corresponding to the combined card | |
184 * based on the current time and show-hide intervals in the combined card. | |
185 * @param {ChromeNotificationId} cardId Card ID. | |
186 * @param {CombinedCard} combinedCard Combined cards with |cardId|. | |
187 * @param {function(ReceivedNotification)=} onCardShown Optional parameter | |
188 * called when each card is shown. | |
189 * @return {(NotificationDataEntry|undefined)} Notification data entry for | |
190 * this card. It's 'undefined' if the card's life is over. | |
191 */ | |
192 function update(cardId, combinedCard, onCardShown) { | |
193 console.log('cardManager.update ' + JSON.stringify(combinedCard)); | |
194 | |
195 chrome.alarms.clear(alarmPrefix + cardId); | |
196 var now = Date.now(); | |
197 /** @type {?UncombinedNotification} */ | |
198 var winningCard = null; | |
199 // Next moment of time when winning notification selection algotithm can | |
200 // potentially return a different notification. | |
201 /** @type {?number} */ | |
202 var nextEventTime = null; | |
203 | |
204 // Find a winning uncombined notification: a highest-priority notification | |
205 // that needs to be shown now. | |
206 enumerateUncombinedNotifications( | |
207 combinedCard, now, function(uncombinedCard, visible) { | |
208 // If the uncombined notification is visible now and set the winning card | |
rgustafson
2013/12/06 23:35:16
Is this the right tab level for a function that is
vadimt
2013/12/06 23:52:58
Done.
| |
209 // to it if its priority is higher. | |
210 if (visible) { | |
211 if (!winningCard || | |
212 uncombinedCard.receivedNotification.chromeNotificationOptions. | |
213 priority > | |
214 winningCard.receivedNotification.chromeNotificationOptions. | |
215 priority) { | |
216 winningCard = uncombinedCard; | |
217 } | |
218 } | |
219 | |
220 // Next event time is the closest hide or show event. | |
221 if (uncombinedCard.showTime && uncombinedCard.showTime > now) { | |
222 if (!nextEventTime || nextEventTime > uncombinedCard.showTime) | |
223 nextEventTime = uncombinedCard.showTime; | |
224 } | |
225 if (uncombinedCard.hideTime > now) { | |
226 if (!nextEventTime || nextEventTime > uncombinedCard.hideTime) | |
227 nextEventTime = uncombinedCard.hideTime; | |
228 } | |
229 }); | |
230 | |
231 // Show/hide the winning card. | |
232 updateNotification( | |
233 cardId, winningCard && winningCard.receivedNotification, onCardShown); | |
234 | |
235 if (nextEventTime) { | |
236 // If we expect more events, create an alarm for the next one. | |
237 chrome.alarms.create(alarmPrefix + cardId, {when: nextEventTime}); | |
238 | |
239 // The trick with stringify/parse is to create a copy of action URLs, | |
240 // otherwise notifications data with 2 pointers to the same object won't | |
241 // be stored correctly to chrome.storage. | |
242 var winningActionUrls = winningCard && | |
243 JSON.parse(JSON.stringify( | |
244 winningCard.receivedNotification.actionUrls)); | |
245 | |
246 return { | |
247 actionUrls: winningActionUrls, | |
248 timestamp: now, | |
249 combinedCard: combinedCard | |
250 }; | |
251 } else { | |
252 // If there are no more events, we are done with this card. | |
253 verify(!winningCard, 'No events left, but card is shown.'); | |
254 clearCardFromGroups(cardId); | |
255 return undefined; | |
256 } | |
257 } | |
258 | |
259 /** | |
260 * Removes dismissed part of a card and refreshes the card. Returns remaining | |
261 * dismissals for the combined card and updated notification data. | |
262 * @param {ChromeNotificationId} cardId Card ID. | |
263 * @param {NotificationDataEntry} notificationData Stored notification entry | |
264 * for this card. | |
265 * @return {{ | |
266 * dismissals: Array.<DismissalData>, | |
267 * notificationData: (NotificationDataEntry|undefined) | |
268 * }} | |
269 */ | |
270 function onDismissal(cardId, notificationData) { | |
271 var dismissals = []; | |
272 var newCombinedCard = []; | |
273 | |
274 // Determine which parts of the combined card need to be dismissed or to be | |
275 // preserved. We dismiss parts that were visible at the moment when the card | |
276 // was last time updated. | |
rgustafson
2013/12/06 23:35:16
was last updated
vadimt
2013/12/06 23:52:58
Done.
| |
277 enumerateUncombinedNotifications( | |
278 notificationData.combinedCard, | |
279 notificationData.timestamp, | |
280 function(uncombinedCard, visible) { | |
281 if (visible) { | |
282 dismissals.push({ | |
283 notificationId: uncombinedCard.receivedNotification.notificationId, | |
284 parameters: uncombinedCard.receivedNotification.dismissal | |
123 }); | 285 }); |
124 } else { | 286 } else { |
125 // Update existing notification. | 287 newCombinedCard.push(uncombinedCard); |
126 instrumented.notifications.update( | 288 } |
127 cardId, | 289 }); |
128 cardCreateInfo.notification, | 290 |
129 function(wasUpdated) { | 291 return { |
130 if (!wasUpdated || chrome.runtime.lastError) { | 292 dismissals: dismissals, |
131 var errorMessage = chrome.runtime.lastError && | 293 notificationData: update(cardId, newCombinedCard) |
132 chrome.runtime.lastError.message; | |
133 console.error('notifications.update: UPDATED=' + wasUpdated + | |
134 ', ERROR=' + errorMessage); | |
135 return; | |
136 } | |
137 | |
138 scheduleHiding(cardId, cardCreateInfo.hideTime); | |
139 }); | |
140 } | |
141 } | |
142 | |
143 /** | |
144 * Updates/creates a card notification with new data. | |
145 * @param {string} cardId Card ID. | |
146 * @param {MergedCard} card Google Now card from the server. | |
147 * @param {number=} previousVersion The version of the shown card with | |
148 * this id, if it exists, undefined otherwise. | |
149 * @param {function(CardCreateInfo)=} onCardShown Optional parameter called | |
150 * when each card is shown. | |
151 * @return {Object} Notification data entry for this card. | |
152 */ | |
153 function update(cardId, card, previousVersion, onCardShown) { | |
154 console.log('cardManager.update ' + JSON.stringify(card) + ' ' + | |
155 previousVersion); | |
156 | |
157 chrome.alarms.clear(cardHidePrefix + cardId); | |
158 | |
159 var cardCreateInfo = { | |
160 notification: card.notification, | |
161 hideTime: card.trigger.hideTime, | |
162 version: card.version, | |
163 previousVersion: previousVersion, | |
164 locationBased: card.locationBased | |
165 }; | 294 }; |
166 | 295 } |
167 var shownImmediately = false; | 296 |
168 var cardShowAlarmName = cardShowPrefix + cardId; | 297 /** |
169 if (card.trigger.showTime && card.trigger.showTime > Date.now()) { | 298 * Removes a card information from 'notificationGroups'. |
rgustafson
2013/12/06 23:35:16
Removes card information
vadimt
2013/12/06 23:52:58
Done.
| |
170 // Card needs to be shown later. | 299 * @param {ChromeNotificationId} cardId Card ID. |
171 console.log('cardManager.update: postponed'); | 300 */ |
172 var alarmInfo = { | 301 function clearCardFromGroups(cardId) { |
173 when: card.trigger.showTime | 302 console.log('cardManager.clearCardFromGroups ' + cardId); |
174 }; | 303 |
175 chrome.alarms.create(cardShowAlarmName, alarmInfo); | 304 instrumented.storage.local.get('notificationGroups', function(items) { |
176 } else { | 305 items = items || {}; |
177 // Card needs to be shown immediately. | 306 /** @type {Object.<string, StoredNotificationGroup>} */ |
178 console.log('cardManager.update: immediate'); | 307 items.notificationGroups = items.notificationGroups || {}; |
179 chrome.alarms.clear(cardShowAlarmName); | 308 |
180 showNotification(cardId, cardCreateInfo, onCardShown); | 309 for (var groupName in items.notificationGroups) { |
181 } | 310 var group = items.notificationGroups[groupName]; |
182 | 311 for (var i = 0; i != group.cards.length; ++i) { |
183 return { | 312 if (group.cards[i].chromeNotificationId == cardId) { |
184 actionUrls: card.actionUrls, | 313 group.cards.splice(i, 1); |
185 cardCreateInfo: cardCreateInfo, | 314 break; |
186 dismissals: card.dismissals | 315 } |
187 }; | 316 } |
188 } | 317 } |
189 | 318 |
190 /** | 319 chrome.storage.local.set(items); |
191 * Removes a card notification. | 320 }); |
192 * @param {string} cardId Card ID. | |
193 * @param {boolean} clearStorage True if the information associated with the | |
194 * card should be erased from chrome.storage. | |
195 */ | |
196 function clear(cardId, clearStorage) { | |
197 console.log('cardManager.clear ' + cardId); | |
198 | |
199 chrome.notifications.clear(cardId, function() {}); | |
200 chrome.alarms.clear(cardShowPrefix + cardId); | |
201 chrome.alarms.clear(cardHidePrefix + cardId); | |
202 | |
203 if (clearStorage) { | |
204 instrumented.storage.local.get( | |
205 ['notificationsData', 'notificationGroups'], | |
206 function(items) { | |
207 items = items || {}; | |
208 items.notificationsData = items.notificationsData || {}; | |
209 items.notificationGroups = items.notificationGroups || {}; | |
210 | |
211 delete items.notificationsData[cardId]; | |
212 | |
213 for (var groupName in items.notificationGroups) { | |
214 var group = items.notificationGroups[groupName]; | |
215 for (var i = 0; i != group.cards.length; ++i) { | |
216 if (group.cards[i].chromeNotificationId == cardId) { | |
217 group.cards.splice(i, 1); | |
218 break; | |
219 } | |
220 } | |
221 } | |
222 | |
223 chrome.storage.local.set(items); | |
224 }); | |
225 } | |
226 } | 321 } |
227 | 322 |
228 instrumented.alarms.onAlarm.addListener(function(alarm) { | 323 instrumented.alarms.onAlarm.addListener(function(alarm) { |
229 console.log('cardManager.onAlarm ' + JSON.stringify(alarm)); | 324 console.log('cardManager.onAlarm ' + JSON.stringify(alarm)); |
230 | 325 |
231 if (alarm.name.indexOf(cardShowPrefix) == 0) { | 326 if (alarm.name.indexOf(alarmPrefix) == 0) { |
232 // Alarm to show the card. | 327 // Alarm to show the card. |
233 tasks.add(SHOW_CARD_TASK_NAME, function() { | 328 tasks.add(UPDATE_CARD_TASK_NAME, function() { |
234 var cardId = alarm.name.substring(cardShowPrefix.length); | 329 var cardId = alarm.name.substring(alarmPrefix.length); |
235 instrumented.storage.local.get('notificationsData', function(items) { | 330 instrumented.storage.local.get('notificationsData', function(items) { |
236 console.log('cardManager.onAlarm.get ' + JSON.stringify(items)); | 331 console.log('cardManager.onAlarm.get ' + JSON.stringify(items)); |
237 if (!items || !items.notificationsData) | 332 items = items || {}; |
238 return; | 333 /** @type {Object.<string, NotificationDataEntry>} */ |
239 var notificationData = items.notificationsData[cardId]; | 334 items.notificationsData = items.notificationsData || {}; |
240 if (!notificationData) | 335 var combinedCard = |
241 return; | 336 (items.notificationsData[cardId] && |
337 items.notificationsData[cardId].combinedCard) || []; | |
242 | 338 |
243 var cardShownCallback = undefined; | 339 var cardShownCallback = undefined; |
244 if (localStorage['locationCardsShown'] < | 340 if (localStorage['locationCardsShown'] < |
245 LOCATION_CARDS_LINK_THRESHOLD) { | 341 LOCATION_CARDS_LINK_THRESHOLD) { |
246 cardShownCallback = countLocationCard; | 342 cardShownCallback = countLocationCard; |
247 } | 343 } |
248 | 344 |
249 showNotification( | 345 items.notificationsData[cardId] = |
250 cardId, notificationData.cardCreateInfo, cardShownCallback); | 346 update(cardId, combinedCard, cardShownCallback); |
347 | |
348 chrome.storage.local.set(items); | |
251 }); | 349 }); |
252 }); | 350 }); |
253 } else if (alarm.name.indexOf(cardHidePrefix) == 0) { | |
254 // Alarm to hide the card. | |
255 tasks.add(CLEAR_CARD_TASK_NAME, function() { | |
256 var cardId = alarm.name.substring(cardHidePrefix.length); | |
257 clear(cardId, true); | |
258 }); | |
259 } | 351 } |
260 }); | 352 }); |
261 | 353 |
262 return { | 354 return { |
263 update: update, | 355 update: update, |
264 clear: clear | 356 onDismissal: onDismissal |
265 }; | 357 }; |
266 } | 358 } |
OLD | NEW |