Upstream version 5.34.92.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / google_now / utility.js
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 'use strict';
6
7 /**
8  * @fileoverview Utility objects and functions for Google Now extension.
9  * Most important entities here:
10  * (1) 'wrapper' is a module used to add error handling and other services to
11  *     callbacks for HTML and Chrome functions and Chrome event listeners.
12  *     Chrome invokes extension code through event listeners. Once entered via
13  *     an event listener, the extension may call a Chrome/HTML API method
14  *     passing a callback (and so forth), and that callback must occur later,
15  *     otherwise, we generate an error. Chrome may unload event pages waiting
16  *     for an event. When the event fires, Chrome will reload the event page. We
17  *     don't require event listeners to fire because they are generally not
18  *     predictable (like a 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 @' + 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
406 wrapper.instrumentChromeApiFunction('alarms.get', 1);
407 wrapper.instrumentChromeApiFunction('alarms.onAlarm.addListener', 0);
408 wrapper.instrumentChromeApiFunction('identity.getAuthToken', 1);
409 wrapper.instrumentChromeApiFunction('identity.onSignInChanged.addListener', 0);
410 wrapper.instrumentChromeApiFunction('identity.removeCachedAuthToken', 1);
411 wrapper.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  */
420 function 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  */
577 function 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  */
687 function 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 }