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

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

Powered by Google App Engine
This is Rietveld 408576698