• 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 location change 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
32var DEBUG_MODE = localStorage['debug_mode'];
33
34/**
35 * Initializes for debug or release modes of operation.
36 */
37function initializeDebug() {
38  if (DEBUG_MODE) {
39    NOTIFICATION_CARDS_URL =
40        localStorage['server_url'] || NOTIFICATION_CARDS_URL;
41  }
42}
43
44initializeDebug();
45
46/**
47 * Location Card Storage.
48 */
49if (localStorage['locationCardsShown'] === undefined)
50  localStorage['locationCardsShown'] = 0;
51
52/**
53 * Builds an error object with a message that may be sent to the server.
54 * @param {string} message Error message. This message may be sent to the
55 *     server.
56 * @return {Error} Error object.
57 */
58function buildErrorWithMessageForServer(message) {
59  var error = new Error(message);
60  error.canSendMessageToServer = true;
61  return error;
62}
63
64/**
65 * Checks for internal errors.
66 * @param {boolean} condition Condition that must be true.
67 * @param {string} message Diagnostic message for the case when the condition is
68 *     false.
69 */
70function verify(condition, message) {
71  if (!condition)
72    throw buildErrorWithMessageForServer('ASSERT: ' + message);
73}
74
75/**
76 * Builds a request to the notification server.
77 * @param {string} method Request method.
78 * @param {string} handlerName Server handler to send the request to.
79 * @param {string=} contentType Value for the Content-type header.
80 * @return {XMLHttpRequest} Server request.
81 */
82function buildServerRequest(method, handlerName, contentType) {
83  var request = new XMLHttpRequest();
84
85  request.responseType = 'text';
86  request.open(method, NOTIFICATION_CARDS_URL + '/' + handlerName, true);
87  if (contentType)
88    request.setRequestHeader('Content-type', contentType);
89
90  return request;
91}
92
93/**
94 * Sends an error report to the server.
95 * @param {Error} error Error to send.
96 */
97function sendErrorReport(error) {
98  // Don't remove 'error.stack.replace' below!
99  var filteredStack = error.canSendMessageToServer ?
100      error.stack : error.stack.replace(/.*\n/, '(message removed)\n');
101  var file;
102  var line;
103  var topFrameLineMatch = filteredStack.match(/\n    at .*\n/);
104  var topFrame = topFrameLineMatch && topFrameLineMatch[0];
105  if (topFrame) {
106    // Examples of a frame:
107    // 1. '\n    at someFunction (chrome-extension://
108    //     pafkbggdmjlpgkdkcbjmhmfcdpncadgh/background.js:915:15)\n'
109    // 2. '\n    at chrome-extension://pafkbggdmjlpgkdkcbjmhmfcdpncadgh/
110    //     utility.js:269:18\n'
111    // 3. '\n    at Function.target.(anonymous function) (extensions::
112    //     SafeBuiltins:19:14)\n'
113    // 4. '\n    at Event.dispatchToListener (event_bindings:382:22)\n'
114    var errorLocation;
115    // Find the the parentheses at the end of the line, if any.
116    var parenthesesMatch = topFrame.match(/\(.*\)\n/);
117    if (parenthesesMatch && parenthesesMatch[0]) {
118      errorLocation =
119          parenthesesMatch[0].substring(1, parenthesesMatch[0].length - 2);
120    } else {
121      errorLocation = topFrame;
122    }
123
124    var topFrameElements = errorLocation.split(':');
125    // topFrameElements is an array that ends like:
126    // [N-3] //pafkbggdmjlpgkdkcbjmhmfcdpncadgh/utility.js
127    // [N-2] 308
128    // [N-1] 19
129    if (topFrameElements.length >= 3) {
130      file = topFrameElements[topFrameElements.length - 3];
131      line = topFrameElements[topFrameElements.length - 2];
132    }
133  }
134
135  var errorText = error.name;
136  if (error.canSendMessageToServer)
137    errorText = errorText + ': ' + error.message;
138
139  var errorObject = {
140    message: errorText,
141    file: file,
142    line: line,
143    trace: filteredStack
144  };
145
146  var request = buildServerRequest('POST', 'jserrors', 'application/json');
147  request.onloadend = function(event) {
148    console.log('sendErrorReport status: ' + request.status);
149  };
150
151  chrome.identity.getAuthToken({interactive: false}, function(token) {
152    if (token) {
153      request.setRequestHeader('Authorization', 'Bearer ' + token);
154      request.send(JSON.stringify(errorObject));
155    }
156  });
157}
158
159// Limiting 1 error report per background page load.
160var errorReported = false;
161
162/**
163 * Reports an error to the server and the user, as appropriate.
164 * @param {Error} error Error to report.
165 */
166function reportError(error) {
167  var message = 'Critical error:\n' + error.stack;
168  console.error(message);
169  if (!errorReported) {
170    errorReported = true;
171    chrome.metricsPrivate.getIsCrashReportingEnabled(function(isEnabled) {
172      if (isEnabled)
173        sendErrorReport(error);
174      if (DEBUG_MODE)
175        alert(message);
176    });
177  }
178}
179
180// Partial mirror of chrome.* for all instrumented functions.
181var instrumented = {};
182
183/**
184 * Wrapper plugin. These plugins extend instrumentation added by
185 * wrapper.wrapCallback by adding code that executes before and after the call
186 * to the original callback provided by the extension.
187 *
188 * @typedef {{
189 *   prologue: function (),
190 *   epilogue: function ()
191 * }}
192 */
193var WrapperPlugin;
194
195/**
196 * Wrapper for callbacks. Used to add error handling and other services to
197 * callbacks for HTML and Chrome functions and events.
198 */
199var wrapper = (function() {
200  /**
201   * Factory for wrapper plugins. If specified, it's used to generate an
202   * instance of WrapperPlugin each time we wrap a callback (which corresponds
203   * to addListener call for Chrome events, and to every API call that specifies
204   * a callback). WrapperPlugin's lifetime ends when the callback for which it
205   * was generated, exits. It's possible to have several instances of
206   * WrapperPlugin at the same time.
207   * An instance of WrapperPlugin can have state that can be shared by its
208   * constructor, prologue() and epilogue(). Also WrapperPlugins can change
209   * state of other objects, for example, to do refcounting.
210   * @type {?function(): WrapperPlugin}
211   */
212  var wrapperPluginFactory = null;
213
214  /**
215   * Registers a wrapper plugin factory.
216   * @param {function(): WrapperPlugin} factory Wrapper plugin factory.
217   */
218  function registerWrapperPluginFactory(factory) {
219    if (wrapperPluginFactory) {
220      reportError(buildErrorWithMessageForServer(
221          'registerWrapperPluginFactory: factory is already registered.'));
222    }
223
224    wrapperPluginFactory = factory;
225  }
226
227  /**
228   * True if currently executed code runs in a callback or event handler that
229   * was instrumented by wrapper.wrapCallback() call.
230   * @type {boolean}
231   */
232  var isInWrappedCallback = false;
233
234  /**
235   * Required callbacks that are not yet called. Includes both task and non-task
236   * callbacks. This is a map from unique callback id to the stack at the moment
237   * when the callback was wrapped. This stack identifies the callback.
238   * Used only for diagnostics.
239   * @type {Object.<number, string>}
240   */
241  var pendingCallbacks = {};
242
243  /**
244   * Unique ID of the next callback.
245   * @type {number}
246   */
247  var nextCallbackId = 0;
248
249  /**
250   * Gets diagnostic string with the status of the wrapper.
251   * @return {string} Diagnostic string.
252   */
253  function debugGetStateString() {
254    return 'pendingCallbacks @' + Date.now() + ' = ' +
255        JSON.stringify(pendingCallbacks);
256  }
257
258  /**
259   * Checks that we run in a wrapped callback.
260   */
261  function checkInWrappedCallback() {
262    if (!isInWrappedCallback) {
263      reportError(buildErrorWithMessageForServer(
264          'Not in instrumented callback'));
265    }
266  }
267
268  /**
269   * Adds error processing to an API callback.
270   * @param {Function} callback Callback to instrument.
271   * @param {boolean=} opt_isEventListener True if the callback is a listener to
272   *     a Chrome API event.
273   * @return {Function} Instrumented callback.
274   */
275  function wrapCallback(callback, opt_isEventListener) {
276    var callbackId = nextCallbackId++;
277
278    if (!opt_isEventListener) {
279      checkInWrappedCallback();
280      pendingCallbacks[callbackId] = new Error().stack + ' @' + Date.now();
281    }
282
283    // wrapperPluginFactory may be null before task manager is built, and in
284    // tests.
285    var wrapperPluginInstance = wrapperPluginFactory && wrapperPluginFactory();
286
287    return function() {
288      // This is the wrapper for the callback.
289      try {
290        verify(!isInWrappedCallback, 'Re-entering instrumented callback');
291        isInWrappedCallback = true;
292
293        if (!opt_isEventListener)
294          delete pendingCallbacks[callbackId];
295
296        if (wrapperPluginInstance)
297          wrapperPluginInstance.prologue();
298
299        // Call the original callback.
300        callback.apply(null, arguments);
301
302        if (wrapperPluginInstance)
303          wrapperPluginInstance.epilogue();
304
305        verify(isInWrappedCallback,
306               'Instrumented callback is not instrumented upon exit');
307        isInWrappedCallback = false;
308      } catch (error) {
309        reportError(error);
310      }
311    };
312  }
313
314  /**
315   * Returns an instrumented function.
316   * @param {!Array.<string>} functionIdentifierParts Path to the chrome.*
317   *     function.
318   * @param {string} functionName Name of the chrome API function.
319   * @param {number} callbackParameter Index of the callback parameter to this
320   *     API function.
321   * @return {Function} An instrumented function.
322   */
323  function createInstrumentedFunction(
324      functionIdentifierParts,
325      functionName,
326      callbackParameter) {
327    return function() {
328      // This is the wrapper for the API function. Pass the wrapped callback to
329      // the original function.
330      var callback = arguments[callbackParameter];
331      if (typeof callback != 'function') {
332        reportError(buildErrorWithMessageForServer(
333            'Argument ' + callbackParameter + ' of ' +
334            functionIdentifierParts.join('.') + '.' + functionName +
335            ' is not a function'));
336      }
337      arguments[callbackParameter] = wrapCallback(
338          callback, functionName == 'addListener');
339
340      var chromeContainer = chrome;
341      functionIdentifierParts.forEach(function(fragment) {
342        chromeContainer = chromeContainer[fragment];
343      });
344      return chromeContainer[functionName].
345          apply(chromeContainer, arguments);
346    };
347  }
348
349  /**
350   * Instruments an API function to add error processing to its user
351   * code-provided callback.
352   * @param {string} functionIdentifier Full identifier of the function without
353   *     the 'chrome.' portion.
354   * @param {number} callbackParameter Index of the callback parameter to this
355   *     API function.
356   */
357  function instrumentChromeApiFunction(functionIdentifier, callbackParameter) {
358    var functionIdentifierParts = functionIdentifier.split('.');
359    var functionName = functionIdentifierParts.pop();
360    var chromeContainer = chrome;
361    var instrumentedContainer = instrumented;
362    functionIdentifierParts.forEach(function(fragment) {
363      chromeContainer = chromeContainer[fragment];
364      if (!chromeContainer) {
365        reportError(buildErrorWithMessageForServer(
366            'Cannot instrument ' + functionIdentifier));
367      }
368
369      if (!(fragment in instrumentedContainer))
370        instrumentedContainer[fragment] = {};
371
372      instrumentedContainer = instrumentedContainer[fragment];
373    });
374
375    var targetFunction = chromeContainer[functionName];
376    if (!targetFunction) {
377      reportError(buildErrorWithMessageForServer(
378          'Cannot instrument ' + functionIdentifier));
379    }
380
381    instrumentedContainer[functionName] = createInstrumentedFunction(
382        functionIdentifierParts,
383        functionName,
384        callbackParameter);
385  }
386
387  instrumentChromeApiFunction('runtime.onSuspend.addListener', 0);
388
389  instrumented.runtime.onSuspend.addListener(function() {
390    var stringifiedPendingCallbacks = JSON.stringify(pendingCallbacks);
391    verify(
392        stringifiedPendingCallbacks == '{}',
393        'Pending callbacks when unloading event page @' + Date.now() + ':' +
394        stringifiedPendingCallbacks);
395  });
396
397  return {
398    wrapCallback: wrapCallback,
399    instrumentChromeApiFunction: instrumentChromeApiFunction,
400    registerWrapperPluginFactory: registerWrapperPluginFactory,
401    checkInWrappedCallback: checkInWrappedCallback,
402    debugGetStateString: debugGetStateString
403  };
404})();
405
406wrapper.instrumentChromeApiFunction('alarms.get', 1);
407wrapper.instrumentChromeApiFunction('alarms.onAlarm.addListener', 0);
408wrapper.instrumentChromeApiFunction('identity.getAuthToken', 1);
409wrapper.instrumentChromeApiFunction('identity.onSignInChanged.addListener', 0);
410wrapper.instrumentChromeApiFunction('identity.removeCachedAuthToken', 1);
411wrapper.instrumentChromeApiFunction('webstorePrivate.getBrowserLogin', 0);
412
413/**
414 * Builds the object to manage tasks (mutually exclusive chains of events).
415 * @param {function(string, string): boolean} areConflicting Function that
416 *     checks if a new task can't be added to a task queue that contains an
417 *     existing task.
418 * @return {Object} Task manager interface.
419 */
420function buildTaskManager(areConflicting) {
421  /**
422   * Queue of scheduled tasks. The first element, if present, corresponds to the
423   * currently running task.
424   * @type {Array.<Object.<string, function()>>}
425   */
426  var queue = [];
427
428  /**
429   * Count of unfinished callbacks of the current task.
430   * @type {number}
431   */
432  var taskPendingCallbackCount = 0;
433
434  /**
435   * True if currently executed code is a part of a task.
436   * @type {boolean}
437   */
438  var isInTask = false;
439
440  /**
441   * Starts the first queued task.
442   */
443  function startFirst() {
444    verify(queue.length >= 1, 'startFirst: queue is empty');
445    verify(!isInTask, 'startFirst: already in task');
446    isInTask = true;
447
448    // Start the oldest queued task, but don't remove it from the queue.
449    verify(
450        taskPendingCallbackCount == 0,
451        'tasks.startFirst: still have pending task callbacks: ' +
452        taskPendingCallbackCount +
453        ', queue = ' + JSON.stringify(queue) + ', ' +
454        wrapper.debugGetStateString());
455    var entry = queue[0];
456    console.log('Starting task ' + entry.name);
457
458    entry.task();
459
460    verify(isInTask, 'startFirst: not in task at exit');
461    isInTask = false;
462    if (taskPendingCallbackCount == 0)
463      finish();
464  }
465
466  /**
467   * Checks if a new task can be added to the task queue.
468   * @param {string} taskName Name of the new task.
469   * @return {boolean} Whether the new task can be added.
470   */
471  function canQueue(taskName) {
472    for (var i = 0; i < queue.length; ++i) {
473      if (areConflicting(taskName, queue[i].name)) {
474        console.log('Conflict: new=' + taskName +
475                    ', scheduled=' + queue[i].name);
476        return false;
477      }
478    }
479
480    return true;
481  }
482
483  /**
484   * Adds a new task. If another task is not running, runs the task immediately.
485   * If any task in the queue is not compatible with the task, ignores the new
486   * task. Otherwise, stores the task for future execution.
487   * @param {string} taskName Name of the task.
488   * @param {function()} task Function to run.
489   */
490  function add(taskName, task) {
491    wrapper.checkInWrappedCallback();
492    console.log('Adding task ' + taskName);
493    if (!canQueue(taskName))
494      return;
495
496    queue.push({name: taskName, task: task});
497
498    if (queue.length == 1) {
499      startFirst();
500    }
501  }
502
503  /**
504   * Completes the current task and starts the next queued task if available.
505   */
506  function finish() {
507    verify(queue.length >= 1,
508           'tasks.finish: The task queue is empty');
509    console.log('Finishing task ' + queue[0].name);
510    queue.shift();
511
512    if (queue.length >= 1)
513      startFirst();
514  }
515
516  instrumented.runtime.onSuspend.addListener(function() {
517    verify(
518        queue.length == 0,
519        'Incomplete task when unloading event page,' +
520        ' queue = ' + JSON.stringify(queue) + ', ' +
521        wrapper.debugGetStateString());
522  });
523
524
525  /**
526   * Wrapper plugin for tasks.
527   * @constructor
528   */
529  function TasksWrapperPlugin() {
530    this.isTaskCallback = isInTask;
531    if (this.isTaskCallback)
532      ++taskPendingCallbackCount;
533  }
534
535  TasksWrapperPlugin.prototype = {
536    /**
537     * Plugin code to be executed before invoking the original callback.
538     */
539    prologue: function() {
540      if (this.isTaskCallback) {
541        verify(!isInTask, 'TasksWrapperPlugin.prologue: already in task');
542        isInTask = true;
543      }
544    },
545
546    /**
547     * Plugin code to be executed after invoking the original callback.
548     */
549    epilogue: function() {
550      if (this.isTaskCallback) {
551        verify(isInTask, 'TasksWrapperPlugin.epilogue: not in task at exit');
552        isInTask = false;
553        if (--taskPendingCallbackCount == 0)
554          finish();
555      }
556    }
557  };
558
559  wrapper.registerWrapperPluginFactory(function() {
560    return new TasksWrapperPlugin();
561  });
562
563  return {
564    add: add
565  };
566}
567
568/**
569 * Builds an object to manage retrying activities with exponential backoff.
570 * @param {string} name Name of this attempt manager.
571 * @param {function()} attempt Activity that the manager retries until it
572 *     calls 'stop' method.
573 * @param {number} initialDelaySeconds Default first delay until first retry.
574 * @param {number} maximumDelaySeconds Maximum delay between retries.
575 * @return {Object} Attempt manager interface.
576 */
577function buildAttemptManager(
578    name, attempt, initialDelaySeconds, maximumDelaySeconds) {
579  var alarmName = 'attempt-scheduler-' + name;
580  var currentDelayStorageKey = 'current-delay-' + name;
581
582  /**
583   * Creates an alarm for the next attempt. The alarm is repeating for the case
584   * when the next attempt crashes before registering next alarm.
585   * @param {number} delaySeconds Delay until next retry.
586   */
587  function createAlarm(delaySeconds) {
588    var alarmInfo = {
589      delayInMinutes: delaySeconds / 60,
590      periodInMinutes: maximumDelaySeconds / 60
591    };
592    chrome.alarms.create(alarmName, alarmInfo);
593  }
594
595  /**
596   * Indicates if this attempt manager has started.
597   * @param {function(boolean)} callback The function's boolean parameter is
598   *     true if the attempt manager has started, false otherwise.
599   */
600  function isRunning(callback) {
601    instrumented.alarms.get(alarmName, function(alarmInfo) {
602      callback(!!alarmInfo);
603    });
604  }
605
606  /**
607   * Schedules next attempt.
608   * @param {number=} opt_previousDelaySeconds Previous delay in a sequence of
609   *     retry attempts, if specified. Not specified for scheduling first retry
610   *     in the exponential sequence.
611   */
612  function scheduleNextAttempt(opt_previousDelaySeconds) {
613    var base = opt_previousDelaySeconds ? opt_previousDelaySeconds * 2 :
614                                          initialDelaySeconds;
615    var newRetryDelaySeconds =
616        Math.min(base * (1 + 0.2 * Math.random()), maximumDelaySeconds);
617
618    createAlarm(newRetryDelaySeconds);
619
620    var items = {};
621    items[currentDelayStorageKey] = newRetryDelaySeconds;
622    chrome.storage.local.set(items);
623  }
624
625  /**
626   * Starts repeated attempts.
627   * @param {number=} opt_firstDelaySeconds Time until the first attempt, if
628   *     specified. Otherwise, initialDelaySeconds will be used for the first
629   *     attempt.
630   */
631  function start(opt_firstDelaySeconds) {
632    if (opt_firstDelaySeconds) {
633      createAlarm(opt_firstDelaySeconds);
634      chrome.storage.local.remove(currentDelayStorageKey);
635    } else {
636      scheduleNextAttempt();
637    }
638  }
639
640  /**
641   * Stops repeated attempts.
642   */
643  function stop() {
644    chrome.alarms.clear(alarmName);
645    chrome.storage.local.remove(currentDelayStorageKey);
646  }
647
648  /**
649   * Plans for the next attempt.
650   * @param {function()} callback Completion callback. It will be invoked after
651   *     the planning is done.
652   */
653  function planForNext(callback) {
654    instrumented.storage.local.get(currentDelayStorageKey, function(items) {
655      if (!items) {
656        items = {};
657        items[currentDelayStorageKey] = maximumDelaySeconds;
658      }
659      console.log('planForNext-get-storage ' + JSON.stringify(items));
660      scheduleNextAttempt(items[currentDelayStorageKey]);
661      callback();
662    });
663  }
664
665  instrumented.alarms.onAlarm.addListener(function(alarm) {
666    if (alarm.name == alarmName)
667      isRunning(function(running) {
668        if (running)
669          attempt();
670      });
671  });
672
673  return {
674    start: start,
675    planForNext: planForNext,
676    stop: stop,
677    isRunning: isRunning
678  };
679}
680
681// TODO(robliao): Use signed-in state change watch API when it's available.
682/**
683 * Wraps chrome.identity to provide limited listening support for
684 * the sign in state by polling periodically for the auth token.
685 * @return {Object} The Authentication Manager interface.
686 */
687function buildAuthenticationManager() {
688  var alarmName = 'sign-in-alarm';
689
690  /**
691   * Gets an OAuth2 access token.
692   * @param {function(string=)} callback Called on completion.
693   *     The string contains the token. It's undefined if there was an error.
694   */
695  function getAuthToken(callback) {
696    instrumented.identity.getAuthToken({interactive: false}, function(token) {
697      token = chrome.runtime.lastError ? undefined : token;
698      callback(token);
699    });
700  }
701
702  /**
703   * Determines whether there is an account attached to the profile.
704   * @param {function(boolean)} callback Called on completion.
705   */
706  function isSignedIn(callback) {
707    instrumented.webstorePrivate.getBrowserLogin(function(accountInfo) {
708      callback(!!accountInfo.login);
709    });
710  }
711
712  /**
713   * Removes the specified cached token.
714   * @param {string} token Authentication Token to remove from the cache.
715   * @param {function()} callback Called on completion.
716   */
717  function removeToken(token, callback) {
718    instrumented.identity.removeCachedAuthToken({token: token}, function() {
719      // Let Chrome now about a possible problem with the token.
720      getAuthToken(function() {});
721      callback();
722    });
723  }
724
725  var listeners = [];
726
727  /**
728   * Registers a listener that gets called back when the signed in state
729   * is found to be changed.
730   * @param {function()} callback Called when the answer to isSignedIn changes.
731   */
732  function addListener(callback) {
733    listeners.push(callback);
734  }
735
736  /**
737   * Checks if the last signed in state matches the current one.
738   * If it doesn't, it notifies the listeners of the change.
739   */
740  function checkAndNotifyListeners() {
741    isSignedIn(function(signedIn) {
742      instrumented.storage.local.get('lastSignedInState', function(items) {
743        items = items || {};
744        if (items.lastSignedInState != signedIn) {
745          chrome.storage.local.set(
746              {lastSignedInState: signedIn});
747          listeners.forEach(function(callback) {
748            callback();
749          });
750        }
751      });
752    });
753  }
754
755  instrumented.identity.onSignInChanged.addListener(function() {
756    checkAndNotifyListeners();
757  });
758
759  instrumented.alarms.onAlarm.addListener(function(alarm) {
760    if (alarm.name == alarmName)
761      checkAndNotifyListeners();
762  });
763
764  // Poll for the sign in state every hour.
765  // One hour is just an arbitrary amount of time chosen.
766  chrome.alarms.create(alarmName, {periodInMinutes: 60});
767
768  return {
769    addListener: addListener,
770    getAuthToken: getAuthToken,
771    isSignedIn: isSignedIn,
772    removeToken: removeToken
773  };
774}
775