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<string, 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