Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(65)

Side by Side Diff: chrome/browser/resources/google_now/background.js

Issue 12316075: Preventing race conditions in Google Now extension (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Rebase/switch to notifications.clear Created 7 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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 errors to the user. 25 // TODO(vadimt): Report errors to the user.
26 26
27 // TODO(vadimt): Figure out the server name. Use it in the manifest and for 27 // TODO(vadimt): Figure out the server name. Use it in the manifest and for
28 // TODO(vadimt): Consider processing errors for all storage.set calls.
29 // NOTIFICATION_CARDS_URL. Meanwhile, to use the feature, you need to manually 28 // NOTIFICATION_CARDS_URL. Meanwhile, to use the feature, you need to manually
30 // edit NOTIFICATION_CARDS_URL before building Chrome. 29 // edit NOTIFICATION_CARDS_URL before building Chrome.
30 // TODO(vadimt): Consider processing errors for all storage.set calls.
31
31 /** 32 /**
32 * URL to retrieve notification cards. 33 * URL to retrieve notification cards.
33 */ 34 */
34 var NOTIFICATION_CARDS_URL = ''; 35 var NOTIFICATION_CARDS_URL = '';
35 36
36 /** 37 /**
37 * 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
38 * code the server will send. 39 * code the server will send.
39 */ 40 */
40 var HTTP_OK = 200; 41 var HTTP_OK = 200;
41 42
42 /** 43 /**
43 * 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
44 * period from the server is not available. 45 * period from the server is not available.
45 */ 46 */
46 var INITIAL_POLLING_PERIOD_SECONDS = 300; // 5 minutes 47 var INITIAL_POLLING_PERIOD_SECONDS = 300; // 5 minutes
47 48
48 /** 49 /**
49 * 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
50 * period from the server is not available. 51 * period from the server is not available.
51 */ 52 */
52 var MAXIMUM_POLLING_PERIOD_SECONDS = 3600; // 1 hour 53 var MAXIMUM_POLLING_PERIOD_SECONDS = 3600; // 1 hour
53 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
54 var storage = chrome.storage.local; 67 var storage = chrome.storage.local;
55 68
69 /////////////////////////////////////////////////////////////////////////
70 ////// ABSOLUTELY NOT FORGET TO MOVE ASSERT AND TASK MANAGER INTO SEPARATE FILE
71 ////// IN THIS CL !!!!!!!!!!!!!
72 ////////////////////////////////////////////////////////////////////////
73
74 /**
75 * Check 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 assert(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);
skare_ 2013/03/02 00:07:05 console.assert()?
skare_ 2013/03/02 00:07:05 I didn't think alert() from a background page work
vadimt 2013/03/06 21:04:38 Then no one will notice it. I'd prefer these impor
vadimt 2013/03/06 21:04:38 We checked that it works, remember :) ?
skare_ 2013/03/06 22:35:57 [ok] yes, but that was after this set of comments
vadimt 2013/03/06 22:56:35 No doubt about that.
skare_ 2013/03/07 23:25:13 bringing this comment back from the dead. Would co
vadimt 2013/03/08 21:11:58 Renamed assert to verify. This assert is for thin
rgustafson 2013/03/11 18:11:04 If it's stuff that should never happen and this wi
skare_ 2013/03/11 18:37:12 re: the alert() line, would it be ok to not show a
vadimt 2013/03/11 21:31:53 The assert shouldn't happen in a different sense :
vadimt 2013/03/11 21:31:53 RE: re: the alert() line Per offline talk, leaving
86 }
87 }
88
89 /**
90 * Build the closure that manages tasks (mutually exclusive chains of events).
skare_ 2013/03/02 00:07:05 you could omit "build the closure" and just say e.
vadimt 2013/03/06 21:04:38 Done.
91 * @return {Object} Set of methods to control 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.
skare_ 2013/03/07 23:25:13 @type this -- {Array.<Task>} // where you'd typede
vadimt 2013/03/08 21:11:58 Coool
108 */
109 var queue = [];
110
111 /**
112 * Name of the current step of the currently running task if present,
113 * otherwise, null. For diagnostics only.
114 * It's set when the task is started and before each asynchronous operation.
115 */
116 var stepName = null;
117
118 /**
119 * Start the first queued task.
120 */
121 function startFirst() {
rgustafson 2013/03/04 22:31:27 startFirstTask() makes this more self explanatory
vadimt 2013/03/06 21:04:38 The idea is that these are members of TaskManager,
122 // Set alarm to verify that the event page will unload in a reasonable time.
123 chrome.alarms.create(CANNOT_UNLOAD_ALARM_NAME,
124 {delayInMinutes: MAXIMUM_LOADED_TIME_MINUTES});
125
126 // Start the oldest queued task, but not remove it from the queue.
skare_ 2013/03/02 00:07:05 nit: s/not/don't
vadimt 2013/03/06 21:04:38 Done.
127 assert(stepName == null, 'tasks.startFirst: stepName is not null');
128 var entry = queue[0];
skare_ 2013/03/02 00:07:05 maybe bail out if queue is empty here?
rgustafson 2013/03/04 22:31:27 This seems to be called only when there is a first
vadimt 2013/03/06 21:04:38 The method will be called only when the queue is n
vadimt 2013/03/06 21:04:38 Done.
vadimt 2013/03/06 21:04:38 Done.
skare_ 2013/03/06 22:35:57 I still like bailing out on an empty queue (or lik
skare_ 2013/03/07 23:25:13 [bringing this into latest round of comments too]
vadimt 2013/03/08 21:11:58 Per the offline talk, seems we are proceeding as i
skare_ 2013/03/11 18:37:12 so long as whatever the assert() is replaced with
vadimt 2013/03/11 21:31:53 Added to verify(): // TODO(vadimt): Make sure the
129 stepName = entry.name + '-initial';
130 entry.task();
131 }
132
133 /**
134 * Check if a new task can be added to a queue that contains an existing task.
135 * @param {string} newTaskName Name of the new task.
136 * @param {string} queuedTaskName Name of the task in the queue.
137 * @return {boolean} Whether the new task doesn't conflict with the existing
skare_ 2013/03/02 00:07:05 maybe invert logic -- isCompatible -> tasksConflic
vadimt 2013/03/06 21:04:38 Done.
138 * task.
139 */
140 function isCompatible(newTaskName, queuedTaskName) {
141 if (newTaskName == TaskName.UPDATE_CARDS &&
142 queuedTaskName == TaskName.UPDATE_CARDS) {
143 // If a card update is requested while an old update is still in the
144 // queue, we don't need the new update.
145 return false;
146 }
147
148 return true;
149 }
150
151 /**
152 * Check if a new task can be added to the task queue.
skare_ 2013/03/02 00:07:05 nit (throughout comments in the file): this is in
vadimt 2013/03/06 21:04:38 Done.
153 * @param {TaskName} taskName Name of the new task.
154 * @return {boolean} Whether the new task can be added.
155 */
156 function canQueue(taskName) {
157 for (var i = 0; i < queue.length; ++i)
158 if (!isCompatible(taskName, queue[i].name))
159 return false;
160
161 return true;
162 }
163
164 /**
165 * Add a new task. If another task is not running, run the task immediately.
166 * If any task in the queue is not compatible with the task, ignore the new
167 * task. Otherwise, store the task for future execution.
168 * @param {TaskName} taskName Name of the task.
169 * @param {function()} task Code of the task.
170 */
171 function submit(taskName, task) {
172 if (!canQueue(taskName))
173 return;
174
175 queue.push({name: taskName, task: task});
176
177 if (queue.length == 1) {
178 startFirst();
skare_ 2013/03/02 00:07:05 it's ok as used now now, but you're ok with the fa
vadimt 2013/03/06 21:04:38 Yes, I think that it's better than starting asynch
179 }
180 }
181
182 /**
183 * Complete the current task and start the next queued task if available.
184 */
185 function finish() {
skare_ 2013/03/02 00:07:05 high-level comment: a lot of JS patterns end up ha
vadimt 2013/03/06 21:04:38 I've considered this. I thought that this would be
skare_ 2013/03/06 22:35:57 feel free to get other people to comment here but
vadimt 2013/03/06 22:56:35 Done.
186 assert(queue.length >= 1, 'tasks.finish: The task queue is empty.');
187 queue.shift();
188 stepName = null;
189
190 if (queue.length >= 1)
191 startFirst();
192 }
193
194 /**
195 * Associate a name with the current step of the task. Used for diagnostics
196 * only. A task is a chain of asynchronous events; setStep should be called
197 * before starting any asynchronous operation.
198 * @param {string} step Name of new step.
199 */
200 function setStep(step) {
rgustafson 2013/03/04 22:31:27 Is there a reason you don't use this inside the Ta
vadimt 2013/03/06 21:04:38 One day, for example, I may want to add a "stepNam
skare_ 2013/03/06 22:35:57 more general question -- could state name be instr
vadimt 2013/03/06 22:56:35 The plan about this is: in the release, we'll prob
skare_ 2013/03/07 23:25:13 would UMA error counters be slightly more granular
vadimt 2013/03/08 21:11:58 May be, yes, may be, no. We'll have to decide then
rgustafson 2013/03/11 18:11:05 Are we really expecting our event page being impro
skare_ 2013/03/11 18:37:12 re: 'parent name' I meant hypothetically myTask.se
skare_ 2013/03/11 19:46:26 ignore setStep() naming comment; resolved in next
vadimt 2013/03/11 21:31:53 If I knew situations that cannot be caught with te
vadimt 2013/03/11 21:31:53 RE: re: 'parent name' I meant hypothetically In th
vadimt 2013/03/11 21:31:53 (In a gloom voice:) Too late. Already renamed to d
201 stepName = step;
202 }
203
204 chrome.alarms.onAlarm.addListener(function(alarm) {
205 if (alarm.name == CANNOT_UNLOAD_ALARM_NAME) {
206 // Error if the event page wasn't unloaded after a reasonable timeout
207 // since starting the last task.
208 // TODO(vadimt): Uncomment the assert once this bug is fixed:
209 // crbug.com/177563
210 // assert(false, 'Event page didn\'t unload, queue = ' +
211 // JSON.stringify(tasks) + ', step = ' + stepName + ' (ignore this assert
212 // if devtools is attached).');
213 }
214 });
215
216 chrome.runtime.onSuspend.addListener(function() {
217 chrome.alarms.clear(CANNOT_UNLOAD_ALARM_NAME);
218 assert(queue.length == 0 && stepName == null,
219 'Incomplete task when unloading event page, queue = ' +
220 JSON.stringify(queue) + ', step = ' + stepName);
221 });
222
223 return {
224 submit: submit,
225 setStep: setStep,
226 finish: finish
227 };
228 }
229
230 var tasks = TaskManager();
231
56 /** 232 /**
57 * Show a notification and remember information associated with it. 233 * Show a notification and remember information associated with it.
58 * @param {Object} card Google Now card represented as a set of parameters for 234 * @param {Object} card Google Now card represented as a set of parameters for
59 * showing a Chrome notification. 235 * showing a Chrome notification.
60 * @param {Object} notificationsUrlInfo Map from notification id to the 236 * @param {Object} notificationsUrlInfo Map from notification id to the
61 * notification's set of URLs. 237 * notification's set of URLs.
62 */ 238 */
63 function createNotification(card, notificationsUrlInfo) { 239 function createNotification(card, notificationsUrlInfo) {
64 // Create a notification or quietly update if it already exists. 240 // Create a notification or quietly update if it already exists.
65 // TODO(vadimt): Implement non-quiet updates. 241 // TODO(vadimt): Implement non-quiet updates.
66 chrome.experimental.notification.create( 242 chrome.experimental.notification.create(
67 card.notificationId, 243 card.notificationId,
68 card.notification, 244 card.notification,
69 function(assignedNotificationId) {}); 245 function() {});
70 246
71 notificationsUrlInfo[card.notificationId] = card.actionUrls; 247 notificationsUrlInfo[card.notificationId] = card.actionUrls;
72 } 248 }
73 249
74 /** 250 /**
75 * Parse JSON response from the notification server, show notifications and 251 * Parse JSON response from the notification server, show notifications and
76 * schedule next update. 252 * schedule next update.
77 * @param {string} response Server response. 253 * @param {string} response Server response.
78 */ 254 */
79 function parseAndShowNotificationCards(response) { 255 function parseAndShowNotificationCards(response) {
80 try { 256 try {
81 var parsedResponse = JSON.parse(response); 257 var parsedResponse = JSON.parse(response);
82 } catch (error) { 258 } catch (error) {
83 // TODO(vadimt): Report errors to the user. 259 // TODO(vadimt): Report errors to the user.
84 return; 260 return;
85 } 261 }
86 262
87 var cards = parsedResponse.cards; 263 var cards = parsedResponse.cards;
88 264
89 if (!(cards instanceof Array)) { 265 if (!(cards instanceof Array)) {
90 // TODO(vadimt): Report errors to the user. 266 // TODO(vadimt): Report errors to the user.
91 return; 267 return;
92 } 268 }
93 269
94 if (typeof parsedResponse.expiration_timestamp_seconds != 'number') { 270 if (typeof parsedResponse.expiration_timestamp_seconds != 'number') {
95 // TODO(vadimt): Report errors to the user. 271 // TODO(vadimt): Report errors to the user.
96 return; 272 return;
97 } 273 }
98 274
275 tasks.setStep('parseAndShowNotificationCards-get-active-notifications');
99 storage.get('activeNotifications', function(items) { 276 storage.get('activeNotifications', function(items) {
100 // Mark existing notifications that received an update in this server 277 // Mark existing notifications that received an update in this server
101 // response. 278 // response.
102 for (var i = 0; i < cards.length; ++i) { 279 for (var i = 0; i < cards.length; ++i) {
103 var notificationId = cards[i].notificationId; 280 var notificationId = cards[i].notificationId;
104 if (notificationId in items.activeNotifications) 281 if (notificationId in items.activeNotifications)
105 items.activeNotifications[notificationId].hasUpdate = true; 282 items.activeNotifications[notificationId].hasUpdate = true;
106 } 283 }
107 284
108 // Delete notifications that didn't receive an update. 285 // Delete notifications that didn't receive an update.
109 for (var notificationId in items.activeNotifications) 286 for (var notificationId in items.activeNotifications)
110 if (!items.activeNotifications[notificationId].hasUpdate) { 287 if (!items.activeNotifications[notificationId].hasUpdate) {
111 chrome.experimental.notification.clear( 288 chrome.experimental.notification.clear(
112 notificationId, 289 notificationId,
113 function(wasDeleted) {}); 290 function() {});
114 } 291 }
115 292
116 // Create/update notifications and store their new properties. 293 // Create/update notifications and store their new properties.
117 var notificationsUrlInfo = {}; 294 var notificationsUrlInfo = {};
118 295
119 for (var i = 0; i < cards.length; ++i) { 296 for (var i = 0; i < cards.length; ++i) {
120 try { 297 try {
121 createNotification(cards[i], notificationsUrlInfo); 298 createNotification(cards[i], notificationsUrlInfo);
122 } catch (error) { 299 } catch (error) {
123 // TODO(vadimt): Report errors to the user. 300 // TODO(vadimt): Report errors to the user.
124 } 301 }
125 } 302 }
126 storage.set({activeNotifications: notificationsUrlInfo}); 303 storage.set({activeNotifications: notificationsUrlInfo});
127 304
128 scheduleNextUpdate(parsedResponse.expiration_timestamp_seconds); 305 scheduleNextUpdate(parsedResponse.expiration_timestamp_seconds);
129 306
130 // Now that we got a valid response from the server, reset the retry period 307 // Now that we got a valid response from the server, reset the retry period
131 // to the initial value. This retry period will be used the next time we 308 // to the initial value. This retry period will be used the next time we
132 // fail to get the server-provided period. 309 // fail to get the server-provided period.
133 storage.set({retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS}); 310 storage.set({retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS});
311 tasks.finish();
134 }); 312 });
135 } 313 }
136 314
137 /** 315 /**
138 * Request notification cards from the server. 316 * Request notification cards from the server.
139 * @param {string} requestParameters Query string for the request. 317 * @param {string} requestParameters Query string for the request.
140 */ 318 */
141 function requestNotificationCards(requestParameters) { 319 function requestNotificationCards(requestParameters) {
142 // TODO(vadimt): Figure out how to send user's identity to the server. 320 // TODO(vadimt): Figure out how to send user's identity to the server.
143 var request = new XMLHttpRequest(); 321 var request = new XMLHttpRequest();
144 322
145 request.responseType = 'text'; 323 request.responseType = 'text';
146 request.onload = function(event) { 324 request.onload = function() {
147 if (request.status == HTTP_OK) 325 if (request.status == HTTP_OK)
148 parseAndShowNotificationCards(request.response); 326 parseAndShowNotificationCards(request.response);
327 else
328 tasks.finish();
329 }
330
331 request.onerror = function() {
rgustafson 2013/03/04 22:31:27 Are there any other callbacks we need to handle li
vadimt 2013/03/06 21:04:38 Done.
332 tasks.finish();
149 } 333 }
150 334
151 request.open( 335 request.open(
152 'GET', 336 'GET',
153 NOTIFICATION_CARDS_URL + '/notifications' + requestParameters, 337 NOTIFICATION_CARDS_URL + '/notifications' + requestParameters,
154 true); 338 true);
339 tasks.setStep('requestNotificationCards-send-request');
155 request.send(); 340 request.send();
156 } 341 }
157 342
158 /** 343 /**
159 * Request notification cards from the server when we have geolocation. 344 * Request notification cards from the server when we have geolocation.
160 * @param {Geoposition} position Location of this computer. 345 * @param {Geoposition} position Location of this computer.
161 */ 346 */
162 function requestNotificationCardsWithLocation(position) { 347 function requestNotificationCardsWithLocation(position) {
163 // TODO(vadimt): Should we use 'q' as the parameter name? 348 // TODO(vadimt): Should we use 'q' as the parameter name?
164 var requestParameters = 349 var requestParameters =
(...skipping 10 matching lines...) Expand all
175 */ 360 */
176 function requestNotificationCardsWithoutLocation(positionError) { 361 function requestNotificationCardsWithoutLocation(positionError) {
177 requestNotificationCards(''); 362 requestNotificationCards('');
178 } 363 }
179 364
180 /** 365 /**
181 * Obtain new location; request and show notification cards based on this 366 * Obtain new location; request and show notification cards based on this
182 * location. 367 * location.
183 */ 368 */
184 function updateNotificationsCards() { 369 function updateNotificationsCards() {
185 storage.get('retryDelaySeconds', function(items) { 370 tasks.submit(TaskName.UPDATE_CARDS, function() {
186 // Immediately schedule the update after the current retry period. Then, 371 tasks.setStep('updateNotificationsCards-get-retryDelaySeconds');
187 // we'll use update time from the server if available. 372 storage.get('retryDelaySeconds', function(items) {
188 scheduleNextUpdate(items.retryDelaySeconds); 373 // Immediately schedule the update after the current retry period. Then,
374 // we'll use update time from the server if available.
375 scheduleNextUpdate(items.retryDelaySeconds);
189 376
190 // TODO(vadimt): Consider interrupting waiting for the next update if we 377 // TODO(vadimt): Consider interrupting waiting for the next update if we
191 // detect that the network conditions have changed. Also, decide whether the 378 // detect that the network conditions have changed. Also, decide whether
192 // exponential backoff is needed both when we are offline and when there are 379 // the exponential backoff is needed both when we are offline and when
193 // failures on the server side. 380 // there are failures on the server side.
194 var newRetryDelaySeconds = 381 var newRetryDelaySeconds =
195 Math.min(items.retryDelaySeconds * 2 * (1 + 0.2 * Math.random()), 382 Math.min(items.retryDelaySeconds * 2 * (1 + 0.2 * Math.random()),
196 MAXIMUM_POLLING_PERIOD_SECONDS); 383 MAXIMUM_POLLING_PERIOD_SECONDS);
197 storage.set({retryDelaySeconds: newRetryDelaySeconds}); 384 storage.set({retryDelaySeconds: newRetryDelaySeconds});
198 385
199 navigator.geolocation.getCurrentPosition( 386 tasks.setStep('updateNotificationsCards-get-location');
200 requestNotificationCardsWithLocation, 387 navigator.geolocation.getCurrentPosition(
201 requestNotificationCardsWithoutLocation); 388 requestNotificationCardsWithLocation,
389 requestNotificationCardsWithoutLocation);
390 });
202 }); 391 });
203 } 392 }
204 393
205 /** 394 /**
206 * Opens URL corresponding to the clicked part of the notification. 395 * Opens URL corresponding to the clicked part of the notification.
207 * @param {string} notificationId Unique identifier of the notification. 396 * @param {string} notificationId Unique identifier of the notification.
208 * @param {function(Object): string} selector Function that extracts the url for 397 * @param {function(Object): string} selector Function that extracts the url for
209 * the clicked area from the button action URLs info. 398 * the clicked area from the button action URLs info.
210 */ 399 */
211 function onNotificationClicked(notificationId, selector) { 400 function onNotificationClicked(notificationId, selector) {
212 storage.get('activeNotifications', function(items) { 401 tasks.submit(TaskName.CARD_CLICKED, function() {
213 var actionUrls = items.activeNotifications[notificationId]; 402 tasks.setStep('onNotificationClicked-get-activeNotifications');
214 if (typeof actionUrls != 'object') { 403 storage.get('activeNotifications', function(items) {
215 // TODO(vadimt): report an error. 404 var actionUrls = items.activeNotifications[notificationId];
216 return; 405 if (typeof actionUrls != 'object') {
217 } 406 // TODO(vadimt): report an error.
407 tasks.finish();
408 return;
409 }
218 410
219 var url = selector(actionUrls); 411 var url = selector(actionUrls);
220 412
221 if (typeof url != 'string') { 413 if (typeof url != 'string') {
222 // TODO(vadimt): report an error. 414 // TODO(vadimt): report an error.
223 return; 415 tasks.finish();
224 } 416 return;
417 }
225 418
226 chrome.tabs.create({url: url}); 419 chrome.tabs.create({url: url});
420 tasks.finish();
421 });
227 }); 422 });
228 } 423 }
229 424
230 /** 425 /**
231 * Callback for chrome.experimental.notification.onClosed event. 426 * Callback for chrome.experimental.notification.onClosed event.
232 * @param {string} notificationId Unique identifier of the notification. 427 * @param {string} notificationId Unique identifier of the notification.
233 * @param {boolean} byUser Whether the notification was closed by the user. 428 * @param {boolean} byUser Whether the notification was closed by the user.
234 */ 429 */
235 function onNotificationClosed(notificationId, byUser) { 430 function onNotificationClosed(notificationId, byUser) {
236 if (byUser) { 431 if (byUser) {
237 // TODO(vadimt): Analyze possible race conditions between request for cards 432 tasks.submit(TaskName.DISMISS_CARD, function() {
238 // and dismissal. 433 // Deleting the notification in case it was re-added while this task was
239 // Send a dismiss request to the server. 434 // waiting in the queue.
240 var requestParameters = '?id=' + notificationId; 435 chrome.experimental.notification.clear(
rgustafson 2013/03/04 22:31:27 This is going to have a notification pop up for a
vadimt 2013/03/06 21:04:38 The reasoning for not doing this is that I expect
rgustafson 2013/03/11 18:11:05 I'm okay with this for now. Eventually, I think th
skare_ 2013/03/11 18:37:12 especially if there's ever the ability to have the
241 var request = new XMLHttpRequest(); 436 notificationId,
242 request.responseType = 'text'; 437 function() {});
243 // TODO(vadimt): If the request fails, for example, because there is no 438
244 // internet connection, do retry with exponential backoff. 439 // Send a dismiss request to the server.
245 request.open( 440 var requestParameters = '?id=' + notificationId;
246 'GET', 441 var request = new XMLHttpRequest();
247 NOTIFICATION_CARDS_URL + '/dismiss' + requestParameters, 442 request.responseType = 'text';
248 true); 443 request.onloadend = function() {
249 request.send(); 444 tasks.finish();
445 }
446 // TODO(vadimt): If the request fails, for example, because there is no
447 // internet connection, do retry with exponential backoff.
448 request.open(
449 'GET',
450 NOTIFICATION_CARDS_URL + '/dismiss' + requestParameters,
451 true);
452 tasks.setStep('onNotificationClosed-send-request');
453 request.send();
454 });
250 } 455 }
251 } 456 }
252 457
253 /** 458 /**
254 * Schedule next update for notification cards. 459 * Schedule next update for notification cards.
255 * @param {int} delaySeconds Length of time in seconds after which the alarm 460 * @param {int} delaySeconds Length of time in seconds after which the alarm
256 * event should fire. 461 * event should fire.
257 */ 462 */
258 function scheduleNextUpdate(delaySeconds) { 463 function scheduleNextUpdate(delaySeconds) {
259 // Schedule an alarm after the specified delay. 'periodInMinutes' is for the 464 // Schedule an alarm after the specified delay. 'periodInMinutes' is for the
260 // case when we fail to re-register the alarm. 465 // case when we fail to re-register the alarm.
261 chrome.alarms.create({ 466 var alarmInfo = {
262 delayInMinutes: delaySeconds / 60, 467 delayInMinutes: delaySeconds / 60,
263 periodInMinutes: MAXIMUM_POLLING_PERIOD_SECONDS / 60 468 periodInMinutes: MAXIMUM_POLLING_PERIOD_SECONDS / 60
264 }); 469 };
470
471 chrome.alarms.create(UPDATE_NOTIFICATIONS_ALARM_NAME, alarmInfo);
265 } 472 }
266 473
267 /** 474 /**
268 * Initialize the event page on install or on browser startup. 475 * Initialize the event page on install or on browser startup.
269 */ 476 */
270 function initialize() { 477 function initialize() {
271 var initialStorage = { 478 var initialStorage = {
272 activeNotifications: {}, 479 activeNotifications: {},
273 retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS 480 retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS
274 }; 481 };
275 storage.set(initialStorage, updateNotificationsCards); 482 storage.set(initialStorage);
483 updateNotificationsCards();
rgustafson 2013/03/04 22:31:27 Couldn't retryDelaySeconds not be set when update
vadimt 2013/03/06 21:04:38 I had a long conversation with extensions guys who
rgustafson 2013/03/11 18:11:05 What exactly was their promise? A set followed by
vadimt 2013/03/11 21:31:53 Literally: If get is executed after set(X), 'get'
276 } 484 }
277 485
278 chrome.runtime.onInstalled.addListener(function(details) { 486 chrome.runtime.onInstalled.addListener(function(details) {
279 if (details.reason != 'chrome_update') 487 if (details.reason != 'chrome_update')
280 initialize(); 488 initialize();
281 }); 489 });
282 490
283 chrome.runtime.onStartup.addListener(function() { 491 chrome.runtime.onStartup.addListener(function() {
284 initialize(); 492 initialize();
285 }); 493 });
286 494
287 chrome.alarms.onAlarm.addListener(function(alarm) { 495 chrome.alarms.onAlarm.addListener(function(alarm) {
288 updateNotificationsCards(); 496 if (alarm.name == UPDATE_NOTIFICATIONS_ALARM_NAME)
497 updateNotificationsCards();
289 }); 498 });
290 499
291 chrome.experimental.notification.onClicked.addListener( 500 chrome.experimental.notification.onClicked.addListener(
292 function(notificationId) { 501 function(notificationId) {
293 onNotificationClicked(notificationId, function(actionUrls) { 502 onNotificationClicked(notificationId, function(actionUrls) {
294 return actionUrls.messageUrl; 503 return actionUrls.messageUrl;
295 }); 504 });
296 }); 505 });
297 506
298 chrome.experimental.notification.onButtonClicked.addListener( 507 chrome.experimental.notification.onButtonClicked.addListener(
299 function(notificationId, buttonIndex) { 508 function(notificationId, buttonIndex) {
300 onNotificationClicked(notificationId, function(actionUrls) { 509 onNotificationClicked(notificationId, function(actionUrls) {
301 if (!Array.isArray(actionUrls.buttonUrls)) 510 if (!Array.isArray(actionUrls.buttonUrls))
302 return undefined; 511 return undefined;
303 512
304 return actionUrls.buttonUrls[buttonIndex]; 513 return actionUrls.buttonUrls[buttonIndex];
305 }); 514 });
306 }); 515 });
307 516
308 chrome.experimental.notification.onClosed.addListener(onNotificationClosed); 517 chrome.experimental.notification.onClosed.addListener(onNotificationClosed);
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698