summaryrefslogtreecommitdiffstats
path: root/chromium/chrome/browser/resources/google_now/utility.js
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/chrome/browser/resources/google_now/utility.js')
-rw-r--r--chromium/chrome/browser/resources/google_now/utility.js420
1 files changed, 354 insertions, 66 deletions
diff --git a/chromium/chrome/browser/resources/google_now/utility.js b/chromium/chrome/browser/resources/google_now/utility.js
index 6674edf7754..591b2f79a4e 100644
--- a/chromium/chrome/browser/resources/google_now/utility.js
+++ b/chromium/chrome/browser/resources/google_now/utility.js
@@ -15,7 +15,7 @@
* otherwise, we generate an error. Chrome may unload event pages waiting
* for an event. When the event fires, Chrome will reload the event page. We
* don't require event listeners to fire because they are generally not
- * predictable (like a location change event).
+ * predictable (like a button clicked event).
* (2) Task Manager (built with buildTaskManager() call) provides controlling
* mutually excluding chains of callbacks called tasks. Task Manager uses
* WrapperPlugins to add instrumentation code to 'wrapper' to determine
@@ -29,13 +29,21 @@
*/
var NOTIFICATION_CARDS_URL = 'https://www.googleapis.com/chromenow/v1';
-var DEBUG_MODE = localStorage['debug_mode'];
+/**
+ * Returns true if debug mode is enabled.
+ * localStorage returns items as strings, which means if we store a boolean,
+ * it returns a string. Use this function to compare against true.
+ * @return {boolean} Whether debug mode is enabled.
+ */
+function isInDebugMode() {
+ return localStorage.debug_mode === 'true';
+}
/**
* Initializes for debug or release modes of operation.
*/
function initializeDebug() {
- if (DEBUG_MODE) {
+ if (isInDebugMode()) {
NOTIFICATION_CARDS_URL =
localStorage['server_url'] || NOTIFICATION_CARDS_URL;
}
@@ -44,10 +52,28 @@ function initializeDebug() {
initializeDebug();
/**
- * Location Card Storage.
+ * Conditionally allow console.log output based off of the debug mode.
+ */
+console.log = function() {
+ var originalConsoleLog = console.log;
+ return function() {
+ if (isInDebugMode()) {
+ originalConsoleLog.apply(console, arguments);
+ }
+ };
+}();
+
+/**
+ * Explanation Card Storage.
+ */
+if (localStorage['explanatoryCardsShown'] === undefined)
+ localStorage['explanatoryCardsShown'] = 0;
+
+/**
+ * Location Card Count Cleanup.
*/
-if (localStorage['locationCardsShown'] === undefined)
- localStorage['locationCardsShown'] = 0;
+if (localStorage.locationCardsShown !== undefined)
+ localStorage.removeItem('locationCardsShown');
/**
* Builds an error object with a message that may be sent to the server.
@@ -76,16 +102,16 @@ function verify(condition, message) {
* Builds a request to the notification server.
* @param {string} method Request method.
* @param {string} handlerName Server handler to send the request to.
- * @param {string=} contentType Value for the Content-type header.
+ * @param {string=} opt_contentType Value for the Content-type header.
* @return {XMLHttpRequest} Server request.
*/
-function buildServerRequest(method, handlerName, contentType) {
+function buildServerRequest(method, handlerName, opt_contentType) {
var request = new XMLHttpRequest();
request.responseType = 'text';
request.open(method, NOTIFICATION_CARDS_URL + '/' + handlerName, true);
- if (contentType)
- request.setRequestHeader('Content-type', contentType);
+ if (opt_contentType)
+ request.setRequestHeader('Content-type', opt_contentType);
return request;
}
@@ -143,6 +169,8 @@ function sendErrorReport(error) {
trace: filteredStack
};
+ // We use relatively direct calls here because the instrumentation may be in
+ // a bad state. Wrappers and promises should not be involved in the reporting.
var request = buildServerRequest('POST', 'jserrors', 'application/json');
request.onloadend = function(event) {
console.log('sendErrorReport status: ' + request.status);
@@ -165,13 +193,15 @@ var errorReported = false;
*/
function reportError(error) {
var message = 'Critical error:\n' + error.stack;
- console.error(message);
+ if (isInDebugMode())
+ console.error(message);
+
if (!errorReported) {
errorReported = true;
chrome.metricsPrivate.getIsCrashReportingEnabled(function(isEnabled) {
if (isEnabled)
sendErrorReport(error);
- if (DEBUG_MODE)
+ if (isInDebugMode())
alert(message);
});
}
@@ -297,7 +327,7 @@ var wrapper = (function() {
wrapperPluginInstance.prologue();
// Call the original callback.
- callback.apply(null, arguments);
+ var returnValue = callback.apply(null, arguments);
if (wrapperPluginInstance)
wrapperPluginInstance.epilogue();
@@ -305,6 +335,8 @@ var wrapper = (function() {
verify(isInWrappedCallback,
'Instrumented callback is not instrumented upon exit');
isInWrappedCallback = false;
+
+ return returnValue;
} catch (error) {
reportError(error);
}
@@ -408,9 +440,252 @@ wrapper.instrumentChromeApiFunction('alarms.onAlarm.addListener', 0);
wrapper.instrumentChromeApiFunction('identity.getAuthToken', 1);
wrapper.instrumentChromeApiFunction('identity.onSignInChanged.addListener', 0);
wrapper.instrumentChromeApiFunction('identity.removeCachedAuthToken', 1);
+wrapper.instrumentChromeApiFunction('storage.local.get', 1);
wrapper.instrumentChromeApiFunction('webstorePrivate.getBrowserLogin', 0);
/**
+ * Promise adapter for all JS promises to the task manager.
+ */
+function registerPromiseAdapter() {
+ var originalThen = Promise.prototype.then;
+ var originalCatch = Promise.prototype.catch;
+
+ /**
+ * Takes a promise and adds the callback tracker to it.
+ * @param {object} promise Promise that receives the callback tracker.
+ */
+ function instrumentPromise(promise) {
+ if (promise.__tracker === undefined) {
+ promise.__tracker = createPromiseCallbackTracker(promise);
+ }
+ }
+
+ Promise.prototype.then = function(onResolved, onRejected) {
+ instrumentPromise(this);
+ return this.__tracker.handleThen(onResolved, onRejected);
+ }
+
+ Promise.prototype.catch = function(onRejected) {
+ instrumentPromise(this);
+ return this.__tracker.handleCatch(onRejected);
+ }
+
+ /**
+ * Promise Callback Tracker.
+ * Handles coordination of 'then' and 'catch' callbacks in a task
+ * manager compatible way. For an individual promise, either the 'then'
+ * arguments or the 'catch' arguments will be processed, never both.
+ *
+ * Example:
+ * var p = new Promise([Function]);
+ * p.then([ThenA]);
+ * p.then([ThenB]);
+ * p.catch([CatchA]);
+ * On resolution, [ThenA] and [ThenB] will be used. [CatchA] is discarded.
+ * On rejection, vice versa.
+ *
+ * Clarification:
+ * Chained promises create a new promise that is tracked separately from
+ * the originaing promise, as the example below demonstrates:
+ *
+ * var p = new Promise([Function]));
+ * p.then([ThenA]).then([ThenB]).catch([CatchA]);
+ * ^ ^ ^
+ * | | + Returns a new promise.
+ * | + Returns a new promise.
+ * + Returns a new promise.
+ *
+ * Four promises exist in the above statement, each with its own
+ * resolution and rejection state. However, by default, this state is
+ * chained to the previous promise's resolution or rejection
+ * state.
+ *
+ * If p resolves, then the 'then' calls will execute until all the 'then'
+ * clauses are executed. If the result of either [ThenA] or [ThenB] is a
+ * promise, then that execution state will guide the remaining chain.
+ * Similarly, if [CatchA] returns a promise, it can also guide the
+ * remaining chain. In this specific case, the chain ends, so there
+ * is nothing left to do.
+ * @param {object} promise Promise being tracked.
+ * @return {object} A promise callback tracker.
+ */
+ function createPromiseCallbackTracker(promise) {
+ /**
+ * Callback Tracker. Holds an array of callbacks created for this promise.
+ * The indirection allows quick checks against the array and clearing the
+ * array without ugly splicing and copying.
+ * @typedef {{
+ * callback: array.<Function>=
+ * }}
+ */
+ var CallbackTracker;
+
+ /** @type {CallbackTracker} */
+ var thenTracker = {callbacks: []};
+ /** @type {CallbackTracker} */
+ var catchTracker = {callbacks: []};
+
+ /**
+ * Returns true if the specified value is callable.
+ * @param {*} value Value to check.
+ * @return {boolean} True if the value is a callable.
+ */
+ function isCallable(value) {
+ return typeof value === 'function';
+ }
+
+ /**
+ * Takes a tracker and clears its callbacks in a manner consistent with
+ * the task manager. For the task manager, it also calls all callbacks
+ * by no-oping them first and then calling them.
+ * @param {CallbackTracker} tracker Tracker to clear.
+ */
+ function clearTracker(tracker) {
+ if (tracker.callbacks) {
+ var callbacksToClear = tracker.callbacks;
+ // No-ops all callbacks of this type.
+ tracker.callbacks = undefined;
+ // Do not wrap the promise then argument!
+ // It will call wrapped callbacks.
+ originalThen.call(Promise.resolve(), function() {
+ for (var i = 0; i < callbacksToClear.length; i++) {
+ callbacksToClear[i]();
+ }
+ });
+ }
+ }
+
+ /**
+ * Takes the argument to a 'then' or 'catch' function and applies
+ * a wrapping to callables consistent to ECMA promises.
+ * @param {*} maybeCallback Argument to 'then' or 'catch'.
+ * @param {CallbackTracker} sameTracker Tracker for the call type.
+ * Example: If the argument is from a 'then' call, use thenTracker.
+ * @param {CallbackTracker} otherTracker Tracker for the opposing call type.
+ * Example: If the argument is from a 'then' call, use catchTracker.
+ * @return {*} Consumable argument with necessary wrapping applied.
+ */
+ function registerAndWrapMaybeCallback(
+ maybeCallback, sameTracker, otherTracker) {
+ // If sameTracker.callbacks is undefined, we've reached an ending state
+ // that means this callback will never be called back.
+ // We will still forward this call on to let the promise system
+ // handle further processing, but since this promise is in an ending state
+ // we can be confident it will never be called back.
+ if (isCallable(maybeCallback) &&
+ !maybeCallback.wrappedByPromiseTracker &&
+ sameTracker.callbacks) {
+ var handler = wrapper.wrapCallback(function() {
+ if (sameTracker.callbacks) {
+ clearTracker(otherTracker);
+ return maybeCallback.apply(null, arguments);
+ }
+ }, false);
+ // Harmony promises' catch calls will call into handleThen,
+ // double-wrapping all catch callbacks. Regular promise catch calls do
+ // not call into handleThen. Setting an attribute on the wrapped
+ // function is compatible with both promise implementations.
+ handler.wrappedByPromiseTracker = true;
+ sameTracker.callbacks.push(handler);
+ return handler;
+ } else {
+ return maybeCallback;
+ }
+ }
+
+ /**
+ * Tracks then calls equivalent to Promise.prototype.then.
+ * @param {*} onResolved Argument to use if the promise is resolved.
+ * @param {*} onRejected Argument to use if the promise is rejected.
+ * @return {object} Promise resulting from the 'then' call.
+ */
+ function handleThen(onResolved, onRejected) {
+ var resolutionHandler =
+ registerAndWrapMaybeCallback(onResolved, thenTracker, catchTracker);
+ var rejectionHandler =
+ registerAndWrapMaybeCallback(onRejected, catchTracker, thenTracker);
+ return originalThen.call(promise, resolutionHandler, rejectionHandler);
+ }
+
+ /**
+ * Tracks then calls equivalent to Promise.prototype.catch.
+ * @param {*} onRejected Argument to use if the promise is rejected.
+ * @return {object} Promise resulting from the 'catch' call.
+ */
+ function handleCatch(onRejected) {
+ var rejectionHandler =
+ registerAndWrapMaybeCallback(onRejected, catchTracker, thenTracker);
+ return originalCatch.call(promise, rejectionHandler);
+ }
+
+ // Register at least one resolve and reject callback so we always receive
+ // a callback to update the task manager and clear the callbacks
+ // that will never occur.
+ //
+ // The then form is used to avoid reentrancy by handleCatch,
+ // which ends up calling handleThen.
+ handleThen(function() {}, function() {});
+
+ return {
+ handleThen: handleThen,
+ handleCatch: handleCatch
+ };
+ }
+}
+
+registerPromiseAdapter();
+
+/**
+ * Control promise rejection.
+ * @enum {number}
+ */
+var PromiseRejection = {
+ /** Disallow promise rejection */
+ DISALLOW: 0,
+ /** Allow promise rejection */
+ ALLOW: 1
+};
+
+/**
+ * Provides the promise equivalent of instrumented.storage.local.get.
+ * @param {Object} defaultStorageObject Default storage object to fill.
+ * @param {PromiseRejection=} opt_allowPromiseRejection If
+ * PromiseRejection.ALLOW, allow promise rejection on errors, otherwise the
+ * default storage object is resolved.
+ * @return {Promise} A promise that fills the default storage object. On
+ * failure, if promise rejection is allowed, the promise is rejected,
+ * otherwise it is resolved to the default storage object.
+ */
+function fillFromChromeLocalStorage(
+ defaultStorageObject,
+ opt_allowPromiseRejection) {
+ return new Promise(function(resolve, reject) {
+ // We have to create a keys array because keys with a default value
+ // of undefined will cause that key to not be looked up!
+ var keysToGet = [];
+ for (var key in defaultStorageObject) {
+ keysToGet.push(key);
+ }
+ instrumented.storage.local.get(keysToGet, function(items) {
+ if (items) {
+ // Merge the result with the default storage object to ensure all keys
+ // requested have either the default value or the retrieved storage
+ // value.
+ var result = {};
+ for (var key in defaultStorageObject) {
+ result[key] = (key in items) ? items[key] : defaultStorageObject[key];
+ }
+ resolve(result);
+ } else if (opt_allowPromiseRejection === PromiseRejection.ALLOW) {
+ reject();
+ } else {
+ resolve(defaultStorageObject);
+ }
+ });
+ });
+}
+
+/**
* Builds the object to manage tasks (mutually exclusive chains of events).
* @param {function(string, string): boolean} areConflicting Function that
* checks if a new task can't be added to a task queue that contains an
@@ -604,21 +879,19 @@ function buildAttemptManager(
}
/**
- * Schedules next attempt.
- * @param {number=} opt_previousDelaySeconds Previous delay in a sequence of
- * retry attempts, if specified. Not specified for scheduling first retry
- * in the exponential sequence.
+ * Schedules the alarm with a random factor to reduce the chance that all
+ * clients will fire their timers at the same time.
+ * @param {number} durationSeconds Number of seconds before firing the alarm.
*/
- function scheduleNextAttempt(opt_previousDelaySeconds) {
- var base = opt_previousDelaySeconds ? opt_previousDelaySeconds * 2 :
- initialDelaySeconds;
- var newRetryDelaySeconds =
- Math.min(base * (1 + 0.2 * Math.random()), maximumDelaySeconds);
+ function scheduleAlarm(durationSeconds) {
+ var randomizedRetryDuration =
+ Math.min(durationSeconds * (1 + 0.2 * Math.random()),
+ maximumDelaySeconds);
- createAlarm(newRetryDelaySeconds);
+ createAlarm(randomizedRetryDuration);
var items = {};
- items[currentDelayStorageKey] = newRetryDelaySeconds;
+ items[currentDelayStorageKey] = randomizedRetryDuration;
chrome.storage.local.set(items);
}
@@ -633,7 +906,7 @@ function buildAttemptManager(
createAlarm(opt_firstDelaySeconds);
chrome.storage.local.remove(currentDelayStorageKey);
} else {
- scheduleNextAttempt();
+ scheduleAlarm(initialDelaySeconds);
}
}
@@ -646,20 +919,25 @@ function buildAttemptManager(
}
/**
- * Plans for the next attempt.
- * @param {function()} callback Completion callback. It will be invoked after
- * the planning is done.
+ * Schedules an exponential backoff retry.
+ * @return {Promise} A promise to schedule the retry.
*/
- function planForNext(callback) {
- instrumented.storage.local.get(currentDelayStorageKey, function(items) {
- if (!items) {
- items = {};
- items[currentDelayStorageKey] = maximumDelaySeconds;
- }
- console.log('planForNext-get-storage ' + JSON.stringify(items));
- scheduleNextAttempt(items[currentDelayStorageKey]);
- callback();
- });
+ function scheduleRetry() {
+ var request = {};
+ request[currentDelayStorageKey] = undefined;
+ return fillFromChromeLocalStorage(request, PromiseRejection.ALLOW)
+ .catch(function() {
+ request[currentDelayStorageKey] = maximumDelaySeconds;
+ return Promise.resolve(request);
+ })
+ .then(function(items) {
+ console.log('scheduleRetry-get-storage ' + JSON.stringify(items));
+ var retrySeconds = initialDelaySeconds;
+ if (items[currentDelayStorageKey]) {
+ retrySeconds = items[currentDelayStorageKey] * 2;
+ }
+ scheduleAlarm(retrySeconds);
+ });
}
instrumented.alarms.onAlarm.addListener(function(alarm) {
@@ -672,7 +950,7 @@ function buildAttemptManager(
return {
start: start,
- planForNext: planForNext,
+ scheduleRetry: scheduleRetry,
stop: stop,
isRunning: isRunning
};
@@ -689,36 +967,46 @@ function buildAuthenticationManager() {
/**
* Gets an OAuth2 access token.
- * @param {function(string=)} callback Called on completion.
- * The string contains the token. It's undefined if there was an error.
+ * @return {Promise} A promise to get the authentication token. If there is
+ * no token, the request is rejected.
*/
- function getAuthToken(callback) {
- instrumented.identity.getAuthToken({interactive: false}, function(token) {
- token = chrome.runtime.lastError ? undefined : token;
- callback(token);
+ function getAuthToken() {
+ return new Promise(function(resolve, reject) {
+ instrumented.identity.getAuthToken({interactive: false}, function(token) {
+ if (chrome.runtime.lastError || !token) {
+ reject();
+ } else {
+ resolve(token);
+ }
+ });
});
}
/**
* Determines whether there is an account attached to the profile.
- * @param {function(boolean)} callback Called on completion.
+ * @return {Promise} A promise to determine if there is an account attached
+ * to the profile.
*/
- function isSignedIn(callback) {
- instrumented.webstorePrivate.getBrowserLogin(function(accountInfo) {
- callback(!!accountInfo.login);
+ function isSignedIn() {
+ return new Promise(function(resolve) {
+ instrumented.webstorePrivate.getBrowserLogin(function(accountInfo) {
+ resolve(!!accountInfo.login);
+ });
});
}
/**
* Removes the specified cached token.
* @param {string} token Authentication Token to remove from the cache.
- * @param {function()} callback Called on completion.
+ * @return {Promise} A promise that resolves on completion.
*/
- function removeToken(token, callback) {
- instrumented.identity.removeCachedAuthToken({token: token}, function() {
- // Let Chrome now about a possible problem with the token.
- getAuthToken(function() {});
- callback();
+ function removeToken(token) {
+ return new Promise(function(resolve) {
+ instrumented.identity.removeCachedAuthToken({token: token}, function() {
+ // Let Chrome know about a possible problem with the token.
+ getAuthToken();
+ resolve();
+ });
});
}
@@ -738,18 +1026,18 @@ function buildAuthenticationManager() {
* If it doesn't, it notifies the listeners of the change.
*/
function checkAndNotifyListeners() {
- isSignedIn(function(signedIn) {
- instrumented.storage.local.get('lastSignedInState', function(items) {
- items = items || {};
- if (items.lastSignedInState != signedIn) {
- chrome.storage.local.set(
- {lastSignedInState: signedIn});
- listeners.forEach(function(callback) {
- callback();
- });
- }
+ isSignedIn().then(function(signedIn) {
+ fillFromChromeLocalStorage({lastSignedInState: undefined})
+ .then(function(items) {
+ if (items.lastSignedInState != signedIn) {
+ chrome.storage.local.set(
+ {lastSignedInState: signedIn});
+ listeners.forEach(function(callback) {
+ callback();
+ });
+ }
+ });
});
- });
}
instrumented.identity.onSignInChanged.addListener(function() {