Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 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 * @fileoverview The event page for Google Now for Chrome implementation. | 8 * @fileoverview The event page for Google Now for Chrome implementation. |
| 9 * The Google Now event page gets Google Now cards from the server and shows | 9 * The Google Now event page gets Google Now cards from the server and shows |
| 10 * them as Chrome notifications. | 10 * them as Chrome notifications. |
| 11 * The service performs periodic updating of Google Now cards. | 11 * The service performs periodic updating of Google Now cards. |
| 12 * Each updating of the cards includes 3 steps: | 12 * Each updating of the cards includes 3 steps: |
| 13 * 1. Obtaining the location of the machine; | 13 * 1. Obtaining the location of the machine; |
| 14 * 2. Making a server request based on that location; | 14 * 2. Making a server request based on that location; |
| 15 * 3. Showing the received cards as notifications. | 15 * 3. Showing the received cards as notifications. |
| 16 */ | 16 */ |
| 17 | 17 |
| 18 // TODO(vadimt): Use background permission to show notifications even when all | 18 // TODO(vadimt): Use background permission to show notifications even when all |
| 19 // browser windows are closed. | 19 // browser windows are closed. |
| 20 // TODO(vadimt): Remove the C++ implementation. | 20 // TODO(vadimt): Remove the C++ implementation. |
| 21 // TODO(vadimt): Decide what to do in incognito mode. | 21 // TODO(vadimt): Decide what to do in incognito mode. |
| 22 // TODO(vadimt): Gather UMAs. | 22 // TODO(vadimt): Gather UMAs. |
| 23 // TODO(vadimt): Honor the flag the enables Google Now integration. | 23 // TODO(vadimt): Honor the flag the enables Google Now integration. |
| 24 // TODO(vadimt): Figure out the final values of the constants. | 24 // TODO(vadimt): Figure out the final values of the constants. |
| 25 // TODO(vadimt): Report internal and server errors. Collect UMAs on errors where | 25 // TODO(vadimt): Report internal and server errors. Collect UMAs on errors where |
| 26 // appropriate. Also consider logging errors or throwing exceptions. | 26 // appropriate. Also consider logging errors or throwing exceptions. |
| 27 | |
| 28 // TODO(vadimt): Consider processing errors for all storage.set calls. | 27 // TODO(vadimt): Consider processing errors for all storage.set calls. |
| 28 | |
| 29 // TODO(vadimt): Figure out the server name. Use it in the manifest and for | 29 // TODO(vadimt): Figure out the server name. Use it in the manifest and for |
| 30 // NOTIFICATION_CARDS_URL. Meanwhile, to use the feature, you need to manually | 30 // NOTIFICATION_CARDS_URL. Meanwhile, to use the feature, you need to manually |
| 31 // set the server name via local storage. | 31 // set the server name via local storage. |
| 32 /** | 32 /** |
| 33 * URL to retrieve notification cards. | 33 * URL to retrieve notification cards. |
| 34 */ | 34 */ |
| 35 var NOTIFICATION_CARDS_URL = localStorage['server_url']; | 35 var NOTIFICATION_CARDS_URL = localStorage['server_url']; |
| 36 | 36 |
| 37 /** | 37 /** |
| 38 * Standard response code for successful HTTP requests. This is the only success | 38 * Standard response code for successful HTTP requests. This is the only success |
| 39 * code the server will send. | 39 * code the server will send. |
| 40 */ | 40 */ |
| 41 var HTTP_OK = 200; | 41 var HTTP_OK = 200; |
| 42 | 42 |
| 43 /** | 43 /** |
| 44 * Initial period for polling for Google Now Notifications cards to use when the | 44 * Initial period for polling for Google Now Notifications cards to use when the |
| 45 * period from the server is not available. | 45 * period from the server is not available. |
| 46 */ | 46 */ |
| 47 var INITIAL_POLLING_PERIOD_SECONDS = 300; // 5 minutes | 47 var INITIAL_POLLING_PERIOD_SECONDS = 300; // 5 minutes |
| 48 | 48 |
| 49 /** | 49 /** |
| 50 * Maximal period for polling for Google Now Notifications cards to use when the | 50 * Maximal period for polling for Google Now Notifications cards to use when the |
| 51 * period from the server is not available. | 51 * period from the server is not available. |
| 52 */ | 52 */ |
| 53 var MAXIMUM_POLLING_PERIOD_SECONDS = 3600; // 1 hour | 53 var MAXIMUM_POLLING_PERIOD_SECONDS = 3600; // 1 hour |
| 54 | 54 |
| 55 /** | |
| 56 * Names for tasks that can be created by the extension. | |
| 57 * @enum {string} | |
| 58 */ | |
| 59 var TaskName = { | |
| 60 UPDATE_CARDS: 'update-cards', | |
| 61 DISMISS_CARD: 'dismiss-card', | |
| 62 CARD_CLICKED: 'card-clicked' | |
| 63 }; | |
| 64 | |
| 65 var UPDATE_NOTIFICATIONS_ALARM_NAME = 'UPDATE'; | |
| 66 | |
| 55 var storage = chrome.storage.local; | 67 var storage = chrome.storage.local; |
| 56 | 68 |
| 57 /** | 69 ///////////////////////////////////////////////////////////////////////// |
| 58 * Show a notification and remember information associated with it. | 70 ////// ABSOLUTELY NOT FORGET TO MOVE ASSERT AND TASK MANAGER INTO SEPARATE FILE |
| 71 ////// IN THIS CL !!!!!!!!!!!!! | |
| 72 //////////////////////////////////////////////////////////////////////// | |
| 73 | |
| 74 /** | |
| 75 * Checks for internal errors. | |
| 76 * @param {boolean} condition Condition that must be true. | |
| 77 * @param {string} message Diagnostic message for the case when the condition is | |
| 78 * false. | |
| 79 */ | |
| 80 function verify(condition, message) { | |
| 81 // TODO(vadimt): Send UMAs instead of showing alert. | |
| 82 if (!condition) { | |
| 83 var errorText = 'ASSERT: ' + message; | |
| 84 console.error(errorText); | |
| 85 alert(errorText); | |
| 86 } | |
| 87 } | |
| 88 | |
| 89 /** | |
| 90 * Builds the object to manage tasks (mutually exclusive chains of events). | |
| 91 * @return {Object} Set of methods to manage tasks. | |
| 92 */ | |
| 93 function TaskManager() { | |
| 94 /** | |
| 95 * Name of the alarm that triggers the error saying that the event page cannot | |
| 96 * unload. | |
| 97 */ | |
| 98 var CANNOT_UNLOAD_ALARM_NAME = 'CANNOT-UNLOAD'; | |
| 99 | |
| 100 /** | |
| 101 * Maximal time we expect the event page to stay loaded after starting a task. | |
| 102 */ | |
| 103 var MAXIMUM_LOADED_TIME_MINUTES = 5; | |
| 104 | |
| 105 /** | |
| 106 * Queue of scheduled tasks. The first element, if present, corresponds to the | |
| 107 * currently running task. | |
| 108 * @type {Array.<Object.<TaskName, function()>>} | |
| 109 */ | |
| 110 var queue = []; | |
| 111 | |
| 112 /** | |
| 113 * Name of the current step of the currently running task if present, | |
| 114 * otherwise, null. For diagnostics only. | |
| 115 * It's set when the task is started and before each asynchronous operation. | |
| 116 */ | |
| 117 var stepName = null; | |
| 118 | |
| 119 /** | |
| 120 * Starts the first queued task. | |
| 121 */ | |
| 122 function startFirst() { | |
| 123 verify(queue.length >= 1, 'startFirst: queue is empty'); | |
| 124 | |
| 125 // Set alarm to verify that the event page will unload in a reasonable time. | |
| 126 chrome.alarms.create(CANNOT_UNLOAD_ALARM_NAME, | |
| 127 {delayInMinutes: MAXIMUM_LOADED_TIME_MINUTES}); | |
| 128 | |
| 129 // Starts the oldest queued task, but doesn't remove it from the queue. | |
| 130 verify(stepName == null, 'tasks.startFirst: stepName is not null'); | |
| 131 var entry = queue[0]; | |
| 132 stepName = entry.name + '-initial'; | |
| 133 entry.task(); | |
| 134 } | |
| 135 | |
| 136 /** | |
| 137 * Checks if a new task can't be added to a queue that contains an existing | |
| 138 * task. | |
| 139 * @param {string} newTaskName Name of the new task. | |
| 140 * @param {string} queuedTaskName Name of the task in the queue. | |
| 141 * @return {boolean} Whether the new task conflicts with the existing task. | |
| 142 */ | |
| 143 function doConflict(newTaskName, queuedTaskName) { | |
|
rgustafson
2013/03/11 18:11:05
Things starting with do generally mean taking acti
vadimt
2013/03/11 21:31:53
Done.
| |
| 144 if (newTaskName == TaskName.UPDATE_CARDS && | |
| 145 queuedTaskName == TaskName.UPDATE_CARDS) { | |
| 146 // If a card update is requested while an old update is still in the | |
| 147 // queue, we don't need the new update. | |
| 148 return true; | |
| 149 } | |
| 150 | |
| 151 return false; | |
| 152 } | |
| 153 | |
| 154 /** | |
| 155 * Checks if a new task can be added to the task queue. | |
| 156 * @param {TaskName} taskName Name of the new task. | |
| 157 * @return {boolean} Whether the new task can be added. | |
| 158 */ | |
| 159 function canQueue(taskName) { | |
| 160 for (var i = 0; i < queue.length; ++i) | |
| 161 if (doConflict(taskName, queue[i].name)) | |
| 162 return false; | |
| 163 | |
| 164 return true; | |
| 165 } | |
| 166 | |
| 167 /** | |
| 168 * Adds a new task. If another task is not running, run the task immediately. | |
| 169 * If any task in the queue is not compatible with the task, ignore the new | |
| 170 * task. Otherwise, store the task for future execution. | |
|
skare_
2013/03/11 19:46:26
silently ignoring the task will always be ok?
seem
vadimt
2013/03/11 21:31:53
TaskManager::areConflicting() returns true if:
(1)
| |
| 171 * @param {TaskName} taskName Name of the task. | |
| 172 * @param {function()} task Code of the task. | |
| 173 */ | |
| 174 function submit(taskName, task) { | |
| 175 if (!canQueue(taskName)) | |
| 176 return; | |
| 177 | |
| 178 queue.push({name: taskName, task: task}); | |
| 179 | |
| 180 if (queue.length == 1) { | |
| 181 startFirst(); | |
| 182 } | |
| 183 } | |
| 184 | |
| 185 /** | |
| 186 * Completes the current task and start the next queued task if available. | |
| 187 */ | |
| 188 function finish() { | |
| 189 verify(queue.length >= 1, 'tasks.finish: The task queue is empty.'); | |
| 190 queue.shift(); | |
| 191 stepName = null; | |
| 192 | |
| 193 if (queue.length >= 1) | |
| 194 startFirst(); | |
| 195 } | |
| 196 | |
| 197 /** | |
| 198 * Associates a name with the current step of the task. Used for diagnostics | |
| 199 * only. A task is a chain of asynchronous events; setStepName should be | |
| 200 * called before starting any asynchronous operation. | |
| 201 * @param {string} step Name of new step. | |
| 202 */ | |
| 203 function setStepName(step) { | |
| 204 // TODO(vadimt): Pass UMA counters instead of step names. | |
| 205 stepName = step; | |
| 206 } | |
| 207 | |
| 208 chrome.alarms.onAlarm.addListener(function(alarm) { | |
| 209 if (alarm.name == CANNOT_UNLOAD_ALARM_NAME) { | |
| 210 // Error if the event page wasn't unloaded after a reasonable timeout | |
| 211 // since starting the last task. | |
| 212 // TODO(vadimt): Uncomment the verify once this bug is fixed: | |
| 213 // crbug.com/177563 | |
| 214 // verify(false, 'Event page didn\'t unload, queue = ' + | |
| 215 // JSON.stringify(tasks) + ', step = ' + stepName + ' (ignore this verify | |
| 216 // if devtools is attached).'); | |
| 217 } | |
| 218 }); | |
| 219 | |
| 220 chrome.runtime.onSuspend.addListener(function() { | |
| 221 chrome.alarms.clear(CANNOT_UNLOAD_ALARM_NAME); | |
| 222 verify(queue.length == 0 && stepName == null, | |
| 223 'Incomplete task when unloading event page, queue = ' + | |
| 224 JSON.stringify(queue) + ', step = ' + stepName); | |
| 225 }); | |
| 226 | |
| 227 return { | |
| 228 submit: submit, | |
| 229 setStepName: setStepName, | |
| 230 finish: finish | |
| 231 }; | |
| 232 } | |
| 233 | |
| 234 var tasks = TaskManager(); | |
| 235 | |
| 236 /** | |
| 237 * Shows a notification and remember information associated with it. | |
|
skare_
2013/03/11 19:53:07
tiny nit: s/remember/remembers
vadimt
2013/03/11 21:31:53
Done.
| |
| 59 * @param {Object} card Google Now card represented as a set of parameters for | 238 * @param {Object} card Google Now card represented as a set of parameters for |
| 60 * showing a Chrome notification. | 239 * showing a Chrome notification. |
| 61 * @param {Object} notificationsUrlInfo Map from notification id to the | 240 * @param {Object} notificationsUrlInfo Map from notification id to the |
| 62 * notification's set of URLs. | 241 * notification's set of URLs. |
| 63 */ | 242 */ |
| 64 function createNotification(card, notificationsUrlInfo) { | 243 function createNotification(card, notificationsUrlInfo) { |
| 65 // Create a notification or quietly update if it already exists. | 244 // Create a notification or quietly update if it already exists. |
| 66 // TODO(vadimt): Implement non-quiet updates. | 245 // TODO(vadimt): Implement non-quiet updates. |
| 67 chrome.notifications.create( | 246 chrome.notifications.create( |
| 68 card.notificationId, | 247 card.notificationId, |
| 69 card.notification, | 248 card.notification, |
| 70 function(assignedNotificationId) {}); | 249 function() {}); |
| 71 | 250 |
| 72 notificationsUrlInfo[card.notificationId] = card.actionUrls; | 251 notificationsUrlInfo[card.notificationId] = card.actionUrls; |
| 73 } | 252 } |
| 74 | 253 |
| 75 /** | 254 /** |
| 76 * Parse JSON response from the notification server, show notifications and | 255 * Parses JSON response from the notification server, show notifications and |
| 77 * schedule next update. | 256 * schedule next update. |
| 78 * @param {string} response Server response. | 257 * @param {string} response Server response. |
| 79 */ | 258 */ |
| 80 function parseAndShowNotificationCards(response) { | 259 function parseAndShowNotificationCards(response) { |
| 81 try { | 260 try { |
| 82 var parsedResponse = JSON.parse(response); | 261 var parsedResponse = JSON.parse(response); |
| 83 } catch (error) { | 262 } catch (error) { |
| 84 // TODO(vadimt): Report errors to the user. | 263 // TODO(vadimt): Report errors to the user. |
| 85 return; | 264 return; |
| 86 } | 265 } |
| 87 | 266 |
| 88 var cards = parsedResponse.cards; | 267 var cards = parsedResponse.cards; |
| 89 | 268 |
| 90 if (!(cards instanceof Array)) { | 269 if (!(cards instanceof Array)) { |
| 91 // TODO(vadimt): Report errors to the user. | 270 // TODO(vadimt): Report errors to the user. |
| 92 return; | 271 return; |
| 93 } | 272 } |
| 94 | 273 |
| 95 if (typeof parsedResponse.expiration_timestamp_seconds != 'number') { | 274 if (typeof parsedResponse.expiration_timestamp_seconds != 'number') { |
| 96 // TODO(vadimt): Report errors to the user. | 275 // TODO(vadimt): Report errors to the user. |
| 97 return; | 276 return; |
| 98 } | 277 } |
| 99 | 278 |
| 279 tasks.setStepName('parseAndShowNotificationCards-get-active-notifications'); | |
| 100 storage.get('activeNotifications', function(items) { | 280 storage.get('activeNotifications', function(items) { |
| 101 // Mark existing notifications that received an update in this server | 281 // Mark existing notifications that received an update in this server |
| 102 // response. | 282 // response. |
| 103 for (var i = 0; i < cards.length; ++i) { | 283 for (var i = 0; i < cards.length; ++i) { |
| 104 var notificationId = cards[i].notificationId; | 284 var notificationId = cards[i].notificationId; |
| 105 if (notificationId in items.activeNotifications) | 285 if (notificationId in items.activeNotifications) |
| 106 items.activeNotifications[notificationId].hasUpdate = true; | 286 items.activeNotifications[notificationId].hasUpdate = true; |
| 107 } | 287 } |
| 108 | 288 |
| 109 // Delete notifications that didn't receive an update. | 289 // Delete notifications that didn't receive an update. |
| 110 for (var notificationId in items.activeNotifications) | 290 for (var notificationId in items.activeNotifications) |
| 111 if (items.activeNotifications.hasOwnProperty(notificationId) && | 291 if (items.activeNotifications.hasOwnProperty(notificationId) && |
| 112 !items.activeNotifications[notificationId].hasUpdate) { | 292 !items.activeNotifications[notificationId].hasUpdate) { |
| 113 chrome.notifications.clear( | 293 chrome.notifications.clear( |
| 114 notificationId, | 294 notificationId, |
| 115 function(wasDeleted) {}); | 295 function() {}); |
| 116 } | 296 } |
| 117 | 297 |
| 118 // Create/update notifications and store their new properties. | 298 // Create/update notifications and store their new properties. |
| 119 var notificationsUrlInfo = {}; | 299 var notificationsUrlInfo = {}; |
| 120 | 300 |
| 121 for (var i = 0; i < cards.length; ++i) { | 301 for (var i = 0; i < cards.length; ++i) { |
| 122 try { | 302 try { |
| 123 createNotification(cards[i], notificationsUrlInfo); | 303 createNotification(cards[i], notificationsUrlInfo); |
| 124 } catch (error) { | 304 } catch (error) { |
| 125 // TODO(vadimt): Report errors to the user. | 305 // TODO(vadimt): Report errors to the user. |
| 126 } | 306 } |
| 127 } | 307 } |
| 128 storage.set({activeNotifications: notificationsUrlInfo}); | 308 storage.set({activeNotifications: notificationsUrlInfo}); |
| 129 | 309 |
| 130 scheduleNextUpdate(parsedResponse.expiration_timestamp_seconds); | 310 scheduleNextUpdate(parsedResponse.expiration_timestamp_seconds); |
| 131 | 311 |
| 132 // Now that we got a valid response from the server, reset the retry period | 312 // Now that we got a valid response from the server, reset the retry period |
| 133 // to the initial value. This retry period will be used the next time we | 313 // to the initial value. This retry period will be used the next time we |
| 134 // fail to get the server-provided period. | 314 // fail to get the server-provided period. |
| 135 storage.set({retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS}); | 315 storage.set({retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS}); |
| 316 tasks.finish(); | |
| 136 }); | 317 }); |
| 137 } | 318 } |
| 138 | 319 |
| 139 /** | 320 /** |
| 140 * Request notification cards from the server. | 321 * Requests notification cards from the server. |
| 141 * @param {string} requestParameters Query string for the request. | 322 * @param {string} requestParameters Query string for the request. |
| 142 */ | 323 */ |
| 143 function requestNotificationCards(requestParameters) { | 324 function requestNotificationCards(requestParameters) { |
| 144 // TODO(vadimt): Figure out how to send user's identity to the server. | 325 // TODO(vadimt): Figure out how to send user's identity to the server. |
| 145 var request = new XMLHttpRequest(); | 326 var request = new XMLHttpRequest(); |
| 146 | 327 |
| 147 request.responseType = 'text'; | 328 request.responseType = 'text'; |
| 148 request.onload = function(event) { | 329 request.onloadend = function() { |
| 149 if (request.status == HTTP_OK) | 330 if (request.status == HTTP_OK) |
| 150 parseAndShowNotificationCards(request.response); | 331 parseAndShowNotificationCards(request.response); |
| 332 else | |
| 333 tasks.finish(); | |
| 151 } | 334 } |
| 152 | 335 |
| 153 request.open( | 336 request.open( |
| 154 'GET', | 337 'GET', |
| 155 NOTIFICATION_CARDS_URL + '/notifications' + requestParameters, | 338 NOTIFICATION_CARDS_URL + '/notifications' + requestParameters, |
| 156 true); | 339 true); |
| 340 tasks.setStepName('requestNotificationCards-send-request'); | |
| 157 request.send(); | 341 request.send(); |
| 158 } | 342 } |
| 159 | 343 |
| 160 /** | 344 /** |
| 161 * Request notification cards from the server when we have geolocation. | 345 * Requests notification cards from the server when we have geolocation. |
| 162 * @param {Geoposition} position Location of this computer. | 346 * @param {Geoposition} position Location of this computer. |
| 163 */ | 347 */ |
| 164 function requestNotificationCardsWithLocation(position) { | 348 function requestNotificationCardsWithLocation(position) { |
| 165 // TODO(vadimt): Should we use 'q' as the parameter name? | 349 // TODO(vadimt): Should we use 'q' as the parameter name? |
| 166 var requestParameters = | 350 var requestParameters = |
| 167 '?q=' + position.coords.latitude + | 351 '?q=' + position.coords.latitude + |
| 168 ',' + position.coords.longitude + | 352 ',' + position.coords.longitude + |
| 169 ',' + position.coords.accuracy; | 353 ',' + position.coords.accuracy; |
| 170 | 354 |
| 171 requestNotificationCards(requestParameters); | 355 requestNotificationCards(requestParameters); |
| 172 } | 356 } |
| 173 | 357 |
| 174 /** | 358 /** |
| 175 * Request notification cards from the server when we don't have geolocation. | 359 * Requests notification cards from the server when we don't have geolocation. |
| 176 * @param {PositionError} positionError Position error. | 360 * @param {PositionError} positionError Position error. |
| 177 */ | 361 */ |
| 178 function requestNotificationCardsWithoutLocation(positionError) { | 362 function requestNotificationCardsWithoutLocation(positionError) { |
| 179 requestNotificationCards(''); | 363 requestNotificationCards(''); |
| 180 } | 364 } |
| 181 | 365 |
| 182 /** | 366 /** |
| 183 * Obtain new location; request and show notification cards based on this | 367 * Obtains new location; requests and shows notification cards based on this |
| 184 * location. | 368 * location. |
| 185 */ | 369 */ |
| 186 function updateNotificationsCards() { | 370 function updateNotificationsCards() { |
| 187 storage.get('retryDelaySeconds', function(items) { | 371 tasks.submit(TaskName.UPDATE_CARDS, function() { |
| 188 // Immediately schedule the update after the current retry period. Then, | 372 tasks.setStepName('updateNotificationsCards-get-retryDelaySeconds'); |
| 189 // we'll use update time from the server if available. | 373 storage.get('retryDelaySeconds', function(items) { |
| 190 scheduleNextUpdate(items.retryDelaySeconds); | 374 // Immediately schedule the update after the current retry period. Then, |
| 191 | 375 // we'll use update time from the server if available. |
| 192 // TODO(vadimt): Consider interrupting waiting for the next update if we | 376 scheduleNextUpdate(items.retryDelaySeconds); |
| 193 // detect that the network conditions have changed. Also, decide whether the | 377 |
| 194 // exponential backoff is needed both when we are offline and when there are | 378 // TODO(vadimt): Consider interrupting waiting for the next update if we |
| 195 // failures on the server side. | 379 // detect that the network conditions have changed. Also, decide whether |
| 196 var newRetryDelaySeconds = | 380 // the exponential backoff is needed both when we are offline and when |
| 197 Math.min(items.retryDelaySeconds * 2 * (1 + 0.2 * Math.random()), | 381 // there are failures on the server side. |
| 198 MAXIMUM_POLLING_PERIOD_SECONDS); | 382 var newRetryDelaySeconds = |
| 199 storage.set({retryDelaySeconds: newRetryDelaySeconds}); | 383 Math.min(items.retryDelaySeconds * 2 * (1 + 0.2 * Math.random()), |
| 200 | 384 MAXIMUM_POLLING_PERIOD_SECONDS); |
| 201 navigator.geolocation.getCurrentPosition( | 385 storage.set({retryDelaySeconds: newRetryDelaySeconds}); |
| 202 requestNotificationCardsWithLocation, | 386 |
| 203 requestNotificationCardsWithoutLocation); | 387 tasks.setStepName('updateNotificationsCards-get-location'); |
| 388 navigator.geolocation.getCurrentPosition( | |
| 389 requestNotificationCardsWithLocation, | |
| 390 requestNotificationCardsWithoutLocation); | |
| 391 }); | |
| 204 }); | 392 }); |
| 205 } | 393 } |
| 206 | 394 |
| 207 /** | 395 /** |
| 208 * Opens URL corresponding to the clicked part of the notification. | 396 * Opens URL corresponding to the clicked part of the notification. |
| 209 * @param {string} notificationId Unique identifier of the notification. | 397 * @param {string} notificationId Unique identifier of the notification. |
| 210 * @param {function(Object): string} selector Function that extracts the url for | 398 * @param {function(Object): string} selector Function that extracts the url for |
| 211 * the clicked area from the button action URLs info. | 399 * the clicked area from the button action URLs info. |
| 212 */ | 400 */ |
| 213 function onNotificationClicked(notificationId, selector) { | 401 function onNotificationClicked(notificationId, selector) { |
| 214 storage.get('activeNotifications', function(items) { | 402 tasks.submit(TaskName.CARD_CLICKED, function() { |
| 215 var actionUrls = items.activeNotifications[notificationId]; | 403 tasks.setStepName('onNotificationClicked-get-activeNotifications'); |
| 216 if (typeof actionUrls != 'object') { | 404 storage.get('activeNotifications', function(items) { |
| 217 // TODO(vadimt): report an error. | 405 var actionUrls = items.activeNotifications[notificationId]; |
| 218 return; | 406 if (typeof actionUrls != 'object') { |
| 219 } | 407 // TODO(vadimt): report an error. |
| 220 | 408 tasks.finish(); |
|
skare_
2013/03/11 19:53:07
would it be useful in the future to have finish()
vadimt
2013/03/11 21:31:53
At this point, it doesn't seem so. finish() is to
| |
| 221 var url = selector(actionUrls); | 409 return; |
| 222 | 410 } |
| 223 if (typeof url != 'string') { | 411 |
| 224 // TODO(vadimt): report an error. | 412 var url = selector(actionUrls); |
| 225 return; | 413 |
| 226 } | 414 if (typeof url != 'string') { |
| 227 | 415 // TODO(vadimt): report an error. |
| 228 chrome.tabs.create({url: url}); | 416 tasks.finish(); |
| 417 return; | |
| 418 } | |
| 419 | |
| 420 chrome.tabs.create({url: url}); | |
| 421 tasks.finish(); | |
| 422 }); | |
| 229 }); | 423 }); |
| 230 } | 424 } |
| 231 | 425 |
| 232 /** | 426 /** |
| 233 * Callback for chrome.notifications.onClosed event. | 427 * Callback for chrome.notifications.onClosed event. |
| 234 * @param {string} notificationId Unique identifier of the notification. | 428 * @param {string} notificationId Unique identifier of the notification. |
| 235 * @param {boolean} byUser Whether the notification was closed by the user. | 429 * @param {boolean} byUser Whether the notification was closed by the user. |
| 236 */ | 430 */ |
| 237 function onNotificationClosed(notificationId, byUser) { | 431 function onNotificationClosed(notificationId, byUser) { |
| 238 if (byUser) { | 432 if (byUser) { |
| 239 // TODO(vadimt): Analyze possible race conditions between request for cards | 433 tasks.submit(TaskName.DISMISS_CARD, function() { |
| 240 // and dismissal. | 434 // Deleting the notification in case it was re-added while this task was |
| 241 // Send a dismiss request to the server. | 435 // waiting in the queue. |
| 242 var requestParameters = '?id=' + notificationId; | 436 chrome.experimental.notification.clear( |
| 243 var request = new XMLHttpRequest(); | 437 notificationId, |
| 244 request.responseType = 'text'; | 438 function() {}); |
| 245 // TODO(vadimt): If the request fails, for example, because there is no | 439 |
| 246 // internet connection, do retry with exponential backoff. | 440 // Send a dismiss request to the server. |
| 247 request.open( | 441 var requestParameters = '?id=' + notificationId; |
| 248 'GET', | 442 var request = new XMLHttpRequest(); |
| 249 NOTIFICATION_CARDS_URL + '/dismiss' + requestParameters, | 443 request.responseType = 'text'; |
| 250 true); | 444 request.onloadend = function() { |
| 251 request.send(); | 445 tasks.finish(); |
| 252 } | 446 } |
| 253 } | 447 // TODO(vadimt): If the request fails, for example, because there is no |
| 254 | 448 // internet connection, do retry with exponential backoff. |
| 255 /** | 449 request.open( |
| 256 * Schedule next update for notification cards. | 450 'GET', |
| 451 NOTIFICATION_CARDS_URL + '/dismiss' + requestParameters, | |
| 452 true); | |
| 453 tasks.setStepName('onNotificationClosed-send-request'); | |
| 454 request.send(); | |
| 455 }); | |
| 456 } | |
| 457 } | |
| 458 | |
| 459 /** | |
| 460 * Schedules next update for notification cards. | |
| 257 * @param {int} delaySeconds Length of time in seconds after which the alarm | 461 * @param {int} delaySeconds Length of time in seconds after which the alarm |
| 258 * event should fire. | 462 * event should fire. |
| 259 */ | 463 */ |
| 260 function scheduleNextUpdate(delaySeconds) { | 464 function scheduleNextUpdate(delaySeconds) { |
| 261 // Schedule an alarm after the specified delay. 'periodInMinutes' is for the | 465 // Schedule an alarm after the specified delay. 'periodInMinutes' is for the |
| 262 // case when we fail to re-register the alarm. | 466 // case when we fail to re-register the alarm. |
| 263 chrome.alarms.create({ | 467 var alarmInfo = { |
| 264 delayInMinutes: delaySeconds / 60, | 468 delayInMinutes: delaySeconds / 60, |
| 265 periodInMinutes: MAXIMUM_POLLING_PERIOD_SECONDS / 60 | 469 periodInMinutes: MAXIMUM_POLLING_PERIOD_SECONDS / 60 |
| 266 }); | 470 }; |
| 267 } | 471 |
| 268 | 472 chrome.alarms.create(UPDATE_NOTIFICATIONS_ALARM_NAME, alarmInfo); |
| 269 /** | 473 } |
| 270 * Initialize the event page on install or on browser startup. | 474 |
| 475 /** | |
| 476 * Initializes the event page on install or on browser startup. | |
| 271 */ | 477 */ |
| 272 function initialize() { | 478 function initialize() { |
| 273 var initialStorage = { | 479 var initialStorage = { |
| 274 activeNotifications: {}, | 480 activeNotifications: {}, |
| 275 retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS | 481 retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS |
| 276 }; | 482 }; |
| 277 storage.set(initialStorage, updateNotificationsCards); | 483 storage.set(initialStorage); |
| 484 updateNotificationsCards(); | |
| 278 } | 485 } |
| 279 | 486 |
| 280 chrome.runtime.onInstalled.addListener(function(details) { | 487 chrome.runtime.onInstalled.addListener(function(details) { |
| 281 if (details.reason != 'chrome_update') | 488 if (details.reason != 'chrome_update') |
| 282 initialize(); | 489 initialize(); |
| 283 }); | 490 }); |
| 284 | 491 |
| 285 chrome.runtime.onStartup.addListener(function() { | 492 chrome.runtime.onStartup.addListener(function() { |
| 286 initialize(); | 493 initialize(); |
| 287 }); | 494 }); |
| 288 | 495 |
| 289 chrome.alarms.onAlarm.addListener(function(alarm) { | 496 chrome.alarms.onAlarm.addListener(function(alarm) { |
| 290 updateNotificationsCards(); | 497 if (alarm.name == UPDATE_NOTIFICATIONS_ALARM_NAME) |
| 498 updateNotificationsCards(); | |
| 291 }); | 499 }); |
| 292 | 500 |
| 293 chrome.notifications.onClicked.addListener( | 501 chrome.notifications.onClicked.addListener( |
| 294 function(notificationId) { | 502 function(notificationId) { |
| 295 onNotificationClicked(notificationId, function(actionUrls) { | 503 onNotificationClicked(notificationId, function(actionUrls) { |
| 296 return actionUrls.messageUrl; | 504 return actionUrls.messageUrl; |
| 297 }); | 505 }); |
| 298 }); | 506 }); |
| 299 | 507 |
| 300 chrome.notifications.onButtonClicked.addListener( | 508 chrome.notifications.onButtonClicked.addListener( |
| 301 function(notificationId, buttonIndex) { | 509 function(notificationId, buttonIndex) { |
| 302 onNotificationClicked(notificationId, function(actionUrls) { | 510 onNotificationClicked(notificationId, function(actionUrls) { |
| 303 if (!Array.isArray(actionUrls.buttonUrls)) | 511 if (!Array.isArray(actionUrls.buttonUrls)) |
| 304 return undefined; | 512 return undefined; |
| 305 | 513 |
| 306 return actionUrls.buttonUrls[buttonIndex]; | 514 return actionUrls.buttonUrls[buttonIndex]; |
| 307 }); | 515 }); |
| 308 }); | 516 }); |
| 309 | 517 |
| 310 chrome.notifications.onClosed.addListener(onNotificationClosed); | 518 chrome.notifications.onClosed.addListener(onNotificationClosed); |
| OLD | NEW |