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