Upstream version 11.40.277.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / google_now / utility.js
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.
4
5 'use strict';
6
7 /**
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.
23  */
24
25 // TODO(vadimt): Use server name in the manifest.
26
27 /**
28  * Notification server URL.
29  */
30 var NOTIFICATION_CARDS_URL = 'https://www.googleapis.com/chromenow/v1';
31
32 /**
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.
37  */
38 function isInDebugMode() {
39   return localStorage.debug_mode === 'true';
40 }
41
42 /**
43  * Initializes for debug or release modes of operation.
44  */
45 function initializeDebug() {
46   if (isInDebugMode()) {
47     NOTIFICATION_CARDS_URL =
48         localStorage['server_url'] || NOTIFICATION_CARDS_URL;
49   }
50 }
51
52 initializeDebug();
53
54 /**
55  * Conditionally allow console.log output based off of the debug mode.
56  */
57 console.log = function() {
58   var originalConsoleLog = console.log;
59   return function() {
60     if (isInDebugMode()) {
61       originalConsoleLog.apply(console, arguments);
62     }
63   };
64 }();
65
66 /**
67  * Explanation Card Storage.
68  */
69 if (localStorage['explanatoryCardsShown'] === undefined)
70   localStorage['explanatoryCardsShown'] = 0;
71
72 /**
73  * Location Card Count Cleanup.
74  */
75 if (localStorage.locationCardsShown !== undefined)
76   localStorage.removeItem('locationCardsShown');
77
78 /**
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
81  *     server.
82  * @return {Error} Error object.
83  */
84 function buildErrorWithMessageForServer(message) {
85   var error = new Error(message);
86   error.canSendMessageToServer = true;
87   return error;
88 }
89
90 /**
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
94  *     false.
95  */
96 function verify(condition, message) {
97   if (!condition)
98     throw buildErrorWithMessageForServer('ASSERT: ' + message);
99 }
100
101 /**
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.
107  */
108 function buildServerRequest(method, handlerName, opt_contentType) {
109   var request = new XMLHttpRequest();
110
111   request.responseType = 'text';
112   request.open(method, NOTIFICATION_CARDS_URL + '/' + handlerName, true);
113   if (opt_contentType)
114     request.setRequestHeader('Content-type', opt_contentType);
115
116   return request;
117 }
118
119 /**
120  * Sends an error report to the server.
121  * @param {Error} error Error to send.
122  */
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');
127   var file;
128   var line;
129   var topFrameLineMatch = filteredStack.match(/\n    at .*\n/);
130   var topFrame = topFrameLineMatch && topFrameLineMatch[0];
131   if (topFrame) {
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'
140     var errorLocation;
141     // Find the the parentheses at the end of the line, if any.
142     var parenthesesMatch = topFrame.match(/\(.*\)\n/);
143     if (parenthesesMatch && parenthesesMatch[0]) {
144       errorLocation =
145           parenthesesMatch[0].substring(1, parenthesesMatch[0].length - 2);
146     } else {
147       errorLocation = topFrame;
148     }
149
150     var topFrameElements = errorLocation.split(':');
151     // topFrameElements is an array that ends like:
152     // [N-3] //pafkbggdmjlpgkdkcbjmhmfcdpncadgh/utility.js
153     // [N-2] 308
154     // [N-1] 19
155     if (topFrameElements.length >= 3) {
156       file = topFrameElements[topFrameElements.length - 3];
157       line = topFrameElements[topFrameElements.length - 2];
158     }
159   }
160
161   var errorText = error.name;
162   if (error.canSendMessageToServer)
163     errorText = errorText + ': ' + error.message;
164
165   var errorObject = {
166     message: errorText,
167     file: file,
168     line: line,
169     trace: filteredStack
170   };
171
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);
177   };
178
179   chrome.identity.getAuthToken({interactive: false}, function(token) {
180     if (token) {
181       request.setRequestHeader('Authorization', 'Bearer ' + token);
182       request.send(JSON.stringify(errorObject));
183     }
184   });
185 }
186
187 // Limiting 1 error report per background page load.
188 var errorReported = false;
189
190 /**
191  * Reports an error to the server and the user, as appropriate.
192  * @param {Error} error Error to report.
193  */
194 function reportError(error) {
195   var message = 'Critical error:\n' + error.stack;
196   if (isInDebugMode())
197     console.error(message);
198
199   if (!errorReported) {
200     errorReported = true;
201     chrome.metricsPrivate.getIsCrashReportingEnabled(function(isEnabled) {
202       if (isEnabled)
203         sendErrorReport(error);
204       if (isInDebugMode())
205         alert(message);
206     });
207   }
208 }
209
210 // Partial mirror of chrome.* for all instrumented functions.
211 var instrumented = {};
212
213 /**
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.
217  *
218  * @typedef {{
219  *   prologue: function (),
220  *   epilogue: function ()
221  * }}
222  */
223 var WrapperPlugin;
224
225 /**
226  * Wrapper for callbacks. Used to add error handling and other services to
227  * callbacks for HTML and Chrome functions and events.
228  */
229 var wrapper = (function() {
230   /**
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}
241    */
242   var wrapperPluginFactory = null;
243
244   /**
245    * Registers a wrapper plugin factory.
246    * @param {function(): WrapperPlugin} factory Wrapper plugin factory.
247    */
248   function registerWrapperPluginFactory(factory) {
249     if (wrapperPluginFactory) {
250       reportError(buildErrorWithMessageForServer(
251           'registerWrapperPluginFactory: factory is already registered.'));
252     }
253
254     wrapperPluginFactory = factory;
255   }
256
257   /**
258    * True if currently executed code runs in a callback or event handler that
259    * was instrumented by wrapper.wrapCallback() call.
260    * @type {boolean}
261    */
262   var isInWrappedCallback = false;
263
264   /**
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>}
270    */
271   var pendingCallbacks = {};
272
273   /**
274    * Unique ID of the next callback.
275    * @type {number}
276    */
277   var nextCallbackId = 0;
278
279   /**
280    * Gets diagnostic string with the status of the wrapper.
281    * @return {string} Diagnostic string.
282    */
283   function debugGetStateString() {
284     return 'pendingCallbacks @' + Date.now() + ' = ' +
285         JSON.stringify(pendingCallbacks);
286   }
287
288   /**
289    * Checks that we run in a wrapped callback.
290    */
291   function checkInWrappedCallback() {
292     if (!isInWrappedCallback) {
293       reportError(buildErrorWithMessageForServer(
294           'Not in instrumented callback'));
295     }
296   }
297
298   /**
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.
304    */
305   function wrapCallback(callback, opt_isEventListener) {
306     var callbackId = nextCallbackId++;
307
308     if (!opt_isEventListener) {
309       checkInWrappedCallback();
310       pendingCallbacks[callbackId] = new Error().stack + ' @' + Date.now();
311     }
312
313     // wrapperPluginFactory may be null before task manager is built, and in
314     // tests.
315     var wrapperPluginInstance = wrapperPluginFactory && wrapperPluginFactory();
316
317     return function() {
318       // This is the wrapper for the callback.
319       try {
320         verify(!isInWrappedCallback, 'Re-entering instrumented callback');
321         isInWrappedCallback = true;
322
323         if (!opt_isEventListener)
324           delete pendingCallbacks[callbackId];
325
326         if (wrapperPluginInstance)
327           wrapperPluginInstance.prologue();
328
329         // Call the original callback.
330         var returnValue = callback.apply(null, arguments);
331
332         if (wrapperPluginInstance)
333           wrapperPluginInstance.epilogue();
334
335         verify(isInWrappedCallback,
336                'Instrumented callback is not instrumented upon exit');
337         isInWrappedCallback = false;
338
339         return returnValue;
340       } catch (error) {
341         reportError(error);
342       }
343     };
344   }
345
346   /**
347    * Returns an instrumented function.
348    * @param {!Array.<string>} functionIdentifierParts Path to the chrome.*
349    *     function.
350    * @param {string} functionName Name of the chrome API function.
351    * @param {number} callbackParameter Index of the callback parameter to this
352    *     API function.
353    * @return {Function} An instrumented function.
354    */
355   function createInstrumentedFunction(
356       functionIdentifierParts,
357       functionName,
358       callbackParameter) {
359     return function() {
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'));
368       }
369       arguments[callbackParameter] = wrapCallback(
370           callback, functionName == 'addListener');
371
372       var chromeContainer = chrome;
373       functionIdentifierParts.forEach(function(fragment) {
374         chromeContainer = chromeContainer[fragment];
375       });
376       return chromeContainer[functionName].
377           apply(chromeContainer, arguments);
378     };
379   }
380
381   /**
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
387    *     API function.
388    */
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));
399       }
400
401       if (!(fragment in instrumentedContainer))
402         instrumentedContainer[fragment] = {};
403
404       instrumentedContainer = instrumentedContainer[fragment];
405     });
406
407     var targetFunction = chromeContainer[functionName];
408     if (!targetFunction) {
409       reportError(buildErrorWithMessageForServer(
410           'Cannot instrument ' + functionIdentifier));
411     }
412
413     instrumentedContainer[functionName] = createInstrumentedFunction(
414         functionIdentifierParts,
415         functionName,
416         callbackParameter);
417   }
418
419   instrumentChromeApiFunction('runtime.onSuspend.addListener', 0);
420
421   instrumented.runtime.onSuspend.addListener(function() {
422     var stringifiedPendingCallbacks = JSON.stringify(pendingCallbacks);
423     verify(
424         stringifiedPendingCallbacks == '{}',
425         'Pending callbacks when unloading event page @' + Date.now() + ':' +
426         stringifiedPendingCallbacks);
427   });
428
429   return {
430     wrapCallback: wrapCallback,
431     instrumentChromeApiFunction: instrumentChromeApiFunction,
432     registerWrapperPluginFactory: registerWrapperPluginFactory,
433     checkInWrappedCallback: checkInWrappedCallback,
434     debugGetStateString: debugGetStateString
435   };
436 })();
437
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);
445
446 /**
447  * Promise adapter for all JS promises to the task manager.
448  */
449 function registerPromiseAdapter() {
450   var originalThen = Promise.prototype.then;
451   var originalCatch = Promise.prototype.catch;
452
453   /**
454    * Takes a promise and adds the callback tracker to it.
455    * @param {object} promise Promise that receives the callback tracker.
456    */
457   function instrumentPromise(promise) {
458     if (promise.__tracker === undefined) {
459       promise.__tracker = createPromiseCallbackTracker(promise);
460     }
461   }
462
463   Promise.prototype.then = function(onResolved, onRejected) {
464     instrumentPromise(this);
465     return this.__tracker.handleThen(onResolved, onRejected);
466   };
467
468   Promise.prototype.catch = function(onRejected) {
469     instrumentPromise(this);
470     return this.__tracker.handleCatch(onRejected);
471   };
472
473   /**
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.
478    *
479    * Example:
480    *     var p = new Promise([Function]);
481    *     p.then([ThenA]);
482    *     p.then([ThenB]);
483    *     p.catch([CatchA]);
484    *     On resolution, [ThenA] and [ThenB] will be used. [CatchA] is discarded.
485    *     On rejection, vice versa.
486    *
487    * Clarification:
488    *     Chained promises create a new promise that is tracked separately from
489    *     the originaing promise, as the example below demonstrates:
490    *
491    *     var p = new Promise([Function]));
492    *     p.then([ThenA]).then([ThenB]).catch([CatchA]);
493    *         ^             ^             ^
494    *         |             |             + Returns a new promise.
495    *         |             + Returns a new promise.
496    *         + Returns a new promise.
497    *
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
501    *     state.
502    *
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.
511    */
512   function createPromiseCallbackTracker(promise) {
513     /**
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.
517      * @typedef {{
518      *   callback: array.<Function>=
519      * }}
520      */
521     var CallbackTracker;
522
523     /** @type {CallbackTracker} */
524     var thenTracker = {callbacks: []};
525     /** @type {CallbackTracker} */
526     var catchTracker = {callbacks: []};
527
528     /**
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.
532      */
533     function isCallable(value) {
534       return typeof value === 'function';
535     }
536
537     /**
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.
542      */
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]();
553           }
554         });
555       }
556     }
557
558     /**
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.
567      */
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);
582           }
583         }, false);
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);
590         return handler;
591       } else {
592         return maybeCallback;
593       }
594     }
595
596     /**
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.
601      */
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);
608     }
609
610     /**
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.
614      */
615     function handleCatch(onRejected) {
616       var rejectionHandler =
617           registerAndWrapMaybeCallback(onRejected, catchTracker, thenTracker);
618       return originalCatch.call(promise, rejectionHandler);
619     }
620
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.
624     //
625     // The then form is used to avoid reentrancy by handleCatch,
626     // which ends up calling handleThen.
627     handleThen(function() {}, function() {});
628
629     return {
630       handleThen: handleThen,
631       handleCatch: handleCatch
632     };
633   }
634 }
635
636 registerPromiseAdapter();
637
638 /**
639  * Control promise rejection.
640  * @enum {number}
641  */
642 var PromiseRejection = {
643   /** Disallow promise rejection */
644   DISALLOW: 0,
645   /** Allow promise rejection */
646   ALLOW: 1
647 };
648
649 /**
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.
658  */
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!
665     var keysToGet = [];
666     for (var key in defaultStorageObject) {
667       keysToGet.push(key);
668     }
669     instrumented.storage.local.get(keysToGet, function(items) {
670       if (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
673         // value.
674         var result = {};
675         for (var key in defaultStorageObject) {
676           result[key] = (key in items) ? items[key] : defaultStorageObject[key];
677         }
678         resolve(result);
679       } else if (opt_allowPromiseRejection === PromiseRejection.ALLOW) {
680         reject();
681       } else {
682         resolve(defaultStorageObject);
683       }
684     });
685   });
686 }
687
688 /**
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
692  *     existing task.
693  * @return {Object} Task manager interface.
694  */
695 function buildTaskManager(areConflicting) {
696   /**
697    * Queue of scheduled tasks. The first element, if present, corresponds to the
698    * currently running task.
699    * @type {Array.<Object.<string, function()>>}
700    */
701   var queue = [];
702
703   /**
704    * Count of unfinished callbacks of the current task.
705    * @type {number}
706    */
707   var taskPendingCallbackCount = 0;
708
709   /**
710    * True if currently executed code is a part of a task.
711    * @type {boolean}
712    */
713   var isInTask = false;
714
715   /**
716    * Starts the first queued task.
717    */
718   function startFirst() {
719     verify(queue.length >= 1, 'startFirst: queue is empty');
720     verify(!isInTask, 'startFirst: already in task');
721     isInTask = true;
722
723     // Start the oldest queued task, but don't remove it from the queue.
724     verify(
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);
732
733     entry.task();
734
735     verify(isInTask, 'startFirst: not in task at exit');
736     isInTask = false;
737     if (taskPendingCallbackCount == 0)
738       finish();
739   }
740
741   /**
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.
745    */
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);
751         return false;
752       }
753     }
754
755     return true;
756   }
757
758   /**
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.
764    */
765   function add(taskName, task) {
766     wrapper.checkInWrappedCallback();
767     console.log('Adding task ' + taskName);
768     if (!canQueue(taskName))
769       return;
770
771     queue.push({name: taskName, task: task});
772
773     if (queue.length == 1) {
774       startFirst();
775     }
776   }
777
778   /**
779    * Completes the current task and starts the next queued task if available.
780    */
781   function finish() {
782     verify(queue.length >= 1,
783            'tasks.finish: The task queue is empty');
784     console.log('Finishing task ' + queue[0].name);
785     queue.shift();
786
787     if (queue.length >= 1)
788       startFirst();
789   }
790
791   instrumented.runtime.onSuspend.addListener(function() {
792     verify(
793         queue.length == 0,
794         'Incomplete task when unloading event page,' +
795         ' queue = ' + JSON.stringify(queue) + ', ' +
796         wrapper.debugGetStateString());
797   });
798
799
800   /**
801    * Wrapper plugin for tasks.
802    * @constructor
803    */
804   function TasksWrapperPlugin() {
805     this.isTaskCallback = isInTask;
806     if (this.isTaskCallback)
807       ++taskPendingCallbackCount;
808   }
809
810   TasksWrapperPlugin.prototype = {
811     /**
812      * Plugin code to be executed before invoking the original callback.
813      */
814     prologue: function() {
815       if (this.isTaskCallback) {
816         verify(!isInTask, 'TasksWrapperPlugin.prologue: already in task');
817         isInTask = true;
818       }
819     },
820
821     /**
822      * Plugin code to be executed after invoking the original callback.
823      */
824     epilogue: function() {
825       if (this.isTaskCallback) {
826         verify(isInTask, 'TasksWrapperPlugin.epilogue: not in task at exit');
827         isInTask = false;
828         if (--taskPendingCallbackCount == 0)
829           finish();
830       }
831     }
832   };
833
834   wrapper.registerWrapperPluginFactory(function() {
835     return new TasksWrapperPlugin();
836   });
837
838   return {
839     add: add
840   };
841 }
842
843 /**
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.
851  */
852 function buildAttemptManager(
853     name, attempt, initialDelaySeconds, maximumDelaySeconds) {
854   var alarmName = 'attempt-scheduler-' + name;
855   var currentDelayStorageKey = 'current-delay-' + name;
856
857   /**
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.
861    */
862   function createAlarm(delaySeconds) {
863     var alarmInfo = {
864       delayInMinutes: delaySeconds / 60,
865       periodInMinutes: maximumDelaySeconds / 60
866     };
867     chrome.alarms.create(alarmName, alarmInfo);
868   }
869
870   /**
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.
874    */
875   function isRunning(callback) {
876     instrumented.alarms.get(alarmName, function(alarmInfo) {
877       callback(!!alarmInfo);
878     });
879   }
880
881   /**
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.
885    */
886   function scheduleAlarm(durationSeconds) {
887     durationSeconds = Math.min(durationSeconds, maximumDelaySeconds);
888     var randomizedRetryDuration = durationSeconds * (1 + 0.2 * Math.random());
889
890     createAlarm(randomizedRetryDuration);
891
892     var items = {};
893     items[currentDelayStorageKey] = randomizedRetryDuration;
894     chrome.storage.local.set(items);
895   }
896
897   /**
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
901    *     attempt.
902    */
903   function start(opt_firstDelaySeconds) {
904     if (opt_firstDelaySeconds) {
905       createAlarm(opt_firstDelaySeconds);
906       chrome.storage.local.remove(currentDelayStorageKey);
907     } else {
908       scheduleAlarm(initialDelaySeconds);
909     }
910   }
911
912   /**
913    * Stops repeated attempts.
914    */
915   function stop() {
916     chrome.alarms.clear(alarmName);
917     chrome.storage.local.remove(currentDelayStorageKey);
918   }
919
920   /**
921    * Schedules an exponential backoff retry.
922    * @return {Promise} A promise to schedule the retry.
923    */
924   function scheduleRetry() {
925     var request = {};
926     request[currentDelayStorageKey] = undefined;
927     return fillFromChromeLocalStorage(request, PromiseRejection.ALLOW)
928         .catch(function() {
929           request[currentDelayStorageKey] = maximumDelaySeconds;
930           return Promise.resolve(request);
931         })
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;
937           }
938           scheduleAlarm(retrySeconds);
939         });
940   }
941
942   instrumented.alarms.onAlarm.addListener(function(alarm) {
943     if (alarm.name == alarmName)
944       isRunning(function(running) {
945         if (running)
946           attempt();
947       });
948   });
949
950   return {
951     start: start,
952     scheduleRetry: scheduleRetry,
953     stop: stop,
954     isRunning: isRunning
955   };
956 }
957
958 // TODO(robliao): Use signed-in state change watch API when it's available.
959 /**
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.
963  */
964 function buildAuthenticationManager() {
965   var alarmName = 'sign-in-alarm';
966
967   /**
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.
971    */
972   function getAuthToken() {
973     return new Promise(function(resolve, reject) {
974       instrumented.identity.getAuthToken({interactive: false}, function(token) {
975         if (chrome.runtime.lastError || !token) {
976           reject();
977         } else {
978           resolve(token);
979         }
980       });
981     });
982   }
983
984   /**
985    * Determines whether there is an account attached to the profile.
986    * @return {Promise} A promise to determine if there is an account attached
987    *     to the profile.
988    */
989   function isSignedIn() {
990     return new Promise(function(resolve) {
991       instrumented.webstorePrivate.getBrowserLogin(function(accountInfo) {
992         resolve(!!accountInfo.login);
993       });
994     });
995   }
996
997   /**
998    * Removes the specified cached token.
999    * @param {string} token Authentication Token to remove from the cache.
1000    * @return {Promise} A promise that resolves on completion.
1001    */
1002   function removeToken(token) {
1003     return new Promise(function(resolve) {
1004       instrumented.identity.removeCachedAuthToken({token: token}, function() {
1005         // Let Chrome know about a possible problem with the token.
1006         getAuthToken();
1007         resolve();
1008       });
1009     });
1010   }
1011
1012   var listeners = [];
1013
1014   /**
1015    * Registers a listener that gets called back when the signed in state
1016    * is found to be changed.
1017    * @param {function()} callback Called when the answer to isSignedIn changes.
1018    */
1019   function addListener(callback) {
1020     listeners.push(callback);
1021   }
1022
1023   /**
1024    * Checks if the last signed in state matches the current one.
1025    * If it doesn't, it notifies the listeners of the change.
1026    */
1027   function checkAndNotifyListeners() {
1028     isSignedIn().then(function(signedIn) {
1029       fillFromChromeLocalStorage({lastSignedInState: undefined})
1030           .then(function(items) {
1031             if (items.lastSignedInState != signedIn) {
1032               chrome.storage.local.set(
1033                   {lastSignedInState: signedIn});
1034               listeners.forEach(function(callback) {
1035                 callback();
1036               });
1037             }
1038         });
1039       });
1040   }
1041
1042   instrumented.identity.onSignInChanged.addListener(function() {
1043     checkAndNotifyListeners();
1044   });
1045
1046   instrumented.alarms.onAlarm.addListener(function(alarm) {
1047     if (alarm.name == alarmName)
1048       checkAndNotifyListeners();
1049   });
1050
1051   // Poll for the sign in state every hour.
1052   // One hour is just an arbitrary amount of time chosen.
1053   chrome.alarms.create(alarmName, {periodInMinutes: 60});
1054
1055   return {
1056     addListener: addListener,
1057     getAuthToken: getAuthToken,
1058     isSignedIn: isSignedIn,
1059     removeToken: removeToken
1060   };
1061 }