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