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
3 // found in the LICENSE file.
8 * @fileoverview Utility objects and functions for Google Now extension.
9 * Most important entities here:
10 * (1) 'wrapper' is a module used to add error handling and other services to
11 * callbacks for HTML and Chrome functions and Chrome event listeners.
12 * Chrome invokes extension code through event listeners. Once entered via
13 * an event listener, the extension may call a Chrome/HTML API method
14 * passing a callback (and so forth), and that callback must occur later,
15 * otherwise, we generate an error. Chrome may unload event pages waiting
16 * for an event. When the event fires, Chrome will reload the event page. We
17 * don't require event listeners to fire because they are generally not
18 * predictable (like a button clicked event).
19 * (2) Task Manager (built with buildTaskManager() call) provides controlling
20 * mutually excluding chains of callbacks called tasks. Task Manager uses
21 * WrapperPlugins to add instrumentation code to 'wrapper' to determine
22 * when a task completes.
25 // TODO(vadimt): Use server name in the manifest.
28 * Notification server URL.
30 var NOTIFICATION_CARDS_URL = 'https://www.googleapis.com/chromenow/v1';
33 * Returns true if debug mode is enabled.
34 * localStorage returns items as strings, which means if we store a boolean,
35 * it returns a string. Use this function to compare against true.
36 * @return {boolean} Whether debug mode is enabled.
38 function isInDebugMode() {
39 return localStorage.debug_mode === 'true';
43 * Initializes for debug or release modes of operation.
45 function initializeDebug() {
46 if (isInDebugMode()) {
47 NOTIFICATION_CARDS_URL =
48 localStorage['server_url'] || NOTIFICATION_CARDS_URL;
55 * Conditionally allow console.log output based off of the debug mode.
57 console.log = function() {
58 var originalConsoleLog = console.log;
60 if (isInDebugMode()) {
61 originalConsoleLog.apply(console, arguments);
67 * Explanation Card Storage.
69 if (localStorage['explanatoryCardsShown'] === undefined)
70 localStorage['explanatoryCardsShown'] = 0;
73 * Location Card Count Cleanup.
75 if (localStorage.locationCardsShown !== undefined)
76 localStorage.removeItem('locationCardsShown');
79 * Builds an error object with a message that may be sent to the server.
80 * @param {string} message Error message. This message may be sent to the
82 * @return {Error} Error object.
84 function buildErrorWithMessageForServer(message) {
85 var error = new Error(message);
86 error.canSendMessageToServer = true;
91 * Checks for internal errors.
92 * @param {boolean} condition Condition that must be true.
93 * @param {string} message Diagnostic message for the case when the condition is
96 function verify(condition, message) {
98 throw buildErrorWithMessageForServer('ASSERT: ' + message);
102 * Builds a request to the notification server.
103 * @param {string} method Request method.
104 * @param {string} handlerName Server handler to send the request to.
105 * @param {string=} opt_contentType Value for the Content-type header.
106 * @return {XMLHttpRequest} Server request.
108 function buildServerRequest(method, handlerName, opt_contentType) {
109 var request = new XMLHttpRequest();
111 request.responseType = 'text';
112 request.open(method, NOTIFICATION_CARDS_URL + '/' + handlerName, true);
114 request.setRequestHeader('Content-type', opt_contentType);
120 * Sends an error report to the server.
121 * @param {Error} error Error to send.
123 function sendErrorReport(error) {
124 // Don't remove 'error.stack.replace' below!
125 var filteredStack = error.canSendMessageToServer ?
126 error.stack : error.stack.replace(/.*\n/, '(message removed)\n');
129 var topFrameLineMatch = filteredStack.match(/\n at .*\n/);
130 var topFrame = topFrameLineMatch && topFrameLineMatch[0];
132 // Examples of a frame:
133 // 1. '\n at someFunction (chrome-extension://
134 // pafkbggdmjlpgkdkcbjmhmfcdpncadgh/background.js:915:15)\n'
135 // 2. '\n at chrome-extension://pafkbggdmjlpgkdkcbjmhmfcdpncadgh/
136 // utility.js:269:18\n'
137 // 3. '\n at Function.target.(anonymous function) (extensions::
138 // SafeBuiltins:19:14)\n'
139 // 4. '\n at Event.dispatchToListener (event_bindings:382:22)\n'
141 // Find the the parentheses at the end of the line, if any.
142 var parenthesesMatch = topFrame.match(/\(.*\)\n/);
143 if (parenthesesMatch && parenthesesMatch[0]) {
145 parenthesesMatch[0].substring(1, parenthesesMatch[0].length - 2);
147 errorLocation = topFrame;
150 var topFrameElements = errorLocation.split(':');
151 // topFrameElements is an array that ends like:
152 // [N-3] //pafkbggdmjlpgkdkcbjmhmfcdpncadgh/utility.js
155 if (topFrameElements.length >= 3) {
156 file = topFrameElements[topFrameElements.length - 3];
157 line = topFrameElements[topFrameElements.length - 2];
161 var errorText = error.name;
162 if (error.canSendMessageToServer)
163 errorText = errorText + ': ' + error.message;
172 // We use relatively direct calls here because the instrumentation may be in
173 // a bad state. Wrappers and promises should not be involved in the reporting.
174 var request = buildServerRequest('POST', 'jserrors', 'application/json');
175 request.onloadend = function(event) {
176 console.log('sendErrorReport status: ' + request.status);
179 chrome.identity.getAuthToken({interactive: false}, function(token) {
181 request.setRequestHeader('Authorization', 'Bearer ' + token);
182 request.send(JSON.stringify(errorObject));
187 // Limiting 1 error report per background page load.
188 var errorReported = false;
191 * Reports an error to the server and the user, as appropriate.
192 * @param {Error} error Error to report.
194 function reportError(error) {
195 var message = 'Critical error:\n' + error.stack;
197 console.error(message);
199 if (!errorReported) {
200 errorReported = true;
201 chrome.metricsPrivate.getIsCrashReportingEnabled(function(isEnabled) {
203 sendErrorReport(error);
210 // Partial mirror of chrome.* for all instrumented functions.
211 var instrumented = {};
214 * Wrapper plugin. These plugins extend instrumentation added by
215 * wrapper.wrapCallback by adding code that executes before and after the call
216 * to the original callback provided by the extension.
219 * prologue: function (),
220 * epilogue: function ()
226 * Wrapper for callbacks. Used to add error handling and other services to
227 * callbacks for HTML and Chrome functions and events.
229 var wrapper = (function() {
231 * Factory for wrapper plugins. If specified, it's used to generate an
232 * instance of WrapperPlugin each time we wrap a callback (which corresponds
233 * to addListener call for Chrome events, and to every API call that specifies
234 * a callback). WrapperPlugin's lifetime ends when the callback for which it
235 * was generated, exits. It's possible to have several instances of
236 * WrapperPlugin at the same time.
237 * An instance of WrapperPlugin can have state that can be shared by its
238 * constructor, prologue() and epilogue(). Also WrapperPlugins can change
239 * state of other objects, for example, to do refcounting.
240 * @type {?function(): WrapperPlugin}
242 var wrapperPluginFactory = null;
245 * Registers a wrapper plugin factory.
246 * @param {function(): WrapperPlugin} factory Wrapper plugin factory.
248 function registerWrapperPluginFactory(factory) {
249 if (wrapperPluginFactory) {
250 reportError(buildErrorWithMessageForServer(
251 'registerWrapperPluginFactory: factory is already registered.'));
254 wrapperPluginFactory = factory;
258 * True if currently executed code runs in a callback or event handler that
259 * was instrumented by wrapper.wrapCallback() call.
262 var isInWrappedCallback = false;
265 * Required callbacks that are not yet called. Includes both task and non-task
266 * callbacks. This is a map from unique callback id to the stack at the moment
267 * when the callback was wrapped. This stack identifies the callback.
268 * Used only for diagnostics.
269 * @type {Object<number, string>}
271 var pendingCallbacks = {};
274 * Unique ID of the next callback.
277 var nextCallbackId = 0;
280 * Gets diagnostic string with the status of the wrapper.
281 * @return {string} Diagnostic string.
283 function debugGetStateString() {
284 return 'pendingCallbacks @' + Date.now() + ' = ' +
285 JSON.stringify(pendingCallbacks);
289 * Checks that we run in a wrapped callback.
291 function checkInWrappedCallback() {
292 if (!isInWrappedCallback) {
293 reportError(buildErrorWithMessageForServer(
294 'Not in instrumented callback'));
299 * Adds error processing to an API callback.
300 * @param {Function} callback Callback to instrument.
301 * @param {boolean=} opt_isEventListener True if the callback is a listener to
302 * a Chrome API event.
303 * @return {Function} Instrumented callback.
305 function wrapCallback(callback, opt_isEventListener) {
306 var callbackId = nextCallbackId++;
308 if (!opt_isEventListener) {
309 checkInWrappedCallback();
310 pendingCallbacks[callbackId] = new Error().stack + ' @' + Date.now();
313 // wrapperPluginFactory may be null before task manager is built, and in
315 var wrapperPluginInstance = wrapperPluginFactory && wrapperPluginFactory();
318 // This is the wrapper for the callback.
320 verify(!isInWrappedCallback, 'Re-entering instrumented callback');
321 isInWrappedCallback = true;
323 if (!opt_isEventListener)
324 delete pendingCallbacks[callbackId];
326 if (wrapperPluginInstance)
327 wrapperPluginInstance.prologue();
329 // Call the original callback.
330 var returnValue = callback.apply(null, arguments);
332 if (wrapperPluginInstance)
333 wrapperPluginInstance.epilogue();
335 verify(isInWrappedCallback,
336 'Instrumented callback is not instrumented upon exit');
337 isInWrappedCallback = false;
347 * Returns an instrumented function.
348 * @param {!Array<string>} functionIdentifierParts Path to the chrome.*
350 * @param {string} functionName Name of the chrome API function.
351 * @param {number} callbackParameter Index of the callback parameter to this
353 * @return {Function} An instrumented function.
355 function createInstrumentedFunction(
356 functionIdentifierParts,
360 // This is the wrapper for the API function. Pass the wrapped callback to
361 // the original function.
362 var callback = arguments[callbackParameter];
363 if (typeof callback != 'function') {
364 reportError(buildErrorWithMessageForServer(
365 'Argument ' + callbackParameter + ' of ' +
366 functionIdentifierParts.join('.') + '.' + functionName +
367 ' is not a function'));
369 arguments[callbackParameter] = wrapCallback(
370 callback, functionName == 'addListener');
372 var chromeContainer = chrome;
373 functionIdentifierParts.forEach(function(fragment) {
374 chromeContainer = chromeContainer[fragment];
376 return chromeContainer[functionName].
377 apply(chromeContainer, arguments);
382 * Instruments an API function to add error processing to its user
383 * code-provided callback.
384 * @param {string} functionIdentifier Full identifier of the function without
385 * the 'chrome.' portion.
386 * @param {number} callbackParameter Index of the callback parameter to this
389 function instrumentChromeApiFunction(functionIdentifier, callbackParameter) {
390 var functionIdentifierParts = functionIdentifier.split('.');
391 var functionName = functionIdentifierParts.pop();
392 var chromeContainer = chrome;
393 var instrumentedContainer = instrumented;
394 functionIdentifierParts.forEach(function(fragment) {
395 chromeContainer = chromeContainer[fragment];
396 if (!chromeContainer) {
397 reportError(buildErrorWithMessageForServer(
398 'Cannot instrument ' + functionIdentifier));
401 if (!(fragment in instrumentedContainer))
402 instrumentedContainer[fragment] = {};
404 instrumentedContainer = instrumentedContainer[fragment];
407 var targetFunction = chromeContainer[functionName];
408 if (!targetFunction) {
409 reportError(buildErrorWithMessageForServer(
410 'Cannot instrument ' + functionIdentifier));
413 instrumentedContainer[functionName] = createInstrumentedFunction(
414 functionIdentifierParts,
419 instrumentChromeApiFunction('runtime.onSuspend.addListener', 0);
421 instrumented.runtime.onSuspend.addListener(function() {
422 var stringifiedPendingCallbacks = JSON.stringify(pendingCallbacks);
424 stringifiedPendingCallbacks == '{}',
425 'Pending callbacks when unloading event page @' + Date.now() + ':' +
426 stringifiedPendingCallbacks);
430 wrapCallback: wrapCallback,
431 instrumentChromeApiFunction: instrumentChromeApiFunction,
432 registerWrapperPluginFactory: registerWrapperPluginFactory,
433 checkInWrappedCallback: checkInWrappedCallback,
434 debugGetStateString: debugGetStateString
438 wrapper.instrumentChromeApiFunction('alarms.get', 1);
439 wrapper.instrumentChromeApiFunction('alarms.onAlarm.addListener', 0);
440 wrapper.instrumentChromeApiFunction('identity.getAuthToken', 1);
441 wrapper.instrumentChromeApiFunction('identity.onSignInChanged.addListener', 0);
442 wrapper.instrumentChromeApiFunction('identity.removeCachedAuthToken', 1);
443 wrapper.instrumentChromeApiFunction('storage.local.get', 1);
444 wrapper.instrumentChromeApiFunction('webstorePrivate.getBrowserLogin', 0);
447 * Promise adapter for all JS promises to the task manager.
449 function registerPromiseAdapter() {
450 var originalThen = Promise.prototype.then;
451 var originalCatch = Promise.prototype.catch;
454 * Takes a promise and adds the callback tracker to it.
455 * @param {object} promise Promise that receives the callback tracker.
457 function instrumentPromise(promise) {
458 if (promise.__tracker === undefined) {
459 promise.__tracker = createPromiseCallbackTracker(promise);
463 Promise.prototype.then = function(onResolved, onRejected) {
464 instrumentPromise(this);
465 return this.__tracker.handleThen(onResolved, onRejected);
468 Promise.prototype.catch = function(onRejected) {
469 instrumentPromise(this);
470 return this.__tracker.handleCatch(onRejected);
474 * Promise Callback Tracker.
475 * Handles coordination of 'then' and 'catch' callbacks in a task
476 * manager compatible way. For an individual promise, either the 'then'
477 * arguments or the 'catch' arguments will be processed, never both.
480 * var p = new Promise([Function]);
484 * On resolution, [ThenA] and [ThenB] will be used. [CatchA] is discarded.
485 * On rejection, vice versa.
488 * Chained promises create a new promise that is tracked separately from
489 * the originaing promise, as the example below demonstrates:
491 * var p = new Promise([Function]));
492 * p.then([ThenA]).then([ThenB]).catch([CatchA]);
494 * | | + Returns a new promise.
495 * | + Returns a new promise.
496 * + Returns a new promise.
498 * Four promises exist in the above statement, each with its own
499 * resolution and rejection state. However, by default, this state is
500 * chained to the previous promise's resolution or rejection
503 * If p resolves, then the 'then' calls will execute until all the 'then'
504 * clauses are executed. If the result of either [ThenA] or [ThenB] is a
505 * promise, then that execution state will guide the remaining chain.
506 * Similarly, if [CatchA] returns a promise, it can also guide the
507 * remaining chain. In this specific case, the chain ends, so there
508 * is nothing left to do.
509 * @param {object} promise Promise being tracked.
510 * @return {object} A promise callback tracker.
512 function createPromiseCallbackTracker(promise) {
514 * Callback Tracker. Holds an array of callbacks created for this promise.
515 * The indirection allows quick checks against the array and clearing the
516 * array without ugly splicing and copying.
518 * callback: array<Function>=
523 /** @type {CallbackTracker} */
524 var thenTracker = {callbacks: []};
525 /** @type {CallbackTracker} */
526 var catchTracker = {callbacks: []};
529 * Returns true if the specified value is callable.
530 * @param {*} value Value to check.
531 * @return {boolean} True if the value is a callable.
533 function isCallable(value) {
534 return typeof value === 'function';
538 * Takes a tracker and clears its callbacks in a manner consistent with
539 * the task manager. For the task manager, it also calls all callbacks
540 * by no-oping them first and then calling them.
541 * @param {CallbackTracker} tracker Tracker to clear.
543 function clearTracker(tracker) {
544 if (tracker.callbacks) {
545 var callbacksToClear = tracker.callbacks;
546 // No-ops all callbacks of this type.
547 tracker.callbacks = undefined;
548 // Do not wrap the promise then argument!
549 // It will call wrapped callbacks.
550 originalThen.call(Promise.resolve(), function() {
551 for (var i = 0; i < callbacksToClear.length; i++) {
552 callbacksToClear[i]();
559 * Takes the argument to a 'then' or 'catch' function and applies
560 * a wrapping to callables consistent to ECMA promises.
561 * @param {*} maybeCallback Argument to 'then' or 'catch'.
562 * @param {CallbackTracker} sameTracker Tracker for the call type.
563 * Example: If the argument is from a 'then' call, use thenTracker.
564 * @param {CallbackTracker} otherTracker Tracker for the opposing call type.
565 * Example: If the argument is from a 'then' call, use catchTracker.
566 * @return {*} Consumable argument with necessary wrapping applied.
568 function registerAndWrapMaybeCallback(
569 maybeCallback, sameTracker, otherTracker) {
570 // If sameTracker.callbacks is undefined, we've reached an ending state
571 // that means this callback will never be called back.
572 // We will still forward this call on to let the promise system
573 // handle further processing, but since this promise is in an ending state
574 // we can be confident it will never be called back.
575 if (isCallable(maybeCallback) &&
576 !maybeCallback.wrappedByPromiseTracker &&
577 sameTracker.callbacks) {
578 var handler = wrapper.wrapCallback(function() {
579 if (sameTracker.callbacks) {
580 clearTracker(otherTracker);
581 return maybeCallback.apply(null, arguments);
584 // Harmony promises' catch calls will call into handleThen,
585 // double-wrapping all catch callbacks. Regular promise catch calls do
586 // not call into handleThen. Setting an attribute on the wrapped
587 // function is compatible with both promise implementations.
588 handler.wrappedByPromiseTracker = true;
589 sameTracker.callbacks.push(handler);
592 return maybeCallback;
597 * Tracks then calls equivalent to Promise.prototype.then.
598 * @param {*} onResolved Argument to use if the promise is resolved.
599 * @param {*} onRejected Argument to use if the promise is rejected.
600 * @return {object} Promise resulting from the 'then' call.
602 function handleThen(onResolved, onRejected) {
603 var resolutionHandler =
604 registerAndWrapMaybeCallback(onResolved, thenTracker, catchTracker);
605 var rejectionHandler =
606 registerAndWrapMaybeCallback(onRejected, catchTracker, thenTracker);
607 return originalThen.call(promise, resolutionHandler, rejectionHandler);
611 * Tracks then calls equivalent to Promise.prototype.catch.
612 * @param {*} onRejected Argument to use if the promise is rejected.
613 * @return {object} Promise resulting from the 'catch' call.
615 function handleCatch(onRejected) {
616 var rejectionHandler =
617 registerAndWrapMaybeCallback(onRejected, catchTracker, thenTracker);
618 return originalCatch.call(promise, rejectionHandler);
621 // Register at least one resolve and reject callback so we always receive
622 // a callback to update the task manager and clear the callbacks
623 // that will never occur.
625 // The then form is used to avoid reentrancy by handleCatch,
626 // which ends up calling handleThen.
627 handleThen(function() {}, function() {});
630 handleThen: handleThen,
631 handleCatch: handleCatch
636 registerPromiseAdapter();
639 * Control promise rejection.
642 var PromiseRejection = {
643 /** Disallow promise rejection */
645 /** Allow promise rejection */
650 * Provides the promise equivalent of instrumented.storage.local.get.
651 * @param {Object} defaultStorageObject Default storage object to fill.
652 * @param {PromiseRejection=} opt_allowPromiseRejection If
653 * PromiseRejection.ALLOW, allow promise rejection on errors, otherwise the
654 * default storage object is resolved.
655 * @return {Promise} A promise that fills the default storage object. On
656 * failure, if promise rejection is allowed, the promise is rejected,
657 * otherwise it is resolved to the default storage object.
659 function fillFromChromeLocalStorage(
660 defaultStorageObject,
661 opt_allowPromiseRejection) {
662 return new Promise(function(resolve, reject) {
663 // We have to create a keys array because keys with a default value
664 // of undefined will cause that key to not be looked up!
666 for (var key in defaultStorageObject) {
669 instrumented.storage.local.get(keysToGet, function(items) {
671 // Merge the result with the default storage object to ensure all keys
672 // requested have either the default value or the retrieved storage
675 for (var key in defaultStorageObject) {
676 result[key] = (key in items) ? items[key] : defaultStorageObject[key];
679 } else if (opt_allowPromiseRejection === PromiseRejection.ALLOW) {
682 resolve(defaultStorageObject);
689 * Builds the object to manage tasks (mutually exclusive chains of events).
690 * @param {function(string, string): boolean} areConflicting Function that
691 * checks if a new task can't be added to a task queue that contains an
693 * @return {Object} Task manager interface.
695 function buildTaskManager(areConflicting) {
697 * Queue of scheduled tasks. The first element, if present, corresponds to the
698 * currently running task.
699 * @type {Array<Object<function()>>}
704 * Count of unfinished callbacks of the current task.
707 var taskPendingCallbackCount = 0;
710 * True if currently executed code is a part of a task.
713 var isInTask = false;
716 * Starts the first queued task.
718 function startFirst() {
719 verify(queue.length >= 1, 'startFirst: queue is empty');
720 verify(!isInTask, 'startFirst: already in task');
723 // Start the oldest queued task, but don't remove it from the queue.
725 taskPendingCallbackCount == 0,
726 'tasks.startFirst: still have pending task callbacks: ' +
727 taskPendingCallbackCount +
728 ', queue = ' + JSON.stringify(queue) + ', ' +
729 wrapper.debugGetStateString());
730 var entry = queue[0];
731 console.log('Starting task ' + entry.name);
735 verify(isInTask, 'startFirst: not in task at exit');
737 if (taskPendingCallbackCount == 0)
742 * Checks if a new task can be added to the task queue.
743 * @param {string} taskName Name of the new task.
744 * @return {boolean} Whether the new task can be added.
746 function canQueue(taskName) {
747 for (var i = 0; i < queue.length; ++i) {
748 if (areConflicting(taskName, queue[i].name)) {
749 console.log('Conflict: new=' + taskName +
750 ', scheduled=' + queue[i].name);
759 * Adds a new task. If another task is not running, runs the task immediately.
760 * If any task in the queue is not compatible with the task, ignores the new
761 * task. Otherwise, stores the task for future execution.
762 * @param {string} taskName Name of the task.
763 * @param {function()} task Function to run.
765 function add(taskName, task) {
766 wrapper.checkInWrappedCallback();
767 console.log('Adding task ' + taskName);
768 if (!canQueue(taskName))
771 queue.push({name: taskName, task: task});
773 if (queue.length == 1) {
779 * Completes the current task and starts the next queued task if available.
782 verify(queue.length >= 1,
783 'tasks.finish: The task queue is empty');
784 console.log('Finishing task ' + queue[0].name);
787 if (queue.length >= 1)
791 instrumented.runtime.onSuspend.addListener(function() {
794 'Incomplete task when unloading event page,' +
795 ' queue = ' + JSON.stringify(queue) + ', ' +
796 wrapper.debugGetStateString());
801 * Wrapper plugin for tasks.
804 function TasksWrapperPlugin() {
805 this.isTaskCallback = isInTask;
806 if (this.isTaskCallback)
807 ++taskPendingCallbackCount;
810 TasksWrapperPlugin.prototype = {
812 * Plugin code to be executed before invoking the original callback.
814 prologue: function() {
815 if (this.isTaskCallback) {
816 verify(!isInTask, 'TasksWrapperPlugin.prologue: already in task');
822 * Plugin code to be executed after invoking the original callback.
824 epilogue: function() {
825 if (this.isTaskCallback) {
826 verify(isInTask, 'TasksWrapperPlugin.epilogue: not in task at exit');
828 if (--taskPendingCallbackCount == 0)
834 wrapper.registerWrapperPluginFactory(function() {
835 return new TasksWrapperPlugin();
844 * Builds an object to manage retrying activities with exponential backoff.
845 * @param {string} name Name of this attempt manager.
846 * @param {function()} attempt Activity that the manager retries until it
847 * calls 'stop' method.
848 * @param {number} initialDelaySeconds Default first delay until first retry.
849 * @param {number} maximumDelaySeconds Maximum delay between retries.
850 * @return {Object} Attempt manager interface.
852 function buildAttemptManager(
853 name, attempt, initialDelaySeconds, maximumDelaySeconds) {
854 var alarmName = 'attempt-scheduler-' + name;
855 var currentDelayStorageKey = 'current-delay-' + name;
858 * Creates an alarm for the next attempt. The alarm is repeating for the case
859 * when the next attempt crashes before registering next alarm.
860 * @param {number} delaySeconds Delay until next retry.
862 function createAlarm(delaySeconds) {
864 delayInMinutes: delaySeconds / 60,
865 periodInMinutes: maximumDelaySeconds / 60
867 chrome.alarms.create(alarmName, alarmInfo);
871 * Indicates if this attempt manager has started.
872 * @param {function(boolean)} callback The function's boolean parameter is
873 * true if the attempt manager has started, false otherwise.
875 function isRunning(callback) {
876 instrumented.alarms.get(alarmName, function(alarmInfo) {
877 callback(!!alarmInfo);
882 * Schedules the alarm with a random factor to reduce the chance that all
883 * clients will fire their timers at the same time.
884 * @param {number} durationSeconds Number of seconds before firing the alarm.
886 function scheduleAlarm(durationSeconds) {
887 durationSeconds = Math.min(durationSeconds, maximumDelaySeconds);
888 var randomizedRetryDuration = durationSeconds * (1 + 0.2 * Math.random());
890 createAlarm(randomizedRetryDuration);
893 items[currentDelayStorageKey] = randomizedRetryDuration;
894 chrome.storage.local.set(items);
898 * Starts repeated attempts.
899 * @param {number=} opt_firstDelaySeconds Time until the first attempt, if
900 * specified. Otherwise, initialDelaySeconds will be used for the first
903 function start(opt_firstDelaySeconds) {
904 if (opt_firstDelaySeconds) {
905 createAlarm(opt_firstDelaySeconds);
906 chrome.storage.local.remove(currentDelayStorageKey);
908 scheduleAlarm(initialDelaySeconds);
913 * Stops repeated attempts.
916 chrome.alarms.clear(alarmName);
917 chrome.storage.local.remove(currentDelayStorageKey);
921 * Schedules an exponential backoff retry.
922 * @return {Promise} A promise to schedule the retry.
924 function scheduleRetry() {
926 request[currentDelayStorageKey] = undefined;
927 return fillFromChromeLocalStorage(request, PromiseRejection.ALLOW)
929 request[currentDelayStorageKey] = maximumDelaySeconds;
930 return Promise.resolve(request);
932 .then(function(items) {
933 console.log('scheduleRetry-get-storage ' + JSON.stringify(items));
934 var retrySeconds = initialDelaySeconds;
935 if (items[currentDelayStorageKey]) {
936 retrySeconds = items[currentDelayStorageKey] * 2;
938 scheduleAlarm(retrySeconds);
942 instrumented.alarms.onAlarm.addListener(function(alarm) {
943 if (alarm.name == alarmName)
944 isRunning(function(running) {
952 scheduleRetry: scheduleRetry,
958 // TODO(robliao): Use signed-in state change watch API when it's available.
960 * Wraps chrome.identity to provide limited listening support for
961 * the sign in state by polling periodically for the auth token.
962 * @return {Object} The Authentication Manager interface.
964 function buildAuthenticationManager() {
965 var alarmName = 'sign-in-alarm';
968 * Gets an OAuth2 access token.
969 * @return {Promise} A promise to get the authentication token. If there is
970 * no token, the request is rejected.
972 function getAuthToken() {
973 return new Promise(function(resolve, reject) {
974 instrumented.identity.getAuthToken({interactive: false}, function(token) {
975 if (chrome.runtime.lastError || !token) {
985 * Determines the active account's login (username).
986 * @return {Promise} A promise to determine the current account's login.
988 function getLogin() {
989 return new Promise(function(resolve) {
990 instrumented.webstorePrivate.getBrowserLogin(function(accountInfo) {
991 resolve(accountInfo.login);
997 * Determines whether there is an account attached to the profile.
998 * @return {Promise} A promise to determine if there is an account attached
1001 function isSignedIn() {
1002 return getLogin().then(function(login) {
1003 return Promise.resolve(!!login);
1008 * Removes the specified cached token.
1009 * @param {string} token Authentication Token to remove from the cache.
1010 * @return {Promise} A promise that resolves on completion.
1012 function removeToken(token) {
1013 return new Promise(function(resolve) {
1014 instrumented.identity.removeCachedAuthToken({token: token}, function() {
1015 // Let Chrome know about a possible problem with the token.
1025 * Registers a listener that gets called back when the signed in state
1026 * is found to be changed.
1027 * @param {function()} callback Called when the answer to isSignedIn changes.
1029 function addListener(callback) {
1030 listeners.push(callback);
1034 * Checks if the last signed in state matches the current one.
1035 * If it doesn't, it notifies the listeners of the change.
1037 function checkAndNotifyListeners() {
1038 isSignedIn().then(function(signedIn) {
1039 fillFromChromeLocalStorage({lastSignedInState: undefined})
1040 .then(function(items) {
1041 if (items.lastSignedInState != signedIn) {
1042 chrome.storage.local.set(
1043 {lastSignedInState: signedIn});
1044 listeners.forEach(function(callback) {
1052 instrumented.identity.onSignInChanged.addListener(function() {
1053 checkAndNotifyListeners();
1056 instrumented.alarms.onAlarm.addListener(function(alarm) {
1057 if (alarm.name == alarmName)
1058 checkAndNotifyListeners();
1061 // Poll for the sign in state every hour.
1062 // One hour is just an arbitrary amount of time chosen.
1063 chrome.alarms.create(alarmName, {periodInMinutes: 60});
1066 addListener: addListener,
1067 getAuthToken: getAuthToken,
1069 isSignedIn: isSignedIn,
1070 removeToken: removeToken