- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / google_now / utility.js
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 'use strict';
6
7 /**
8  * @fileoverview Utility objects and functions for Google Now extension.
9  * Most important entities here:
10  * (1) 'wrapper' is a module used to add error handling and other services to
11  *     callbacks for HTML and Chrome functions and Chrome event listeners.
12  *     Chrome invokes extension code through event listeners. Once entered via
13  *     an event listener, the extension may call a Chrome/HTML API method
14  *     passing a callback (and so forth), and that callback must occur later,
15  *     otherwise, we generate an error. Chrome may unload event pages waiting
16  *     for an event. When the event fires, Chrome will reload the event page. We
17  *     don't require event listeners to fire because they are generally not
18  *     predictable (like a 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  */
30 var NOTIFICATION_CARDS_URL = 'https://www.googleapis.com/chromenow/v1';
31
32 var DEBUG_MODE = localStorage['debug_mode'];
33
34 /**
35  * Initializes for debug or release modes of operation.
36  */
37 function initializeDebug() {
38   if (DEBUG_MODE) {
39     NOTIFICATION_CARDS_URL =
40         localStorage['server_url'] || NOTIFICATION_CARDS_URL;
41   }
42 }
43
44 initializeDebug();
45
46 /**
47  * Location Card Storage.
48  */
49 if (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  */
58 function 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  */
70 function 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  */
82 function 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  */
97 function 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.
160 var errorReported = false;
161
162 /**
163  * Reports an error to the server and the user, as appropriate.
164  * @param {Error} error Error to report.
165  */
166 function 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.
181 var 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  */
193 var 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  */
199 var 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 = ' + JSON.stringify(pendingCallbacks);
255   }
256
257   /**
258    * Checks that we run in a wrapped callback.
259    */
260   function checkInWrappedCallback() {
261     if (!isInWrappedCallback) {
262       reportError(buildErrorWithMessageForServer(
263           'Not in instrumented callback'));
264     }
265   }
266
267   /**
268    * Adds error processing to an API callback.
269    * @param {Function} callback Callback to instrument.
270    * @param {boolean=} opt_isEventListener True if the callback is a listener to
271    *     a Chrome API event.
272    * @return {Function} Instrumented callback.
273    */
274   function wrapCallback(callback, opt_isEventListener) {
275     var callbackId = nextCallbackId++;
276
277     if (!opt_isEventListener) {
278       checkInWrappedCallback();
279       pendingCallbacks[callbackId] = new Error().stack;
280     }
281
282     // wrapperPluginFactory may be null before task manager is built, and in
283     // tests.
284     var wrapperPluginInstance = wrapperPluginFactory && wrapperPluginFactory();
285
286     return function() {
287       // This is the wrapper for the callback.
288       try {
289         verify(!isInWrappedCallback, 'Re-entering instrumented callback');
290         isInWrappedCallback = true;
291
292         if (!opt_isEventListener)
293           delete pendingCallbacks[callbackId];
294
295         if (wrapperPluginInstance)
296           wrapperPluginInstance.prologue();
297
298         // Call the original callback.
299         callback.apply(null, arguments);
300
301         if (wrapperPluginInstance)
302           wrapperPluginInstance.epilogue();
303
304         verify(isInWrappedCallback,
305                'Instrumented callback is not instrumented upon exit');
306         isInWrappedCallback = false;
307       } catch (error) {
308         reportError(error);
309       }
310     };
311   }
312
313   /**
314    * Returns an instrumented function.
315    * @param {!Array.<string>} functionIdentifierParts Path to the chrome.*
316    *     function.
317    * @param {string} functionName Name of the chrome API function.
318    * @param {number} callbackParameter Index of the callback parameter to this
319    *     API function.
320    * @return {Function} An instrumented function.
321    */
322   function createInstrumentedFunction(
323       functionIdentifierParts,
324       functionName,
325       callbackParameter) {
326     return function() {
327       // This is the wrapper for the API function. Pass the wrapped callback to
328       // the original function.
329       var callback = arguments[callbackParameter];
330       if (typeof callback != 'function') {
331         reportError(buildErrorWithMessageForServer(
332             'Argument ' + callbackParameter + ' of ' +
333             functionIdentifierParts.join('.') + '.' + functionName +
334             ' is not a function'));
335       }
336       arguments[callbackParameter] = wrapCallback(
337           callback, functionName == 'addListener');
338
339       var chromeContainer = chrome;
340       functionIdentifierParts.forEach(function(fragment) {
341         chromeContainer = chromeContainer[fragment];
342       });
343       return chromeContainer[functionName].
344           apply(chromeContainer, arguments);
345     };
346   }
347
348   /**
349    * Instruments an API function to add error processing to its user
350    * code-provided callback.
351    * @param {string} functionIdentifier Full identifier of the function without
352    *     the 'chrome.' portion.
353    * @param {number} callbackParameter Index of the callback parameter to this
354    *     API function.
355    */
356   function instrumentChromeApiFunction(functionIdentifier, callbackParameter) {
357     var functionIdentifierParts = functionIdentifier.split('.');
358     var functionName = functionIdentifierParts.pop();
359     var chromeContainer = chrome;
360     var instrumentedContainer = instrumented;
361     functionIdentifierParts.forEach(function(fragment) {
362       chromeContainer = chromeContainer[fragment];
363       if (!chromeContainer) {
364         reportError(buildErrorWithMessageForServer(
365             'Cannot instrument ' + functionIdentifier));
366       }
367
368       if (!(fragment in instrumentedContainer))
369         instrumentedContainer[fragment] = {};
370
371       instrumentedContainer = instrumentedContainer[fragment];
372     });
373
374     var targetFunction = chromeContainer[functionName];
375     if (!targetFunction) {
376       reportError(buildErrorWithMessageForServer(
377           'Cannot instrument ' + functionIdentifier));
378     }
379
380     instrumentedContainer[functionName] = createInstrumentedFunction(
381         functionIdentifierParts,
382         functionName,
383         callbackParameter);
384   }
385
386   instrumentChromeApiFunction('runtime.onSuspend.addListener', 0);
387
388   instrumented.runtime.onSuspend.addListener(function() {
389     var stringifiedPendingCallbacks = JSON.stringify(pendingCallbacks);
390     verify(
391         stringifiedPendingCallbacks == '{}',
392         'Pending callbacks when unloading event page:' +
393         stringifiedPendingCallbacks);
394   });
395
396   return {
397     wrapCallback: wrapCallback,
398     instrumentChromeApiFunction: instrumentChromeApiFunction,
399     registerWrapperPluginFactory: registerWrapperPluginFactory,
400     checkInWrappedCallback: checkInWrappedCallback,
401     debugGetStateString: debugGetStateString
402   };
403 })();
404
405 wrapper.instrumentChromeApiFunction('alarms.get', 1);
406 wrapper.instrumentChromeApiFunction('alarms.onAlarm.addListener', 0);
407 wrapper.instrumentChromeApiFunction('identity.getAuthToken', 1);
408 wrapper.instrumentChromeApiFunction('identity.onSignInChanged.addListener', 0);
409 wrapper.instrumentChromeApiFunction('identity.removeCachedAuthToken', 1);
410 wrapper.instrumentChromeApiFunction('webstorePrivate.getBrowserLogin', 0);
411
412 /**
413  * Builds the object to manage tasks (mutually exclusive chains of events).
414  * @param {function(string, string): boolean} areConflicting Function that
415  *     checks if a new task can't be added to a task queue that contains an
416  *     existing task.
417  * @return {Object} Task manager interface.
418  */
419 function buildTaskManager(areConflicting) {
420   /**
421    * Queue of scheduled tasks. The first element, if present, corresponds to the
422    * currently running task.
423    * @type {Array.<Object.<string, function()>>}
424    */
425   var queue = [];
426
427   /**
428    * Count of unfinished callbacks of the current task.
429    * @type {number}
430    */
431   var taskPendingCallbackCount = 0;
432
433   /**
434    * True if currently executed code is a part of a task.
435    * @type {boolean}
436    */
437   var isInTask = false;
438
439   /**
440    * Starts the first queued task.
441    */
442   function startFirst() {
443     verify(queue.length >= 1, 'startFirst: queue is empty');
444     verify(!isInTask, 'startFirst: already in task');
445     isInTask = true;
446
447     // Start the oldest queued task, but don't remove it from the queue.
448     verify(
449         taskPendingCallbackCount == 0,
450         'tasks.startFirst: still have pending task callbacks: ' +
451         taskPendingCallbackCount +
452         ', queue = ' + JSON.stringify(queue) + ', ' +
453         wrapper.debugGetStateString());
454     var entry = queue[0];
455     console.log('Starting task ' + entry.name);
456
457     entry.task();
458
459     verify(isInTask, 'startFirst: not in task at exit');
460     isInTask = false;
461     if (taskPendingCallbackCount == 0)
462       finish();
463   }
464
465   /**
466    * Checks if a new task can be added to the task queue.
467    * @param {string} taskName Name of the new task.
468    * @return {boolean} Whether the new task can be added.
469    */
470   function canQueue(taskName) {
471     for (var i = 0; i < queue.length; ++i) {
472       if (areConflicting(taskName, queue[i].name)) {
473         console.log('Conflict: new=' + taskName +
474                     ', scheduled=' + queue[i].name);
475         return false;
476       }
477     }
478
479     return true;
480   }
481
482   /**
483    * Adds a new task. If another task is not running, runs the task immediately.
484    * If any task in the queue is not compatible with the task, ignores the new
485    * task. Otherwise, stores the task for future execution.
486    * @param {string} taskName Name of the task.
487    * @param {function()} task Function to run.
488    */
489   function add(taskName, task) {
490     wrapper.checkInWrappedCallback();
491     console.log('Adding task ' + taskName);
492     if (!canQueue(taskName))
493       return;
494
495     queue.push({name: taskName, task: task});
496
497     if (queue.length == 1) {
498       startFirst();
499     }
500   }
501
502   /**
503    * Completes the current task and starts the next queued task if available.
504    */
505   function finish() {
506     verify(queue.length >= 1,
507            'tasks.finish: The task queue is empty');
508     console.log('Finishing task ' + queue[0].name);
509     queue.shift();
510
511     if (queue.length >= 1)
512       startFirst();
513   }
514
515   instrumented.runtime.onSuspend.addListener(function() {
516     verify(
517         queue.length == 0,
518         'Incomplete task when unloading event page,' +
519         ' queue = ' + JSON.stringify(queue) + ', ' +
520         wrapper.debugGetStateString());
521   });
522
523
524   /**
525    * Wrapper plugin for tasks.
526    * @constructor
527    */
528   function TasksWrapperPlugin() {
529     this.isTaskCallback = isInTask;
530     if (this.isTaskCallback)
531       ++taskPendingCallbackCount;
532   }
533
534   TasksWrapperPlugin.prototype = {
535     /**
536      * Plugin code to be executed before invoking the original callback.
537      */
538     prologue: function() {
539       if (this.isTaskCallback) {
540         verify(!isInTask, 'TasksWrapperPlugin.prologue: already in task');
541         isInTask = true;
542       }
543     },
544
545     /**
546      * Plugin code to be executed after invoking the original callback.
547      */
548     epilogue: function() {
549       if (this.isTaskCallback) {
550         verify(isInTask, 'TasksWrapperPlugin.epilogue: not in task at exit');
551         isInTask = false;
552         if (--taskPendingCallbackCount == 0)
553           finish();
554       }
555     }
556   };
557
558   wrapper.registerWrapperPluginFactory(function() {
559     return new TasksWrapperPlugin();
560   });
561
562   return {
563     add: add
564   };
565 }
566
567 /**
568  * Builds an object to manage retrying activities with exponential backoff.
569  * @param {string} name Name of this attempt manager.
570  * @param {function()} attempt Activity that the manager retries until it
571  *     calls 'stop' method.
572  * @param {number} initialDelaySeconds Default first delay until first retry.
573  * @param {number} maximumDelaySeconds Maximum delay between retries.
574  * @return {Object} Attempt manager interface.
575  */
576 function buildAttemptManager(
577     name, attempt, initialDelaySeconds, maximumDelaySeconds) {
578   var alarmName = 'attempt-scheduler-' + name;
579   var currentDelayStorageKey = 'current-delay-' + name;
580
581   /**
582    * Creates an alarm for the next attempt. The alarm is repeating for the case
583    * when the next attempt crashes before registering next alarm.
584    * @param {number} delaySeconds Delay until next retry.
585    */
586   function createAlarm(delaySeconds) {
587     var alarmInfo = {
588       delayInMinutes: delaySeconds / 60,
589       periodInMinutes: maximumDelaySeconds / 60
590     };
591     chrome.alarms.create(alarmName, alarmInfo);
592   }
593
594   /**
595    * Indicates if this attempt manager has started.
596    * @param {function(boolean)} callback The function's boolean parameter is
597    *     true if the attempt manager has started, false otherwise.
598    */
599   function isRunning(callback) {
600     instrumented.alarms.get(alarmName, function(alarmInfo) {
601       callback(!!alarmInfo);
602     });
603   }
604
605   /**
606    * Schedules next attempt.
607    * @param {number=} opt_previousDelaySeconds Previous delay in a sequence of
608    *     retry attempts, if specified. Not specified for scheduling first retry
609    *     in the exponential sequence.
610    */
611   function scheduleNextAttempt(opt_previousDelaySeconds) {
612     var base = opt_previousDelaySeconds ? opt_previousDelaySeconds * 2 :
613                                           initialDelaySeconds;
614     var newRetryDelaySeconds =
615         Math.min(base * (1 + 0.2 * Math.random()), maximumDelaySeconds);
616
617     createAlarm(newRetryDelaySeconds);
618
619     var items = {};
620     items[currentDelayStorageKey] = newRetryDelaySeconds;
621     chrome.storage.local.set(items);
622   }
623
624   /**
625    * Starts repeated attempts.
626    * @param {number=} opt_firstDelaySeconds Time until the first attempt, if
627    *     specified. Otherwise, initialDelaySeconds will be used for the first
628    *     attempt.
629    */
630   function start(opt_firstDelaySeconds) {
631     if (opt_firstDelaySeconds) {
632       createAlarm(opt_firstDelaySeconds);
633       chrome.storage.local.remove(currentDelayStorageKey);
634     } else {
635       scheduleNextAttempt();
636     }
637   }
638
639   /**
640    * Stops repeated attempts.
641    */
642   function stop() {
643     chrome.alarms.clear(alarmName);
644     chrome.storage.local.remove(currentDelayStorageKey);
645   }
646
647   /**
648    * Plans for the next attempt.
649    * @param {function()} callback Completion callback. It will be invoked after
650    *     the planning is done.
651    */
652   function planForNext(callback) {
653     instrumented.storage.local.get(currentDelayStorageKey, function(items) {
654       if (!items) {
655         items = {};
656         items[currentDelayStorageKey] = maximumDelaySeconds;
657       }
658       console.log('planForNext-get-storage ' + JSON.stringify(items));
659       scheduleNextAttempt(items[currentDelayStorageKey]);
660       callback();
661     });
662   }
663
664   instrumented.alarms.onAlarm.addListener(function(alarm) {
665     if (alarm.name == alarmName)
666       isRunning(function(running) {
667         if (running)
668           attempt();
669       });
670   });
671
672   return {
673     start: start,
674     planForNext: planForNext,
675     stop: stop,
676     isRunning: isRunning
677   };
678 }
679
680 // TODO(robliao): Use signed-in state change watch API when it's available.
681 /**
682  * Wraps chrome.identity to provide limited listening support for
683  * the sign in state by polling periodically for the auth token.
684  * @return {Object} The Authentication Manager interface.
685  */
686 function buildAuthenticationManager() {
687   var alarmName = 'sign-in-alarm';
688
689   /**
690    * Gets an OAuth2 access token.
691    * @param {function(string=)} callback Called on completion.
692    *     The string contains the token. It's undefined if there was an error.
693    */
694   function getAuthToken(callback) {
695     instrumented.identity.getAuthToken({interactive: false}, function(token) {
696       token = chrome.runtime.lastError ? undefined : token;
697       callback(token);
698     });
699   }
700
701   /**
702    * Determines whether there is an account attached to the profile.
703    * @param {function(boolean)} callback Called on completion.
704    */
705   function isSignedIn(callback) {
706     instrumented.webstorePrivate.getBrowserLogin(function(accountInfo) {
707       callback(!!accountInfo.login);
708     });
709   }
710
711   /**
712    * Removes the specified cached token.
713    * @param {string} token Authentication Token to remove from the cache.
714    * @param {function} callback Called on completion.
715    */
716   function removeToken(token, callback) {
717     instrumented.identity.removeCachedAuthToken({token: token}, function() {
718       // Let Chrome now about a possible problem with the token.
719       getAuthToken(function() {});
720       callback();
721     });
722   }
723
724   var listeners = [];
725
726   /**
727    * Registers a listener that gets called back when the signed in state
728    * is found to be changed.
729    * @param {function} callback Called when the answer to isSignedIn changes.
730    */
731   function addListener(callback) {
732     listeners.push(callback);
733   }
734
735   /**
736    * Checks if the last signed in state matches the current one.
737    * If it doesn't, it notifies the listeners of the change.
738    */
739   function checkAndNotifyListeners() {
740     isSignedIn(function(signedIn) {
741       instrumented.storage.local.get('lastSignedInState', function(items) {
742         items = items || {};
743         if (items.lastSignedInState != signedIn) {
744           chrome.storage.local.set(
745               {lastSignedInState: signedIn});
746           if (items.lastSignedInState != undefined) {
747             listeners.forEach(function(callback) {
748               callback();
749             });
750           }
751         }
752       });
753     });
754   }
755
756   instrumented.identity.onSignInChanged.addListener(function() {
757     checkAndNotifyListeners();
758   });
759
760   instrumented.alarms.onAlarm.addListener(function(alarm) {
761     if (alarm.name == alarmName)
762       checkAndNotifyListeners();
763   });
764
765   // Poll for the sign in state every hour.
766   // One hour is just an arbitrary amount of time chosen.
767   chrome.alarms.create(alarmName, {periodInMinutes: 60});
768
769   return {
770     addListener: addListener,
771     getAuthToken: getAuthToken,
772     isSignedIn: isSignedIn,
773     removeToken: removeToken
774   };
775 }