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

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: Getting rid of remaining instances of TaskName. 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
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 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 var UPDATE_NOTIFICATIONS_ALARM_NAME = 'UPDATE';
56
55 var storage = chrome.storage.local; 57 var storage = chrome.storage.local;
56 58
57 /** 59 /**
58 * Show a notification and remember information associated with it. 60 * Names for tasks that can be created by the extension.
61 */
62 var UPDATE_CARDS_TASK_NAME = 'update-cards';
rgustafson 2013/03/14 19:05:20 Why did TaskName go away? Why are these and the t
vadimt 2013/03/14 22:56:05 http://en.wikipedia.org/wiki/Layer_(object-oriente
63 var DISMISS_CARD_TASK_NAME = 'dismiss-card';
64 var CARD_CLICKED_TASK_NAME = 'card-clicked';
65
66 /**
67 * Checks if a new task can't be added to a task queue that contains an
68 * existing task.
69 * @param {string} newTaskName Name of the new task.
70 * @param {string} queuedTaskName Name of the task in the queue.
71 * @return {boolean} Whether the new task conflicts with the existing task.
72 */
73 function areTasksConflicting(newTaskName, queuedTaskName) {
74 if (newTaskName == UPDATE_CARDS_TASK_NAME &&
75 queuedTaskName == UPDATE_CARDS_TASK_NAME) {
76 // If a card update is requested while an old update is still in the
77 // queue, we don't need the new update.
78 return true;
79 }
80
rgustafson 2013/03/14 19:05:20 extra line
vadimt 2013/03/14 22:56:05 It makes sense to separate groups of operators wit
81 return false;
82 }
83
84 var tasks = TaskManager(areTasksConflicting);
85
86 /**
87 * Shows a notification and remembers information associated with it.
59 * @param {Object} card Google Now card represented as a set of parameters for 88 * @param {Object} card Google Now card represented as a set of parameters for
60 * showing a Chrome notification. 89 * showing a Chrome notification.
61 * @param {Object} notificationsUrlInfo Map from notification id to the 90 * @param {Object} notificationsUrlInfo Map from notification id to the
62 * notification's set of URLs. 91 * notification's set of URLs.
63 */ 92 */
64 function createNotification(card, notificationsUrlInfo) { 93 function createNotification(card, notificationsUrlInfo) {
65 // Create a notification or quietly update if it already exists. 94 // Create a notification or quietly update if it already exists.
66 // TODO(vadimt): Implement non-quiet updates. 95 // TODO(vadimt): Implement non-quiet updates.
67 chrome.notifications.create( 96 chrome.notifications.create(
68 card.notificationId, 97 card.notificationId,
69 card.notification, 98 card.notification,
70 function(assignedNotificationId) {}); 99 function() {});
71 100
72 notificationsUrlInfo[card.notificationId] = card.actionUrls; 101 notificationsUrlInfo[card.notificationId] = card.actionUrls;
73 } 102 }
74 103
75 /** 104 /**
76 * Parse JSON response from the notification server, show notifications and 105 * Parses JSON response from the notification server, show notifications and
77 * schedule next update. 106 * schedule next update.
78 * @param {string} response Server response. 107 * @param {string} response Server response.
79 */ 108 */
80 function parseAndShowNotificationCards(response) { 109 function parseAndShowNotificationCards(response) {
81 try { 110 try {
82 var parsedResponse = JSON.parse(response); 111 var parsedResponse = JSON.parse(response);
83 } catch (error) { 112 } catch (error) {
84 // TODO(vadimt): Report errors to the user. 113 // TODO(vadimt): Report errors to the user.
85 return; 114 return;
86 } 115 }
87 116
88 var cards = parsedResponse.cards; 117 var cards = parsedResponse.cards;
89 118
90 if (!(cards instanceof Array)) { 119 if (!(cards instanceof Array)) {
91 // TODO(vadimt): Report errors to the user. 120 // TODO(vadimt): Report errors to the user.
92 return; 121 return;
93 } 122 }
94 123
95 if (typeof parsedResponse.expiration_timestamp_seconds != 'number') { 124 if (typeof parsedResponse.expiration_timestamp_seconds != 'number') {
96 // TODO(vadimt): Report errors to the user. 125 // TODO(vadimt): Report errors to the user.
97 return; 126 return;
98 } 127 }
99 128
129 tasks.debugSetStepName(
130 'parseAndShowNotificationCards-get-active-notifications');
100 storage.get('activeNotifications', function(items) { 131 storage.get('activeNotifications', function(items) {
101 // Mark existing notifications that received an update in this server 132 // Mark existing notifications that received an update in this server
102 // response. 133 // response.
103 for (var i = 0; i < cards.length; ++i) { 134 for (var i = 0; i < cards.length; ++i) {
104 var notificationId = cards[i].notificationId; 135 var notificationId = cards[i].notificationId;
105 if (notificationId in items.activeNotifications) 136 if (notificationId in items.activeNotifications)
106 items.activeNotifications[notificationId].hasUpdate = true; 137 items.activeNotifications[notificationId].hasUpdate = true;
107 } 138 }
108 139
109 // Delete notifications that didn't receive an update. 140 // Delete notifications that didn't receive an update.
110 for (var notificationId in items.activeNotifications) 141 for (var notificationId in items.activeNotifications)
111 if (!items.activeNotifications[notificationId].hasUpdate) { 142 if (!items.activeNotifications[notificationId].hasUpdate) {
112 chrome.notifications.clear( 143 chrome.notifications.clear(
113 notificationId, 144 notificationId,
114 function(wasDeleted) {}); 145 function() {});
115 } 146 }
116 147
117 // Create/update notifications and store their new properties. 148 // Create/update notifications and store their new properties.
118 var notificationsUrlInfo = {}; 149 var notificationsUrlInfo = {};
119 150
120 for (var i = 0; i < cards.length; ++i) { 151 for (var i = 0; i < cards.length; ++i) {
121 try { 152 try {
122 createNotification(cards[i], notificationsUrlInfo); 153 createNotification(cards[i], notificationsUrlInfo);
123 } catch (error) { 154 } catch (error) {
124 // TODO(vadimt): Report errors to the user. 155 // TODO(vadimt): Report errors to the user.
125 } 156 }
126 } 157 }
127 storage.set({activeNotifications: notificationsUrlInfo}); 158 storage.set({activeNotifications: notificationsUrlInfo});
128 159
129 scheduleNextUpdate(parsedResponse.expiration_timestamp_seconds); 160 scheduleNextUpdate(parsedResponse.expiration_timestamp_seconds);
130 161
131 // Now that we got a valid response from the server, reset the retry period 162 // Now that we got a valid response from the server, reset the retry period
132 // to the initial value. This retry period will be used the next time we 163 // to the initial value. This retry period will be used the next time we
133 // fail to get the server-provided period. 164 // fail to get the server-provided period.
134 storage.set({retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS}); 165 storage.set({retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS});
166 tasks.finish();
135 }); 167 });
136 } 168 }
137 169
138 /** 170 /**
139 * Request notification cards from the server. 171 * Requests notification cards from the server.
140 * @param {string} requestParameters Query string for the request. 172 * @param {string} requestParameters Query string for the request.
141 */ 173 */
142 function requestNotificationCards(requestParameters) { 174 function requestNotificationCards(requestParameters) {
143 // TODO(vadimt): Figure out how to send user's identity to the server. 175 // TODO(vadimt): Figure out how to send user's identity to the server.
144 var request = new XMLHttpRequest(); 176 var request = new XMLHttpRequest();
145 177
146 request.responseType = 'text'; 178 request.responseType = 'text';
147 request.onload = function(event) { 179 request.onloadend = function() {
148 if (request.status == HTTP_OK) 180 if (request.status == HTTP_OK)
149 parseAndShowNotificationCards(request.response); 181 parseAndShowNotificationCards(request.response);
182 else
183 tasks.finish();
150 } 184 }
151 185
152 request.open( 186 request.open(
153 'GET', 187 'GET',
154 NOTIFICATION_CARDS_URL + '/notifications' + requestParameters, 188 NOTIFICATION_CARDS_URL + '/notifications' + requestParameters,
155 true); 189 true);
190 tasks.debugSetStepName('requestNotificationCards-send-request');
156 request.send(); 191 request.send();
157 } 192 }
158 193
159 /** 194 /**
160 * Request notification cards from the server when we have geolocation. 195 * Requests notification cards from the server when we have geolocation.
161 * @param {Geoposition} position Location of this computer. 196 * @param {Geoposition} position Location of this computer.
162 */ 197 */
163 function requestNotificationCardsWithLocation(position) { 198 function requestNotificationCardsWithLocation(position) {
164 // TODO(vadimt): Should we use 'q' as the parameter name? 199 // TODO(vadimt): Should we use 'q' as the parameter name?
165 var requestParameters = 200 var requestParameters =
166 '?q=' + position.coords.latitude + 201 '?q=' + position.coords.latitude +
167 ',' + position.coords.longitude + 202 ',' + position.coords.longitude +
168 ',' + position.coords.accuracy; 203 ',' + position.coords.accuracy;
169 204
170 requestNotificationCards(requestParameters); 205 requestNotificationCards(requestParameters);
171 } 206 }
172 207
173 /** 208 /**
174 * Request notification cards from the server when we don't have geolocation. 209 * Requests notification cards from the server when we don't have geolocation.
175 * @param {PositionError} positionError Position error. 210 * @param {PositionError} positionError Position error.
176 */ 211 */
177 function requestNotificationCardsWithoutLocation(positionError) { 212 function requestNotificationCardsWithoutLocation(positionError) {
178 requestNotificationCards(''); 213 requestNotificationCards('');
179 } 214 }
180 215
181 /** 216 /**
182 * Obtain new location; request and show notification cards based on this 217 * Obtains new location; requests and shows notification cards based on this
183 * location. 218 * location.
184 */ 219 */
185 function updateNotificationsCards() { 220 function updateNotificationsCards() {
186 storage.get('retryDelaySeconds', function(items) { 221 tasks.submit(UPDATE_CARDS_TASK_NAME, function() {
187 // Immediately schedule the update after the current retry period. Then, 222 tasks.debugSetStepName('updateNotificationsCards-get-retryDelaySeconds');
188 // we'll use update time from the server if available. 223 storage.get('retryDelaySeconds', function(items) {
189 scheduleNextUpdate(items.retryDelaySeconds); 224 // Immediately schedule the update after the current retry period. Then,
190 225 // we'll use update time from the server if available.
191 // TODO(vadimt): Consider interrupting waiting for the next update if we 226 scheduleNextUpdate(items.retryDelaySeconds);
192 // detect that the network conditions have changed. Also, decide whether the 227
193 // exponential backoff is needed both when we are offline and when there are 228 // TODO(vadimt): Consider interrupting waiting for the next update if we
194 // failures on the server side. 229 // detect that the network conditions have changed. Also, decide whether
195 var newRetryDelaySeconds = 230 // the exponential backoff is needed both when we are offline and when
196 Math.min(items.retryDelaySeconds * 2 * (1 + 0.2 * Math.random()), 231 // there are failures on the server side.
197 MAXIMUM_POLLING_PERIOD_SECONDS); 232 var newRetryDelaySeconds =
198 storage.set({retryDelaySeconds: newRetryDelaySeconds}); 233 Math.min(items.retryDelaySeconds * 2 * (1 + 0.2 * Math.random()),
199 234 MAXIMUM_POLLING_PERIOD_SECONDS);
200 navigator.geolocation.getCurrentPosition( 235 storage.set({retryDelaySeconds: newRetryDelaySeconds});
201 requestNotificationCardsWithLocation, 236
202 requestNotificationCardsWithoutLocation); 237 tasks.debugSetStepName('updateNotificationsCards-get-location');
238 navigator.geolocation.getCurrentPosition(
239 requestNotificationCardsWithLocation,
240 requestNotificationCardsWithoutLocation);
241 });
203 }); 242 });
204 } 243 }
205 244
206 /** 245 /**
207 * Opens URL corresponding to the clicked part of the notification. 246 * Opens URL corresponding to the clicked part of the notification.
208 * @param {string} notificationId Unique identifier of the notification. 247 * @param {string} notificationId Unique identifier of the notification.
209 * @param {function(Object): string} selector Function that extracts the url for 248 * @param {function(Object): string} selector Function that extracts the url for
210 * the clicked area from the button action URLs info. 249 * the clicked area from the button action URLs info.
211 */ 250 */
212 function onNotificationClicked(notificationId, selector) { 251 function onNotificationClicked(notificationId, selector) {
213 storage.get('activeNotifications', function(items) { 252 tasks.submit(CARD_CLICKED_TASK_NAME, function() {
214 var actionUrls = items.activeNotifications[notificationId]; 253 tasks.debugSetStepName('onNotificationClicked-get-activeNotifications');
215 if (typeof actionUrls != 'object') { 254 storage.get('activeNotifications', function(items) {
216 // TODO(vadimt): report an error. 255 var actionUrls = items.activeNotifications[notificationId];
217 return; 256 if (typeof actionUrls != 'object') {
218 } 257 // TODO(vadimt): report an error.
219 258 tasks.finish();
220 var url = selector(actionUrls); 259 return;
221 260 }
222 if (typeof url != 'string') { 261
223 // TODO(vadimt): report an error. 262 var url = selector(actionUrls);
224 return; 263
225 } 264 if (typeof url != 'string') {
226 265 // TODO(vadimt): report an error.
227 chrome.tabs.create({url: url}); 266 tasks.finish();
267 return;
268 }
269
270 chrome.tabs.create({url: url});
271 tasks.finish();
272 });
228 }); 273 });
229 } 274 }
230 275
231 /** 276 /**
232 * Callback for chrome.notifications.onClosed event. 277 * Callback for chrome.notifications.onClosed event.
233 * @param {string} notificationId Unique identifier of the notification. 278 * @param {string} notificationId Unique identifier of the notification.
234 * @param {boolean} byUser Whether the notification was closed by the user. 279 * @param {boolean} byUser Whether the notification was closed by the user.
235 */ 280 */
236 function onNotificationClosed(notificationId, byUser) { 281 function onNotificationClosed(notificationId, byUser) {
237 if (byUser) { 282 if (byUser) {
238 // TODO(vadimt): Analyze possible race conditions between request for cards 283 tasks.submit(DISMISS_CARD_TASK_NAME, function() {
239 // and dismissal. 284 // Deleting the notification in case it was re-added while this task was
240 // Send a dismiss request to the server. 285 // waiting in the queue.
241 var requestParameters = '?id=' + notificationId; 286 chrome.notifications.clear(
242 var request = new XMLHttpRequest(); 287 notificationId,
243 request.responseType = 'text'; 288 function() {});
244 // TODO(vadimt): If the request fails, for example, because there is no 289
245 // internet connection, do retry with exponential backoff. 290 // Send a dismiss request to the server.
246 request.open( 291 var requestParameters = '?id=' + notificationId;
247 'GET', 292 var request = new XMLHttpRequest();
248 NOTIFICATION_CARDS_URL + '/dismiss' + requestParameters, 293 request.responseType = 'text';
249 true); 294 request.onloadend = function() {
250 request.send(); 295 tasks.finish();
251 } 296 }
252 } 297 // TODO(vadimt): If the request fails, for example, because there is no
253 298 // internet connection, do retry with exponential backoff.
254 /** 299 request.open(
255 * Schedule next update for notification cards. 300 'GET',
301 NOTIFICATION_CARDS_URL + '/dismiss' + requestParameters,
302 true);
303 tasks.debugSetStepName('onNotificationClosed-send-request');
304 request.send();
305 });
306 }
307 }
308
309 /**
310 * Schedules next update for notification cards.
256 * @param {int} delaySeconds Length of time in seconds after which the alarm 311 * @param {int} delaySeconds Length of time in seconds after which the alarm
257 * event should fire. 312 * event should fire.
258 */ 313 */
259 function scheduleNextUpdate(delaySeconds) { 314 function scheduleNextUpdate(delaySeconds) {
260 // Schedule an alarm after the specified delay. 'periodInMinutes' is for the 315 // Schedule an alarm after the specified delay. 'periodInMinutes' is for the
261 // case when we fail to re-register the alarm. 316 // case when we fail to re-register the alarm.
262 chrome.alarms.create({ 317 var alarmInfo = {
263 delayInMinutes: delaySeconds / 60, 318 delayInMinutes: delaySeconds / 60,
264 periodInMinutes: MAXIMUM_POLLING_PERIOD_SECONDS / 60 319 periodInMinutes: MAXIMUM_POLLING_PERIOD_SECONDS / 60
265 }); 320 };
266 } 321
267 322 chrome.alarms.create(UPDATE_NOTIFICATIONS_ALARM_NAME, alarmInfo);
268 /** 323 }
269 * Initialize the event page on install or on browser startup. 324
325 /**
326 * Initializes the event page on install or on browser startup.
270 */ 327 */
271 function initialize() { 328 function initialize() {
272 var initialStorage = { 329 var initialStorage = {
273 activeNotifications: {}, 330 activeNotifications: {},
274 retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS 331 retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS
275 }; 332 };
276 storage.set(initialStorage, updateNotificationsCards); 333 storage.set(initialStorage);
334 updateNotificationsCards();
277 } 335 }
278 336
279 chrome.runtime.onInstalled.addListener(function(details) { 337 chrome.runtime.onInstalled.addListener(function(details) {
280 if (details.reason != 'chrome_update') 338 if (details.reason != 'chrome_update')
281 initialize(); 339 initialize();
282 }); 340 });
283 341
284 chrome.runtime.onStartup.addListener(function() { 342 chrome.runtime.onStartup.addListener(function() {
285 initialize(); 343 initialize();
286 }); 344 });
287 345
288 chrome.alarms.onAlarm.addListener(function(alarm) { 346 chrome.alarms.onAlarm.addListener(function(alarm) {
289 updateNotificationsCards(); 347 if (alarm.name == UPDATE_NOTIFICATIONS_ALARM_NAME)
348 updateNotificationsCards();
290 }); 349 });
291 350
292 chrome.notifications.onClicked.addListener( 351 chrome.notifications.onClicked.addListener(
293 function(notificationId) { 352 function(notificationId) {
294 onNotificationClicked(notificationId, function(actionUrls) { 353 onNotificationClicked(notificationId, function(actionUrls) {
295 return actionUrls.messageUrl; 354 return actionUrls.messageUrl;
296 }); 355 });
297 }); 356 });
298 357
299 chrome.notifications.onButtonClicked.addListener( 358 chrome.notifications.onButtonClicked.addListener(
300 function(notificationId, buttonIndex) { 359 function(notificationId, buttonIndex) {
301 onNotificationClicked(notificationId, function(actionUrls) { 360 onNotificationClicked(notificationId, function(actionUrls) {
302 if (!Array.isArray(actionUrls.buttonUrls)) 361 if (!Array.isArray(actionUrls.buttonUrls))
303 return undefined; 362 return undefined;
304 363
305 return actionUrls.buttonUrls[buttonIndex]; 364 return actionUrls.buttonUrls[buttonIndex];
306 }); 365 });
307 }); 366 });
308 367
309 chrome.notifications.onClosed.addListener(onNotificationClosed); 368 chrome.notifications.onClosed.addListener(onNotificationClosed);
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698