Home | History | Annotate | Download | only in google_now
      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 }
   1062