blob: 0ab7c4b7c384b80ee3d9cea4387aef77c0df4cba [file] [log] [blame]
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001// 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
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +01005'use strict';
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +01006
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00007/**
8 * @fileoverview Utility objects and functions for Google Now extension.
9 */
10
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +010011// TODO(vadimt): Figure out the server name. Use it in the manifest and for
12// NOTIFICATION_CARDS_URL. Meanwhile, to use the feature, you need to manually
13// set the server name via local storage.
Ben Murdoch32409262013-08-07 11:04:47 +010014
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +010015/**
16 * Notification server URL.
17 */
18var NOTIFICATION_CARDS_URL = localStorage['server_url'];
19
Ben Murdoch32409262013-08-07 11:04:47 +010020var DEBUG_MODE = localStorage['debug_mode'];
21
22/**
23 * Shows a message popup in debug mode.
24 * @param {string} message Diagnostic message.
25 */
26function debugAlert(message) {
27 if (DEBUG_MODE)
28 alert(message);
29}
30
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000031/**
32 * Checks for internal errors.
33 * @param {boolean} condition Condition that must be true.
34 * @param {string} message Diagnostic message for the case when the condition is
35 * false.
36 */
37function verify(condition, message) {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +010038 if (!condition)
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +010039 throw new Error('\nASSERT: ' + message);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000040}
41
42/**
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +010043 * Builds a request to the notification server.
44 * @param {string} handlerName Server handler to send the request to.
Ben Murdochca12bfa2013-07-23 11:17:05 +010045 * @param {string} contentType Value for the Content-type header.
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +010046 * @return {XMLHttpRequest} Server request.
47 */
Ben Murdochca12bfa2013-07-23 11:17:05 +010048function buildServerRequest(handlerName, contentType) {
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +010049 var request = new XMLHttpRequest();
50
51 request.responseType = 'text';
52 request.open('POST', NOTIFICATION_CARDS_URL + '/' + handlerName, true);
Ben Murdochca12bfa2013-07-23 11:17:05 +010053 request.setRequestHeader('Content-type', contentType);
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +010054
55 return request;
56}
57
58/**
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000059 * Builds the object to manage tasks (mutually exclusive chains of events).
60 * @param {function(string, string): boolean} areConflicting Function that
61 * checks if a new task can't be added to a task queue that contains an
62 * existing task.
63 * @return {Object} Task manager interface.
64 */
65function buildTaskManager(areConflicting) {
66 /**
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000067 * Queue of scheduled tasks. The first element, if present, corresponds to the
68 * currently running task.
69 * @type {Array.<Object.<string, function(function())>>}
70 */
71 var queue = [];
72
73 /**
Ben Murdoch558790d2013-07-30 15:19:42 +010074 * Count of unfinished callbacks of the current task.
75 * @type {number}
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000076 */
Ben Murdoch558790d2013-07-30 15:19:42 +010077 var taskPendingCallbackCount = 0;
78
79 /**
80 * Required callbacks that are not yet called. Includes both task and non-task
81 * callbacks. This is a map from unique callback id to the stack at the moment
82 * when the callback was wrapped. This stack identifies the callback.
83 * Used only for diagnostics.
84 * @type {Object.<number, string>}
85 */
86 var pendingCallbacks = {};
87
88 /**
89 * True if currently executed code is a part of a task.
90 * @type {boolean}
91 */
92 var isInTask = false;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000093
94 /**
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010095 * True if currently executed code runs in an instrumented callback.
96 * @type {boolean}
97 */
98 var isInInstrumentedCallback = false;
99
100 /**
101 * Checks that we run in an instrumented callback.
102 */
103 function checkInInstrumentedCallback() {
104 if (!isInInstrumentedCallback) {
105 // Cannot use verify() since no one will catch the exception.
Ben Murdoch32409262013-08-07 11:04:47 +0100106 // This check will detect bugs at the development stage, and is very
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100107 // unlikely to be seen by users.
108 var error = 'Not in instrumented callback: ' + new Error().stack;
109 console.error(error);
Ben Murdoch32409262013-08-07 11:04:47 +0100110 debugAlert(error);
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100111 }
112 }
113
114 /**
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000115 * Starts the first queued task.
116 */
117 function startFirst() {
118 verify(queue.length >= 1, 'startFirst: queue is empty');
Ben Murdoch558790d2013-07-30 15:19:42 +0100119 verify(!isInTask, 'startFirst: already in task');
120 isInTask = true;
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000121
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000122 // Start the oldest queued task, but don't remove it from the queue.
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100123 verify(
Ben Murdoch558790d2013-07-30 15:19:42 +0100124 taskPendingCallbackCount == 0,
125 'tasks.startFirst: still have pending task callbacks: ' +
126 taskPendingCallbackCount +
127 ', queue = ' + JSON.stringify(queue) +
128 ', pendingCallbacks = ' + JSON.stringify(pendingCallbacks));
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000129 var entry = queue[0];
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100130 console.log('Starting task ' + entry.name);
Ben Murdoch558790d2013-07-30 15:19:42 +0100131
132 entry.task(function() {}); // TODO(vadimt): Don't pass parameter.
133
134 verify(isInTask, 'startFirst: not in task at exit');
135 isInTask = false;
136 if (taskPendingCallbackCount == 0)
137 finish();
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000138 }
139
140 /**
141 * Checks if a new task can be added to the task queue.
142 * @param {string} taskName Name of the new task.
143 * @return {boolean} Whether the new task can be added.
144 */
145 function canQueue(taskName) {
146 for (var i = 0; i < queue.length; ++i) {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100147 if (areConflicting(taskName, queue[i].name)) {
148 console.log('Conflict: new=' + taskName +
149 ', scheduled=' + queue[i].name);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000150 return false;
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100151 }
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000152 }
153
154 return true;
155 }
156
157 /**
158 * Adds a new task. If another task is not running, runs the task immediately.
159 * If any task in the queue is not compatible with the task, ignores the new
160 * task. Otherwise, stores the task for future execution.
161 * @param {string} taskName Name of the task.
162 * @param {function(function())} task Function to run. Takes a callback
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100163 * parameter. Call this callback on completion.
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000164 */
165 function add(taskName, task) {
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100166 checkInInstrumentedCallback();
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100167 console.log('Adding task ' + taskName);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000168 if (!canQueue(taskName))
169 return;
170
171 queue.push({name: taskName, task: task});
172
173 if (queue.length == 1) {
174 startFirst();
175 }
176 }
177
178 /**
179 * Completes the current task and starts the next queued task if available.
180 */
181 function finish() {
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100182 verify(queue.length >= 1,
Ben Murdoch558790d2013-07-30 15:19:42 +0100183 'tasks.finish: The task queue is empty');
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100184 console.log('Finishing task ' + queue[0].name);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000185 queue.shift();
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000186
187 if (queue.length >= 1)
188 startFirst();
189 }
190
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100191 // Limiting 1 error report per background page load.
192 var errorReported = false;
193
194 /**
195 * Sends an error report to the server.
196 * @param {Error} error Error to report.
197 */
198 function sendErrorReport(error) {
Ben Murdocheb525c52013-07-10 11:40:50 +0100199 var filteredStack = error.stack.replace(/.*\n/, '\n');
200 var file;
201 var line;
202 var topFrameMatches = filteredStack.match(/\(.*\)/);
203 // topFrameMatches's example:
204 // (chrome-extension://pmofbkohncoogjjhahejjfbppikbjigm/utility.js:308:19)
205 var crashLocation = topFrameMatches && topFrameMatches[0];
206 if (crashLocation) {
207 var topFrameElements =
208 crashLocation.substring(1, crashLocation.length - 1).split(':');
209 // topFrameElements for the above example will look like:
210 // [0] chrome-extension
211 // [1] //pmofbkohncoogjjhahejjfbppikbjigm/utility.js
212 // [2] 308
213 // [3] 19
214 if (topFrameElements.length >= 3) {
215 file = topFrameElements[0] + ':' + topFrameElements[1];
216 line = topFrameElements[2];
217 }
218 }
Torne (Richard Coles)868fa2f2013-06-11 10:57:03 +0100219 var requestParameters =
Ben Murdocheb525c52013-07-10 11:40:50 +0100220 'error=' + encodeURIComponent(error.name) +
221 '&script=' + encodeURIComponent(file) +
222 '&line=' + encodeURIComponent(line) +
223 '&trace=' + encodeURIComponent(filteredStack);
Ben Murdochca12bfa2013-07-23 11:17:05 +0100224 var request = buildServerRequest('jserror',
225 'application/x-www-form-urlencoded');
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100226 request.onloadend = function(event) {
227 console.log('sendErrorReport status: ' + request.status);
228 };
229 request.send(requestParameters);
230 }
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100231
232 /**
Ben Murdoch558790d2013-07-30 15:19:42 +0100233 * Unique ID of the next callback.
234 * @type {number}
235 */
236 var nextCallbackId = 0;
237
238 /**
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100239 * Adds error processing to an API callback.
240 * @param {Function} callback Callback to instrument.
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100241 * @param {boolean=} opt_isEventListener True if the callback is an event
242 * listener.
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100243 * @return {Function} Instrumented callback.
244 */
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100245 function wrapCallback(callback, opt_isEventListener) {
246 verify(!(opt_isEventListener && isInTask),
247 'Unrequired callback in a task.');
Ben Murdoch558790d2013-07-30 15:19:42 +0100248 var callbackId = nextCallbackId++;
249 var isTaskCallback = isInTask;
250 if (isTaskCallback)
251 ++taskPendingCallbackCount;
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100252 if (!opt_isEventListener) {
253 checkInInstrumentedCallback();
Ben Murdoch558790d2013-07-30 15:19:42 +0100254 pendingCallbacks[callbackId] = new Error().stack;
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100255 }
Ben Murdoch558790d2013-07-30 15:19:42 +0100256
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100257 return function() {
258 // This is the wrapper for the callback.
259 try {
Ben Murdochbb1529c2013-08-08 10:24:53 +0100260 verify(!isInInstrumentedCallback, 'Re-entering instrumented callback');
261 isInInstrumentedCallback = true;
262
Ben Murdoch558790d2013-07-30 15:19:42 +0100263 if (isTaskCallback) {
264 verify(!isInTask, 'wrapCallback: already in task');
265 isInTask = true;
266 }
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100267 if (!opt_isEventListener)
Ben Murdoch558790d2013-07-30 15:19:42 +0100268 delete pendingCallbacks[callbackId];
269
270 // Call the original callback.
271 callback.apply(null, arguments);
272
273 if (isTaskCallback) {
274 verify(isInTask, 'wrapCallback: not in task at exit');
275 isInTask = false;
276 if (--taskPendingCallbackCount == 0)
277 finish();
278 }
Ben Murdochbb1529c2013-08-08 10:24:53 +0100279
280 verify(isInInstrumentedCallback,
281 'Instrumented callback is not instrumented upon exit');
282 isInInstrumentedCallback = false;
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100283 } catch (error) {
284 var message = 'Uncaught exception:\n' + error.stack;
285 console.error(message);
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100286 if (!errorReported) {
287 errorReported = true;
288 chrome.metricsPrivate.getIsCrashReportingEnabled(function(isEnabled) {
289 if (isEnabled)
290 sendErrorReport(error);
291 });
Ben Murdoch32409262013-08-07 11:04:47 +0100292 debugAlert(message);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100293 }
294 }
295 };
296 }
297
298 /**
299 * Instruments an API function to add error processing to its user
300 * code-provided callback.
301 * @param {Object} namespace Namespace of the API function.
302 * @param {string} functionName Name of the API function.
303 * @param {number} callbackParameter Index of the callback parameter to this
304 * API function.
305 */
306 function instrumentApiFunction(namespace, functionName, callbackParameter) {
307 var originalFunction = namespace[functionName];
308
309 if (!originalFunction)
Ben Murdoch32409262013-08-07 11:04:47 +0100310 debugAlert('Cannot instrument ' + functionName);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100311
312 namespace[functionName] = function() {
313 // This is the wrapper for the API function. Pass the wrapped callback to
314 // the original function.
315 var callback = arguments[callbackParameter];
316 if (typeof callback != 'function') {
Ben Murdoch32409262013-08-07 11:04:47 +0100317 debugAlert('Argument ' + callbackParameter + ' of ' + functionName +
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100318 ' is not a function');
319 }
Ben Murdoch558790d2013-07-30 15:19:42 +0100320 arguments[callbackParameter] = wrapCallback(
321 callback, functionName == 'addListener');
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100322 return originalFunction.apply(namespace, arguments);
323 };
324 }
325
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100326 instrumentApiFunction(chrome.alarms, 'get', 1);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100327 instrumentApiFunction(chrome.alarms.onAlarm, 'addListener', 0);
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100328 instrumentApiFunction(chrome.identity, 'getAuthToken', 1);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100329 instrumentApiFunction(chrome.runtime.onSuspend, 'addListener', 0);
330
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000331 chrome.runtime.onSuspend.addListener(function() {
Ben Murdoch558790d2013-07-30 15:19:42 +0100332 var stringifiedPendingCallbacks = JSON.stringify(pendingCallbacks);
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100333 verify(
Ben Murdoch558790d2013-07-30 15:19:42 +0100334 queue.length == 0 && stringifiedPendingCallbacks == '{}',
335 'Incomplete task or pending callbacks when unloading event page,' +
336 ' queue = ' + JSON.stringify(queue) +
337 ', pendingCallbacks = ' + stringifiedPendingCallbacks);
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000338 });
339
340 return {
341 add: add,
Ben Murdoch558790d2013-07-30 15:19:42 +0100342 debugSetStepName: function() {}, // TODO(vadimt): remove
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100343 instrumentApiFunction: instrumentApiFunction,
344 wrapCallback: wrapCallback
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000345 };
346}
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100347
348var storage = chrome.storage.local;
349
350/**
351 * Builds an object to manage retrying activities with exponential backoff.
352 * @param {string} name Name of this attempt manager.
353 * @param {function()} attempt Activity that the manager retries until it
354 * calls 'stop' method.
355 * @param {number} initialDelaySeconds Default first delay until first retry.
356 * @param {number} maximumDelaySeconds Maximum delay between retries.
357 * @return {Object} Attempt manager interface.
358 */
359function buildAttemptManager(
360 name, attempt, initialDelaySeconds, maximumDelaySeconds) {
Ben Murdoch558790d2013-07-30 15:19:42 +0100361 var alarmName = 'attempt-scheduler-' + name;
362 var currentDelayStorageKey = 'current-delay-' + name;
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100363
364 /**
365 * Creates an alarm for the next attempt. The alarm is repeating for the case
366 * when the next attempt crashes before registering next alarm.
367 * @param {number} delaySeconds Delay until next retry.
368 */
369 function createAlarm(delaySeconds) {
370 var alarmInfo = {
371 delayInMinutes: delaySeconds / 60,
372 periodInMinutes: maximumDelaySeconds / 60
373 };
374 chrome.alarms.create(alarmName, alarmInfo);
375 }
376
377 /**
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100378 * Indicates if this attempt manager has started.
379 * @param {function(boolean)} callback The function's boolean parameter is
380 * true if the attempt manager has started, false otherwise.
381 */
382 function isRunning(callback) {
383 chrome.alarms.get(alarmName, function(alarmInfo) {
384 callback(!!alarmInfo);
385 });
386 }
387
388 /**
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100389 * Schedules next attempt.
390 * @param {number=} opt_previousDelaySeconds Previous delay in a sequence of
391 * retry attempts, if specified. Not specified for scheduling first retry
392 * in the exponential sequence.
393 */
394 function scheduleNextAttempt(opt_previousDelaySeconds) {
395 var base = opt_previousDelaySeconds ? opt_previousDelaySeconds * 2 :
396 initialDelaySeconds;
397 var newRetryDelaySeconds =
398 Math.min(base * (1 + 0.2 * Math.random()), maximumDelaySeconds);
399
400 createAlarm(newRetryDelaySeconds);
401
402 var items = {};
403 items[currentDelayStorageKey] = newRetryDelaySeconds;
404 storage.set(items);
405 }
406
407 /**
408 * Starts repeated attempts.
409 * @param {number=} opt_firstDelaySeconds Time until the first attempt, if
410 * specified. Otherwise, initialDelaySeconds will be used for the first
411 * attempt.
412 */
413 function start(opt_firstDelaySeconds) {
414 if (opt_firstDelaySeconds) {
415 createAlarm(opt_firstDelaySeconds);
416 storage.remove(currentDelayStorageKey);
417 } else {
418 scheduleNextAttempt();
419 }
420 }
421
422 /**
423 * Stops repeated attempts.
424 */
425 function stop() {
426 chrome.alarms.clear(alarmName);
427 storage.remove(currentDelayStorageKey);
428 }
429
430 /**
431 * Plans for the next attempt.
432 * @param {function()} callback Completion callback. It will be invoked after
433 * the planning is done.
434 */
435 function planForNext(callback) {
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100436 storage.get(currentDelayStorageKey, function(items) {
437 console.log('planForNext-get-storage ' + JSON.stringify(items));
438 scheduleNextAttempt(items[currentDelayStorageKey]);
439 callback();
440 });
441 }
442
443 chrome.alarms.onAlarm.addListener(function(alarm) {
444 if (alarm.name == alarmName)
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100445 isRunning(function(running) {
446 if (running)
447 attempt();
448 });
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100449 });
450
451 return {
452 start: start,
453 planForNext: planForNext,
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100454 stop: stop,
455 isRunning: isRunning
456 };
457}
458
459// TODO(robliao): Ideally, the authentication watcher infrastructure
460// below would be an API change to chrome.identity.
461// When this happens, remove the code below.
462
463/**
464 * Wraps chrome.identity to provide limited listening support for
465 * the sign in state by polling periodically for the auth token.
466 * @return {Object} The Authentication Manager interface.
467 */
468function buildAuthenticationManager() {
469 var alarmName = 'sign-in-alarm';
470
471 /**
472 * Determines if the user is signed in and provides a token if signed in.
473 * @param {function(string=)} callback Called on completion.
474 * If the user is signed in, the string contains the token.
475 */
476 function isSignedIn(callback) {
477 chrome.identity.getAuthToken({interactive: false}, function(token) {
478 token = chrome.runtime.lastError ? undefined : token;
479 callback(token);
480 checkAndNotifyListeners(!!token);
481 });
482 }
483
484 /**
485 * Removes the specified cached token.
486 * @param {string} token Authentication Token to remove from the cache.
487 * @param {function} onSuccess Called on completion.
488 */
489 function removeToken(token, onSuccess) {
490 chrome.identity.removeCachedAuthToken({token: token}, function() {
491 // Removing the token from the cache will change the sign in state.
492 // Repoll now to check the state and notify listeners.
493 // This also lets Chrome now about a possible problem with the token.
494 isSignedIn(function() {});
495 onSuccess();
496 });
497 }
498
499 var listeners = [];
500
501 /**
502 * Registers a listener that gets called back when the signed in state
503 * is found to be changed.
504 * @param {function} callback Called when the answer to isSignedIn changes.
505 */
506 function addListener(callback) {
507 listeners.push(callback);
508 }
509
510 // Tracks the last answer of isSignedIn. checkAndNotifyListeners will not
511 // notify the listeners if this is null because technically, no sign in
512 // state change occurred.
513 var lastReturnedSignedInState = null;
514
515 function checkAndNotifyListeners(currentSignedInState) {
516 if ((lastReturnedSignedInState !== currentSignedInState) &&
517 (lastReturnedSignedInState !== null)) {
518 for (var listenerIndex in listeners) {
519 listeners[listenerIndex]();
520 }
521 }
522 lastReturnedSignedInState = currentSignedInState;
523 }
524
525 chrome.alarms.onAlarm.addListener(function(alarm) {
526 if (alarm.name == alarmName)
527 isSignedIn(function() {});
528 });
529
530 // Poll for the sign in state every hour.
531 // One hour is just an arbitrary amount of time chosen.
532 chrome.alarms.create(alarmName, {periodInMinutes: 60});
533
534 return {
535 addListener: addListener,
536 isSignedIn: isSignedIn,
537 removeToken: removeToken
Torne (Richard Coles)90dce4d2013-05-29 14:40:03 +0100538 };
539}