• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
30var 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 */
38function isInDebugMode() {
39  return localStorage.debug_mode === 'true';
40}
41
42/**
43 * Initializes for debug or release modes of operation.
44 */
45function initializeDebug() {
46  if (isInDebugMode()) {
47    NOTIFICATION_CARDS_URL =
48        localStorage['server_url'] || NOTIFICATION_CARDS_URL;
49  }
50}
51
52initializeDebug();
53
54/**
55 * Conditionally allow console.log output based off of the debug mode.
56 */
57console.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 */
69if (localStorage['explanatoryCardsShown'] === undefined)
70  localStorage['explanatoryCardsShown'] = 0;
71
72/**
73 * Location Card Count Cleanup.
74 */
75if (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 */
84function 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 */
96function 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 */
108function 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 */
123function 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.
188var errorReported = false;
189
190/**
191 * Reports an error to the server and the user, as appropriate.
192 * @param {Error} error Error to report.
193 */
194function 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.
211var 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 */
223var 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 */
229var 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
438wrapper.instrumentChromeApiFunction('alarms.get', 1);
439wrapper.instrumentChromeApiFunction('alarms.onAlarm.addListener', 0);
440wrapper.instrumentChromeApiFunction('identity.getAuthToken', 1);
441wrapper.instrumentChromeApiFunction('identity.onSignInChanged.addListener', 0);
442wrapper.instrumentChromeApiFunction('identity.removeCachedAuthToken', 1);
443wrapper.instrumentChromeApiFunction('storage.local.get', 1);
444wrapper.instrumentChromeApiFunction('webstorePrivate.getBrowserLogin', 0);
445
446/**
447 * Promise adapter for all JS promises to the task manager.
448 */
449function 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
636registerPromiseAdapter();
637
638/**
639 * Control promise rejection.
640 * @enum {number}
641 */
642var 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 */
659function 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 */
695function 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 */
852function 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 */
964function 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