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

Unified 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: arv@ comments. 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 side-by-side diff with in-line comments
Download patch
Index: chrome/browser/resources/google_now/background.js
diff --git a/chrome/browser/resources/google_now/background.js b/chrome/browser/resources/google_now/background.js
index 2646154d2185a78e844486cf684ae670b7880fea..f55be5d556f5081c63145eb908d5a7def164b2a2 100644
--- a/chrome/browser/resources/google_now/background.js
+++ b/chrome/browser/resources/google_now/background.js
@@ -1,309 +1,369 @@
-// Copyright (c) 2013 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-'use strict';
-
-/**
- * @fileoverview The event page for Google Now for Chrome implementation.
- * The Google Now event page gets Google Now cards from the server and shows
- * them as Chrome notifications.
- * The service performs periodic updating of Google Now cards.
- * Each updating of the cards includes 3 steps:
- * 1. Obtaining the location of the machine;
- * 2. Making a server request based on that location;
- * 3. Showing the received cards as notifications.
- */
-
-// TODO(vadimt): Use background permission to show notifications even when all
-// browser windows are closed.
-// TODO(vadimt): Remove the C++ implementation.
-// TODO(vadimt): Decide what to do in incognito mode.
-// TODO(vadimt): Gather UMAs.
-// TODO(vadimt): Honor the flag the enables Google Now integration.
-// TODO(vadimt): Figure out the final values of the constants.
-// TODO(vadimt): Report internal and server errors. Collect UMAs on errors where
-// appropriate. Also consider logging errors or throwing exceptions.
-
-// TODO(vadimt): Consider processing errors for all storage.set calls.
-// TODO(vadimt): Figure out the server name. Use it in the manifest and for
-// NOTIFICATION_CARDS_URL. Meanwhile, to use the feature, you need to manually
-// set the server name via local storage.
-/**
- * URL to retrieve notification cards.
- */
-var NOTIFICATION_CARDS_URL = localStorage['server_url'];
-
-/**
- * Standard response code for successful HTTP requests. This is the only success
- * code the server will send.
- */
-var HTTP_OK = 200;
-
-/**
- * Initial period for polling for Google Now Notifications cards to use when the
- * period from the server is not available.
- */
-var INITIAL_POLLING_PERIOD_SECONDS = 300; // 5 minutes
-
-/**
- * Maximal period for polling for Google Now Notifications cards to use when the
- * period from the server is not available.
- */
-var MAXIMUM_POLLING_PERIOD_SECONDS = 3600; // 1 hour
-
-var storage = chrome.storage.local;
-
-/**
- * Show a notification and remember information associated with it.
- * @param {Object} card Google Now card represented as a set of parameters for
- * showing a Chrome notification.
- * @param {Object} notificationsUrlInfo Map from notification id to the
- * notification's set of URLs.
- */
-function createNotification(card, notificationsUrlInfo) {
- // Create a notification or quietly update if it already exists.
- // TODO(vadimt): Implement non-quiet updates.
- chrome.notifications.create(
- card.notificationId,
- card.notification,
- function(assignedNotificationId) {});
-
- notificationsUrlInfo[card.notificationId] = card.actionUrls;
-}
-
-/**
- * Parse JSON response from the notification server, show notifications and
- * schedule next update.
- * @param {string} response Server response.
- */
-function parseAndShowNotificationCards(response) {
- try {
- var parsedResponse = JSON.parse(response);
- } catch (error) {
- // TODO(vadimt): Report errors to the user.
- return;
- }
-
- var cards = parsedResponse.cards;
-
- if (!(cards instanceof Array)) {
- // TODO(vadimt): Report errors to the user.
- return;
- }
-
- if (typeof parsedResponse.expiration_timestamp_seconds != 'number') {
- // TODO(vadimt): Report errors to the user.
- return;
- }
-
- storage.get('activeNotifications', function(items) {
- // Mark existing notifications that received an update in this server
- // response.
- for (var i = 0; i < cards.length; ++i) {
- var notificationId = cards[i].notificationId;
- if (notificationId in items.activeNotifications)
- items.activeNotifications[notificationId].hasUpdate = true;
- }
-
- // Delete notifications that didn't receive an update.
- for (var notificationId in items.activeNotifications)
- if (!items.activeNotifications[notificationId].hasUpdate) {
- chrome.notifications.clear(
- notificationId,
- function(wasDeleted) {});
- }
-
- // Create/update notifications and store their new properties.
- var notificationsUrlInfo = {};
-
- for (var i = 0; i < cards.length; ++i) {
- try {
- createNotification(cards[i], notificationsUrlInfo);
- } catch (error) {
- // TODO(vadimt): Report errors to the user.
- }
- }
- storage.set({activeNotifications: notificationsUrlInfo});
-
- scheduleNextUpdate(parsedResponse.expiration_timestamp_seconds);
-
- // Now that we got a valid response from the server, reset the retry period
- // to the initial value. This retry period will be used the next time we
- // fail to get the server-provided period.
- storage.set({retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS});
- });
-}
-
-/**
- * Request notification cards from the server.
- * @param {string} requestParameters Query string for the request.
- */
-function requestNotificationCards(requestParameters) {
- // TODO(vadimt): Figure out how to send user's identity to the server.
- var request = new XMLHttpRequest();
-
- request.responseType = 'text';
- request.onload = function(event) {
- if (request.status == HTTP_OK)
- parseAndShowNotificationCards(request.response);
- }
-
- request.open(
- 'GET',
- NOTIFICATION_CARDS_URL + '/notifications' + requestParameters,
- true);
- request.send();
-}
-
-/**
- * Request notification cards from the server when we have geolocation.
- * @param {Geoposition} position Location of this computer.
- */
-function requestNotificationCardsWithLocation(position) {
- // TODO(vadimt): Should we use 'q' as the parameter name?
- var requestParameters =
- '?q=' + position.coords.latitude +
- ',' + position.coords.longitude +
- ',' + position.coords.accuracy;
-
- requestNotificationCards(requestParameters);
-}
-
-/**
- * Request notification cards from the server when we don't have geolocation.
- * @param {PositionError} positionError Position error.
- */
-function requestNotificationCardsWithoutLocation(positionError) {
- requestNotificationCards('');
-}
-
-/**
- * Obtain new location; request and show notification cards based on this
- * location.
- */
-function updateNotificationsCards() {
- storage.get('retryDelaySeconds', function(items) {
- // Immediately schedule the update after the current retry period. Then,
- // we'll use update time from the server if available.
- scheduleNextUpdate(items.retryDelaySeconds);
-
- // TODO(vadimt): Consider interrupting waiting for the next update if we
- // detect that the network conditions have changed. Also, decide whether the
- // exponential backoff is needed both when we are offline and when there are
- // failures on the server side.
- var newRetryDelaySeconds =
- Math.min(items.retryDelaySeconds * 2 * (1 + 0.2 * Math.random()),
- MAXIMUM_POLLING_PERIOD_SECONDS);
- storage.set({retryDelaySeconds: newRetryDelaySeconds});
-
- navigator.geolocation.getCurrentPosition(
- requestNotificationCardsWithLocation,
- requestNotificationCardsWithoutLocation);
- });
-}
-
-/**
- * Opens URL corresponding to the clicked part of the notification.
- * @param {string} notificationId Unique identifier of the notification.
- * @param {function(Object): string} selector Function that extracts the url for
- * the clicked area from the button action URLs info.
- */
-function onNotificationClicked(notificationId, selector) {
- storage.get('activeNotifications', function(items) {
- var actionUrls = items.activeNotifications[notificationId];
- if (typeof actionUrls != 'object') {
- // TODO(vadimt): report an error.
- return;
- }
-
- var url = selector(actionUrls);
-
- if (typeof url != 'string') {
- // TODO(vadimt): report an error.
- return;
- }
-
- chrome.tabs.create({url: url});
- });
-}
-
-/**
- * Callback for chrome.notifications.onClosed event.
- * @param {string} notificationId Unique identifier of the notification.
- * @param {boolean} byUser Whether the notification was closed by the user.
- */
-function onNotificationClosed(notificationId, byUser) {
- if (byUser) {
- // TODO(vadimt): Analyze possible race conditions between request for cards
- // and dismissal.
- // Send a dismiss request to the server.
- var requestParameters = '?id=' + notificationId;
- var request = new XMLHttpRequest();
- request.responseType = 'text';
- // TODO(vadimt): If the request fails, for example, because there is no
- // internet connection, do retry with exponential backoff.
- request.open(
- 'GET',
- NOTIFICATION_CARDS_URL + '/dismiss' + requestParameters,
- true);
- request.send();
- }
-}
-
-/**
- * Schedule next update for notification cards.
- * @param {int} delaySeconds Length of time in seconds after which the alarm
- * event should fire.
- */
-function scheduleNextUpdate(delaySeconds) {
- // Schedule an alarm after the specified delay. 'periodInMinutes' is for the
- // case when we fail to re-register the alarm.
- chrome.alarms.create({
- delayInMinutes: delaySeconds / 60,
- periodInMinutes: MAXIMUM_POLLING_PERIOD_SECONDS / 60
- });
-}
-
-/**
- * Initialize the event page on install or on browser startup.
- */
-function initialize() {
- var initialStorage = {
- activeNotifications: {},
- retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS
- };
- storage.set(initialStorage, updateNotificationsCards);
-}
-
-chrome.runtime.onInstalled.addListener(function(details) {
- if (details.reason != 'chrome_update')
- initialize();
-});
-
-chrome.runtime.onStartup.addListener(function() {
- initialize();
-});
-
-chrome.alarms.onAlarm.addListener(function(alarm) {
- updateNotificationsCards();
-});
-
-chrome.notifications.onClicked.addListener(
- function(notificationId) {
- onNotificationClicked(notificationId, function(actionUrls) {
- return actionUrls.messageUrl;
- });
- });
-
-chrome.notifications.onButtonClicked.addListener(
- function(notificationId, buttonIndex) {
- onNotificationClicked(notificationId, function(actionUrls) {
- if (!Array.isArray(actionUrls.buttonUrls))
- return undefined;
-
- return actionUrls.buttonUrls[buttonIndex];
- });
- });
-
-chrome.notifications.onClosed.addListener(onNotificationClosed);
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+'use strict';
+
+/**
+ * @fileoverview The event page for Google Now for Chrome implementation.
+ * The Google Now event page gets Google Now cards from the server and shows
+ * them as Chrome notifications.
+ * The service performs periodic updating of Google Now cards.
+ * Each updating of the cards includes 3 steps:
+ * 1. Obtaining the location of the machine;
+ * 2. Making a server request based on that location;
+ * 3. Showing the received cards as notifications.
+ */
+
+// TODO(vadimt): Use background permission to show notifications even when all
+// browser windows are closed.
+// TODO(vadimt): Remove the C++ implementation.
+// TODO(vadimt): Decide what to do in incognito mode.
+// TODO(vadimt): Gather UMAs.
+// TODO(vadimt): Honor the flag the enables Google Now integration.
+// TODO(vadimt): Figure out the final values of the constants.
+// TODO(vadimt): Report internal and server errors. Collect UMAs on errors where
+// appropriate. Also consider logging errors or throwing exceptions.
+// TODO(vadimt): Consider processing errors for all storage.set calls.
+
+// TODO(vadimt): Figure out the server name. Use it in the manifest and for
+// NOTIFICATION_CARDS_URL. Meanwhile, to use the feature, you need to manually
+// set the server name via local storage.
+/**
+ * URL to retrieve notification cards.
+ */
+var NOTIFICATION_CARDS_URL = localStorage['server_url'];
+
+/**
+ * Standard response code for successful HTTP requests. This is the only success
+ * code the server will send.
+ */
+var HTTP_OK = 200;
+
+/**
+ * Initial period for polling for Google Now Notifications cards to use when the
+ * period from the server is not available.
+ */
+var INITIAL_POLLING_PERIOD_SECONDS = 300; // 5 minutes
+
+/**
+ * Maximal period for polling for Google Now Notifications cards to use when the
+ * period from the server is not available.
+ */
+var MAXIMUM_POLLING_PERIOD_SECONDS = 3600; // 1 hour
+
+var UPDATE_NOTIFICATIONS_ALARM_NAME = 'UPDATE';
+
+var storage = chrome.storage.local;
+
+/**
+ * Names for tasks that can be created by the extension.
+ */
+var UPDATE_CARDS_TASK_NAME = 'update-cards';
+var DISMISS_CARD_TASK_NAME = 'dismiss-card';
+var CARD_CLICKED_TASK_NAME = 'card-clicked';
+
+/**
+ * Checks if a new task can't be scheduled when another task is already
+ * scheduled.
+ * @param {string} newTaskName Name of the new task.
+ * @param {string} queuedTaskName Name of the task in the queue.
+ * @return {boolean} Whether the new task conflicts with the existing task.
+ */
+function areTasksConflicting(newTaskName, queuedTaskName) {
+ if (newTaskName == UPDATE_CARDS_TASK_NAME &&
+ queuedTaskName == UPDATE_CARDS_TASK_NAME) {
+ // If a card update is requested while an old update is still scheduled, we
+ // don't need the new update.
+ return true;
+ }
+
+ return false;
+}
+
+var tasks = buildTaskManager(areTasksConflicting);
+
+/**
+ * Shows a notification and remembers information associated with it.
+ * @param {Object} card Google Now card represented as a set of parameters for
+ * showing a Chrome notification.
+ * @param {Object} notificationsUrlInfo Map from notification id to the
+ * notification's set of URLs.
+ */
+function createNotification(card, notificationsUrlInfo) {
+ // Create a notification or quietly update if it already exists.
+ // TODO(vadimt): Implement non-quiet updates.
+ chrome.notifications.create(
+ card.notificationId,
+ card.notification,
+ function() {});
+
+ notificationsUrlInfo[card.notificationId] = card.actionUrls;
+}
+
+/**
+ * Parses JSON response from the notification server, show notifications and
+ * schedule next update.
+ * @param {string} response Server response.
+ * @param {function()} callback Completion callback.
+ */
+function parseAndShowNotificationCards(response, callback) {
+ try {
+ var parsedResponse = JSON.parse(response);
+ } catch (error) {
+ // TODO(vadimt): Report errors to the user.
+ return;
+ }
+
+ var cards = parsedResponse.cards;
+
+ if (!(cards instanceof Array)) {
+ // TODO(vadimt): Report errors to the user.
+ return;
+ }
+
+ if (typeof parsedResponse.expiration_timestamp_seconds != 'number') {
+ // TODO(vadimt): Report errors to the user.
+ return;
+ }
+
+ tasks.debugSetStepName(
+ 'parseAndShowNotificationCards-get-active-notifications');
+ storage.get('activeNotifications', function(items) {
+ // Mark existing notifications that received an update in this server
+ // response.
+ for (var i = 0; i < cards.length; ++i) {
+ var notificationId = cards[i].notificationId;
+ if (notificationId in items.activeNotifications)
+ items.activeNotifications[notificationId].hasUpdate = true;
+ }
+
+ // Delete notifications that didn't receive an update.
+ for (var notificationId in items.activeNotifications) {
+ if (!items.activeNotifications[notificationId].hasUpdate) {
+ chrome.notifications.clear(
+ notificationId,
+ function() {});
+ }
+ }
+
+ // Create/update notifications and store their new properties.
+ var notificationsUrlInfo = {};
+
+ for (var i = 0; i < cards.length; ++i) {
+ try {
+ createNotification(cards[i], notificationsUrlInfo);
+ } catch (error) {
+ // TODO(vadimt): Report errors to the user.
+ }
+ }
+ storage.set({activeNotifications: notificationsUrlInfo});
+
+ scheduleNextUpdate(parsedResponse.expiration_timestamp_seconds);
+
+ // Now that we got a valid response from the server, reset the retry period
+ // to the initial value. This retry period will be used the next time we
+ // fail to get the server-provided period.
+ storage.set({retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS});
+ callback();
+ });
+}
+
+/**
+ * Requests notification cards from the server.
+ * @param {string} requestParameters Query string for the request.
+ * @param {function()} callback Completion callback.
+ */
+function requestNotificationCards(requestParameters, callback) {
+ // TODO(vadimt): Figure out how to send user's identity to the server.
+ var request = new XMLHttpRequest();
+
+ request.responseType = 'text';
+ request.onloadend = function(event) {
+ if (request.status == HTTP_OK)
+ parseAndShowNotificationCards(request.response, callback);
+ else
+ callback();
+ };
+
+ request.open(
+ 'GET',
+ NOTIFICATION_CARDS_URL + '/notifications' + requestParameters,
+ true);
+ tasks.debugSetStepName('requestNotificationCards-send-request');
+ request.send();
+}
+
+/**
+ * Requests notification cards from the server when we have geolocation.
+ * @param {Geoposition} position Location of this computer.
+ * @param {function()} callback Completion callback.
+ */
+function requestNotificationCardsWithLocation(position, callback) {
+ // TODO(vadimt): Should we use 'q' as the parameter name?
+ var requestParameters =
+ '?q=' + position.coords.latitude +
+ ',' + position.coords.longitude +
+ ',' + position.coords.accuracy;
+
+ requestNotificationCards(requestParameters, callback);
+}
+
+/**
+ * Obtains new location; requests and shows notification cards based on this
+ * location.
+ */
+function updateNotificationsCards() {
+ tasks.add(UPDATE_CARDS_TASK_NAME, function(callback) {
+ tasks.debugSetStepName('updateNotificationsCards-get-retryDelaySeconds');
+ storage.get('retryDelaySeconds', function(items) {
+ // Immediately schedule the update after the current retry period. Then,
+ // we'll use update time from the server if available.
+ scheduleNextUpdate(items.retryDelaySeconds);
+
+ // TODO(vadimt): Consider interrupting waiting for the next update if we
+ // detect that the network conditions have changed. Also, decide whether
+ // the exponential backoff is needed both when we are offline and when
+ // there are failures on the server side.
+ var newRetryDelaySeconds =
+ Math.min(items.retryDelaySeconds * 2 * (1 + 0.2 * Math.random()),
+ MAXIMUM_POLLING_PERIOD_SECONDS);
+ storage.set({retryDelaySeconds: newRetryDelaySeconds});
+
+ tasks.debugSetStepName('updateNotificationsCards-get-location');
+ navigator.geolocation.getCurrentPosition(
+ function(position) {
+ requestNotificationCardsWithLocation(position, callback);
+ },
+ function() {
+ requestNotificationCards('', callback);
+ });
+ });
+ });
+}
+
+/**
+ * Opens URL corresponding to the clicked part of the notification.
+ * @param {string} notificationId Unique identifier of the notification.
+ * @param {function(Object): string} selector Function that extracts the url for
+ * the clicked area from the button action URLs info.
+ */
+function onNotificationClicked(notificationId, selector) {
+ tasks.add(CARD_CLICKED_TASK_NAME, function(callback) {
+ tasks.debugSetStepName('onNotificationClicked-get-activeNotifications');
+ storage.get('activeNotifications', function(items) {
+ var actionUrls = items.activeNotifications[notificationId];
+ if (typeof actionUrls != 'object') {
+ // TODO(vadimt): report an error.
+ callback();
+ return;
+ }
+
+ var url = selector(actionUrls);
+
+ if (typeof url != 'string') {
+ // TODO(vadimt): report an error.
+ callback();
+ return;
+ }
+
+ chrome.tabs.create({url: url});
+ callback();
+ });
+ });
+}
+
+/**
+ * Callback for chrome.notifications.onClosed event.
+ * @param {string} notificationId Unique identifier of the notification.
+ * @param {boolean} byUser Whether the notification was closed by the user.
+ */
+function onNotificationClosed(notificationId, byUser) {
+ if (!byUser)
+ return;
+
+ tasks.add(DISMISS_CARD_TASK_NAME, function(callback) {
+ // Deleting the notification in case it was re-added while this task was
+ // waiting in the queue.
+ chrome.notifications.clear(
+ notificationId,
+ function() {});
+
+ // Send a dismiss request to the server.
+ var requestParameters = '?id=' + notificationId;
+ var request = new XMLHttpRequest();
+ request.responseType = 'text';
+ request.onloadend = function(event) {
+ callback();
+ };
+ // TODO(vadimt): If the request fails, for example, because there is no
+ // internet connection, do retry with exponential backoff.
+ request.open(
+ 'GET',
+ NOTIFICATION_CARDS_URL + '/dismiss' + requestParameters,
+ true);
+ tasks.debugSetStepName('onNotificationClosed-send-request');
+ request.send();
+ });
+}
+
+/**
+ * Schedules next update for notification cards.
+ * @param {int} delaySeconds Length of time in seconds after which the alarm
+ * event should fire.
+ */
+function scheduleNextUpdate(delaySeconds) {
+ // Schedule an alarm after the specified delay. 'periodInMinutes' is for the
+ // case when we fail to re-register the alarm.
+ var alarmInfo = {
+ delayInMinutes: delaySeconds / 60,
+ periodInMinutes: MAXIMUM_POLLING_PERIOD_SECONDS / 60
+ };
+
+ chrome.alarms.create(UPDATE_NOTIFICATIONS_ALARM_NAME, alarmInfo);
+}
+
+/**
+ * Initializes the event page on install or on browser startup.
+ */
+function initialize() {
+ var initialStorage = {
+ activeNotifications: {},
+ retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS
+ };
+ storage.set(initialStorage);
+ updateNotificationsCards();
+}
+
+chrome.runtime.onInstalled.addListener(function(details) {
+ if (details.reason != 'chrome_update')
+ initialize();
+});
+
+chrome.runtime.onStartup.addListener(function() {
+ initialize();
+});
+
+chrome.alarms.onAlarm.addListener(function(alarm) {
+ if (alarm.name == UPDATE_NOTIFICATIONS_ALARM_NAME)
+ updateNotificationsCards();
+});
+
+chrome.notifications.onClicked.addListener(
+ function(notificationId) {
+ onNotificationClicked(notificationId, function(actionUrls) {
+ return actionUrls.messageUrl;
+ });
+ });
+
+chrome.notifications.onButtonClicked.addListener(
+ function(notificationId, buttonIndex) {
+ onNotificationClicked(notificationId, function(actionUrls) {
+ if (!Array.isArray(actionUrls.buttonUrls))
+ return undefined;
+
+ return actionUrls.buttonUrls[buttonIndex];
+ });
+ });
+
+chrome.notifications.onClosed.addListener(onNotificationClosed);
« no previous file with comments | « chrome/browser/resources/component_extension_resources.grd ('k') | chrome/browser/resources/google_now/manifest.json » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698