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. 4 5'use strict'; 6 7/** 8 * Show/hide trigger in a card. 9 * 10 * @typedef {{ 11 * showTimeSec: (string|undefined), 12 * hideTimeSec: string 13 * }} 14 */ 15var Trigger; 16 17/** 18 * ID of an individual (uncombined) notification. 19 * 20 * @typedef {string} 21 */ 22var NotificationId; 23 24/** 25 * Data to build a dismissal request for a card from a specific group. 26 * 27 * @typedef {{ 28 * notificationId: NotificationId, 29 * parameters: Object 30 * }} 31 */ 32var DismissalData; 33 34/** 35 * Urls that need to be opened when clicking a notification or its buttons. 36 * 37 * @typedef {{ 38 * messageUrl: (string|undefined), 39 * buttonUrls: (Array.<string>|undefined) 40 * }} 41 */ 42var ActionUrls; 43 44/** 45 * ID of a combined notification. This is the ID used with chrome.notifications 46 * API. 47 * 48 * @typedef {string} 49 */ 50var ChromeNotificationId; 51 52/** 53 * Notification as sent by the server. 54 * 55 * @typedef {{ 56 * notificationId: NotificationId, 57 * chromeNotificationId: ChromeNotificationId, 58 * trigger: Trigger, 59 * chromeNotificationOptions: Object, 60 * actionUrls: (ActionUrls|undefined), 61 * dismissal: Object, 62 * locationBased: (boolean|undefined), 63 * groupName: string 64 * }} 65 */ 66var ReceivedNotification; 67 68/** 69 * Received notification in a self-sufficient form that doesn't require group's 70 * timestamp to calculate show and hide times. 71 * 72 * @typedef {{ 73 * receivedNotification: ReceivedNotification, 74 * showTime: (number|undefined), 75 * hideTime: number 76 * }} 77 */ 78var UncombinedNotification; 79 80/** 81 * Card combined from potentially multiple groups. 82 * 83 * @typedef {Array.<UncombinedNotification>} 84 */ 85var CombinedCard; 86 87/** 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(). 91 * 92 * @typedef {{ 93 * actionUrls: (ActionUrls|undefined), 94 * timestamp: number, 95 * combinedCard: CombinedCard 96 * }} 97 * 98 */ 99 var NotificationDataEntry; 100 101/** 102 * Names for tasks that can be created by the this file. 103 */ 104var UPDATE_CARD_TASK_NAME = 'update-card'; 105 106/** 107 * Builds an object to manage notification card set. 108 * @return {Object} Card set interface. 109 */ 110function buildCardSet() { 111 var alarmPrefix = 'card-'; 112 113 /** 114 * Creates/updates/deletes a Chrome notification. 115 * @param {ChromeNotificationId} cardId Card ID. 116 * @param {?ReceivedNotification} receivedNotification Google Now card 117 * represented as a set of parameters for showing a Chrome notification, 118 * or null if the notification needs to be deleted. 119 * @param {function(ReceivedNotification)=} onCardShown Optional parameter 120 * called when each card is shown. 121 */ 122 function updateNotification(cardId, receivedNotification, onCardShown) { 123 console.log('cardManager.updateNotification ' + cardId + ' ' + 124 JSON.stringify(receivedNotification)); 125 126 if (!receivedNotification) { 127 instrumented.notifications.clear(cardId, function() {}); 128 return; 129 } 130 131 // Try updating the notification. 132 instrumented.notifications.update( 133 cardId, 134 receivedNotification.chromeNotificationOptions, 135 function(wasUpdated) { 136 if (!wasUpdated) { 137 // If the notification wasn't updated, it probably didn't exist. 138 // Create it. 139 console.log('cardManager.updateNotification ' + cardId + 140 ' failed to update, creating'); 141 instrumented.notifications.create( 142 cardId, 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); 150 return; 151 } 152 153 if (onCardShown !== undefined) 154 onCardShown(receivedNotification); 155 }); 156 } 157 }); 158 } 159 160 /** 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|. 169 */ 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; 176 177 callback(uncombinedNotification, shouldShow && !shouldHide); 178 } 179 } 180 181 /** 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. 192 */ 193 function update(cardId, combinedCard, notificationGroups, onCardShown) { 194 console.log('cardManager.update ' + JSON.stringify(combinedCard)); 195 196 chrome.alarms.clear(alarmPrefix + cardId); 197 var now = Date.now(); 198 /** @type {?UncombinedNotification} */ 199 var winningCard = null; 200 // Next moment of time when winning notification selection algotithm can 201 // potentially return a different notification. 202 /** @type {?number} */ 203 var nextEventTime = null; 204 205 // Find a winning uncombined notification: a highest-priority notification 206 // that needs to be shown now. 207 iterateUncombinedNotifications( 208 combinedCard, 209 now, 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. 213 if (visible) { 214 if (!winningCard || 215 uncombinedCard.receivedNotification.chromeNotificationOptions. 216 priority > 217 winningCard.receivedNotification.chromeNotificationOptions. 218 priority) { 219 winningCard = uncombinedCard; 220 } 221 } 222 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; 227 } 228 if (uncombinedCard.hideTime > now) { 229 if (!nextEventTime || nextEventTime > uncombinedCard.hideTime) 230 nextEventTime = uncombinedCard.hideTime; 231 } 232 }); 233 234 // Show/hide the winning card. 235 updateNotification( 236 cardId, winningCard && winningCard.receivedNotification, onCardShown); 237 238 if (nextEventTime) { 239 // If we expect more events, create an alarm for the next one. 240 chrome.alarms.create(alarmPrefix + cardId, {when: nextEventTime}); 241 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 || null) && 247 JSON.parse(JSON.stringify( 248 winningCard.receivedNotification.actionUrls)); 249 250 return { 251 actionUrls: winningActionUrls, 252 timestamp: now, 253 combinedCard: combinedCard 254 }; 255 } else { 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); 260 return undefined; 261 } 262 } 263 264 /** 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 269 * for this card. 270 * @param {Object.<string, StoredNotificationGroup>} notificationGroups 271 * Map from group name to group information. 272 * @return {{ 273 * dismissals: Array.<DismissalData>, 274 * notificationData: (NotificationDataEntry|undefined) 275 * }} 276 */ 277 function onDismissal(cardId, notificationData, notificationGroups) { 278 var dismissals = []; 279 var newCombinedCard = []; 280 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 283 // was last updated. 284 iterateUncombinedNotifications( 285 notificationData.combinedCard, 286 notificationData.timestamp, 287 function(uncombinedCard, visible) { 288 if (visible) { 289 dismissals.push({ 290 notificationId: uncombinedCard.receivedNotification.notificationId, 291 parameters: uncombinedCard.receivedNotification.dismissal 292 }); 293 } else { 294 newCombinedCard.push(uncombinedCard); 295 } 296 }); 297 298 return { 299 dismissals: dismissals, 300 notificationData: update(cardId, newCombinedCard, notificationGroups) 301 }; 302 } 303 304 /** 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. 309 */ 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); 317 break; 318 } 319 } 320 } 321 } 322 323 instrumented.alarms.onAlarm.addListener(function(alarm) { 324 console.log('cardManager.onAlarm ' + JSON.stringify(alarm)); 325 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'], 332 function(items) { 333 console.log('cardManager.onAlarm.get ' + JSON.stringify(items)); 334 items = items || {}; 335 /** @type {Object.<string, NotificationDataEntry>} */ 336 items.notificationsData = items.notificationsData || {}; 337 /** @type {Object.<string, StoredNotificationGroup>} */ 338 items.notificationGroups = items.notificationGroups || {}; 339 340 var combinedCard = 341 (items.notificationsData[cardId] && 342 items.notificationsData[cardId].combinedCard) || []; 343 344 var cardShownCallback = undefined; 345 if (localStorage['locationCardsShown'] < 346 LOCATION_CARDS_LINK_THRESHOLD) { 347 cardShownCallback = countLocationCard; 348 } 349 350 items.notificationsData[cardId] = 351 update( 352 cardId, 353 combinedCard, 354 items.notificationGroups, 355 cardShownCallback); 356 357 chrome.storage.local.set(items); 358 }); 359 }); 360 } 361 }); 362 363 return { 364 update: update, 365 onDismissal: onDismissal 366 }; 367} 368