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 */ 15 var Trigger; 16 17 /** 18 * ID of an individual (uncombined) notification. 19 * This ID comes directly from the server. 20 * 21 * @typedef {string} 22 */ 23 var ServerNotificationId; 24 25 /** 26 * Data to build a dismissal request for a card from a specific group. 27 * 28 * @typedef {{ 29 * notificationId: ServerNotificationId, 30 * parameters: Object 31 * }} 32 */ 33 var DismissalData; 34 35 /** 36 * Urls that need to be opened when clicking a notification or its buttons. 37 * 38 * @typedef {{ 39 * messageUrl: (string|undefined), 40 * buttonUrls: (Array.<string>|undefined) 41 * }} 42 */ 43 var ActionUrls; 44 45 /** 46 * ID of a combined notification. 47 * This is the ID used with chrome.notifications API. 48 * 49 * @typedef {string} 50 */ 51 var ChromeNotificationId; 52 53 /** 54 * Notification as sent by the server. 55 * 56 * @typedef {{ 57 * notificationId: ServerNotificationId, 58 * chromeNotificationId: ChromeNotificationId, 59 * trigger: Trigger, 60 * chromeNotificationOptions: Object, 61 * actionUrls: (ActionUrls|undefined), 62 * dismissal: Object, 63 * locationBased: (boolean|undefined), 64 * groupName: string, 65 * cardTypeId: (number|undefined) 66 * }} 67 */ 68 var ReceivedNotification; 69 70 /** 71 * Received notification in a self-sufficient form that doesn't require group's 72 * timestamp to calculate show and hide times. 73 * 74 * @typedef {{ 75 * receivedNotification: ReceivedNotification, 76 * showTime: (number|undefined), 77 * hideTime: number 78 * }} 79 */ 80 var UncombinedNotification; 81 82 /** 83 * Card combined from potentially multiple groups. 84 * 85 * @typedef {Array.<UncombinedNotification>} 86 */ 87 var CombinedCard; 88 89 /** 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(). 93 * 94 * @typedef {{ 95 * actionUrls: (ActionUrls|undefined), 96 * cardTypeId: (number|undefined), 97 * timestamp: number, 98 * combinedCard: CombinedCard 99 * }} 100 * 101 */ 102 var NotificationDataEntry; 103 104 /** 105 * Names for tasks that can be created by the this file. 106 */ 107 var UPDATE_CARD_TASK_NAME = 'update-card'; 108 109 /** 110 * Builds an object to manage notification card set. 111 * @return {Object} Card set interface. 112 */ 113 function buildCardSet() { 114 var alarmPrefix = 'card-'; 115 116 /** 117 * Creates/updates/deletes a Chrome notification. 118 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID 119 * of the card. 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. 125 */ 126 function updateNotification( 127 chromeNotificationId, receivedNotification, onCardShown) { 128 console.log( 129 'cardManager.updateNotification ' + chromeNotificationId + ' ' + 130 JSON.stringify(receivedNotification)); 131 132 if (!receivedNotification) { 133 instrumented.notifications.clear(chromeNotificationId, function() {}); 134 return; 135 } 136 137 // Try updating the notification. 138 instrumented.notifications.update( 139 chromeNotificationId, 140 receivedNotification.chromeNotificationOptions, 141 function(wasUpdated) { 142 if (!wasUpdated) { 143 // If the notification wasn't updated, it probably didn't exist. 144 // Create it. 145 console.log( 146 'cardManager.updateNotification ' + chromeNotificationId + 147 ' failed to update, creating'); 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); 157 return; 158 } 159 160 if (onCardShown !== undefined) 161 onCardShown(receivedNotification); 162 }); 163 } 164 }); 165 } 166 167 /** 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|. 176 */ 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; 183 184 callback(uncombinedNotification, shouldShow && !shouldHide); 185 } 186 } 187 188 /** 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 192 * of the card. 193 * @param {CombinedCard} combinedCard Combined cards with 194 * |chromeNotificationId|. 195 * @param {Object.<string, StoredNotificationGroup>} notificationGroups 196 * Map from group 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. 201 */ 202 function update( 203 chromeNotificationId, combinedCard, notificationGroups, onCardShown) { 204 console.log('cardManager.update ' + JSON.stringify(combinedCard)); 205 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; 214 215 // Find a winning uncombined notification: a highest-priority notification 216 // that needs to be shown now. 217 iterateUncombinedNotifications( 218 combinedCard, 219 now, 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. 223 if (visible) { 224 if (!winningCard || 225 uncombinedCard.receivedNotification.chromeNotificationOptions. 226 priority > 227 winningCard.receivedNotification.chromeNotificationOptions. 228 priority) { 229 winningCard = uncombinedCard; 230 } 231 } 232 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; 237 } 238 if (uncombinedCard.hideTime > now) { 239 if (!nextEventTime || nextEventTime > uncombinedCard.hideTime) 240 nextEventTime = uncombinedCard.hideTime; 241 } 242 }); 243 244 // Show/hide the winning card. 245 updateNotification( 246 chromeNotificationId, 247 winningCard && winningCard.receivedNotification, 248 onCardShown); 249 250 if (nextEventTime) { 251 // If we expect more events, create an alarm for the next one. 252 chrome.alarms.create( 253 alarmPrefix + chromeNotificationId, {when: nextEventTime}); 254 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; 264 return { 265 actionUrls: winningActionUrls, 266 cardTypeId: winningCardTypeId, 267 timestamp: now, 268 combinedCard: combinedCard 269 }; 270 } else { 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); 275 return undefined; 276 } 277 } 278 279 /** 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 283 * of the card. 284 * @param {NotificationDataEntry} notificationData Stored notification entry 285 * for this card. 286 * @param {Object.<string, StoredNotificationGroup>} notificationGroups 287 * Map from group name to group information. 288 * @return {{ 289 * dismissals: Array.<DismissalData>, 290 * notificationData: (NotificationDataEntry|undefined) 291 * }} 292 */ 293 function onDismissal( 294 chromeNotificationId, notificationData, notificationGroups) { 295 /** @type {Array.<DismissalData>} */ 296 var dismissals = []; 297 /** @type {Array.<UncombinedNotification>} */ 298 var newCombinedCard = []; 299 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 302 // was last updated. 303 iterateUncombinedNotifications( 304 notificationData.combinedCard, 305 notificationData.timestamp, 306 function(uncombinedCard, visible) { 307 if (visible) { 308 dismissals.push({ 309 notificationId: uncombinedCard.receivedNotification.notificationId, 310 parameters: uncombinedCard.receivedNotification.dismissal 311 }); 312 } else { 313 newCombinedCard.push(uncombinedCard); 314 } 315 }); 316 317 return { 318 dismissals: dismissals, 319 notificationData: update( 320 chromeNotificationId, newCombinedCard, notificationGroups) 321 }; 322 } 323 324 /** 325 * Removes card information from |notificationGroups|. 326 * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID 327 * of the card. 328 * @param {Object.<string, StoredNotificationGroup>} notificationGroups 329 * Map from group name to group information. 330 */ 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); 338 break; 339 } 340 } 341 } 342 } 343 344 instrumented.alarms.onAlarm.addListener(function(alarm) { 345 console.log('cardManager.onAlarm ' + JSON.stringify(alarm)); 346 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.<string, StoredNotificationGroup>} */ 356 notificationGroups: {} 357 }).then(function(items) { 358 console.log('cardManager.onAlarm.get ' + JSON.stringify(items)); 359 360 var combinedCard = 361 (items.notificationsData[chromeNotificationId] && 362 items.notificationsData[chromeNotificationId].combinedCard) || []; 363 364 var cardShownCallback = undefined; 365 if (localStorage['explanatoryCardsShown'] < 366 EXPLANATORY_CARDS_LINK_THRESHOLD) { 367 cardShownCallback = countExplanatoryCard; 368 } 369 370 items.notificationsData[chromeNotificationId] = 371 update( 372 chromeNotificationId, 373 combinedCard, 374 items.notificationGroups, 375 cardShownCallback); 376 377 chrome.storage.local.set(items); 378 }); 379 }); 380 } 381 }); 382 383 return { 384 update: update, 385 onDismissal: onDismissal 386 }; 387 } 388