• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (c) 2012 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/**
6 * This class provides a "bridge" for communicating between the javascript and
7 * the browser.
8 */
9var BrowserBridge = (function() {
10  'use strict';
11
12  /**
13   * Delay in milliseconds between updates of certain browser information.
14   */
15  var POLL_INTERVAL_MS = 5000;
16
17  /**
18   * @constructor
19   */
20  function BrowserBridge() {
21    assertFirstConstructorCall(BrowserBridge);
22
23    // List of observers for various bits of browser state.
24    this.connectionTestsObservers_ = [];
25    this.hstsObservers_ = [];
26    this.constantsObservers_ = [];
27    this.crosONCFileParseObservers_ = [];
28    this.storeDebugLogsObservers_ = [];
29    this.setNetworkDebugModeObservers_ = [];
30    // Unprocessed data received before the constants.  This serves to protect
31    // against passing along data before having information on how to interpret
32    // it.
33    this.earlyReceivedData_ = [];
34
35    this.pollableDataHelpers_ = {};
36    this.pollableDataHelpers_.proxySettings =
37        new PollableDataHelper('onProxySettingsChanged',
38                               this.sendGetProxySettings.bind(this));
39    this.pollableDataHelpers_.badProxies =
40        new PollableDataHelper('onBadProxiesChanged',
41                               this.sendGetBadProxies.bind(this));
42    this.pollableDataHelpers_.httpCacheInfo =
43        new PollableDataHelper('onHttpCacheInfoChanged',
44                               this.sendGetHttpCacheInfo.bind(this));
45    this.pollableDataHelpers_.hostResolverInfo =
46        new PollableDataHelper('onHostResolverInfoChanged',
47                               this.sendGetHostResolverInfo.bind(this));
48    this.pollableDataHelpers_.socketPoolInfo =
49        new PollableDataHelper('onSocketPoolInfoChanged',
50                               this.sendGetSocketPoolInfo.bind(this));
51    this.pollableDataHelpers_.sessionNetworkStats =
52      new PollableDataHelper('onSessionNetworkStatsChanged',
53                             this.sendGetSessionNetworkStats.bind(this));
54    this.pollableDataHelpers_.historicNetworkStats =
55      new PollableDataHelper('onHistoricNetworkStatsChanged',
56                             this.sendGetHistoricNetworkStats.bind(this));
57    this.pollableDataHelpers_.quicInfo =
58        new PollableDataHelper('onQuicInfoChanged',
59                               this.sendGetQuicInfo.bind(this));
60    this.pollableDataHelpers_.spdySessionInfo =
61        new PollableDataHelper('onSpdySessionInfoChanged',
62                               this.sendGetSpdySessionInfo.bind(this));
63    this.pollableDataHelpers_.spdyStatus =
64        new PollableDataHelper('onSpdyStatusChanged',
65                               this.sendGetSpdyStatus.bind(this));
66    this.pollableDataHelpers_.spdyAlternateProtocolMappings =
67        new PollableDataHelper('onSpdyAlternateProtocolMappingsChanged',
68                               this.sendGetSpdyAlternateProtocolMappings.bind(
69                                   this));
70    if (cr.isWindows) {
71      this.pollableDataHelpers_.serviceProviders =
72          new PollableDataHelper('onServiceProvidersChanged',
73                                 this.sendGetServiceProviders.bind(this));
74    }
75    this.pollableDataHelpers_.prerenderInfo =
76        new PollableDataHelper('onPrerenderInfoChanged',
77                               this.sendGetPrerenderInfo.bind(this));
78    this.pollableDataHelpers_.httpPipeliningStatus =
79        new PollableDataHelper('onHttpPipeliningStatusChanged',
80                               this.sendGetHttpPipeliningStatus.bind(this));
81    this.pollableDataHelpers_.extensionInfo =
82        new PollableDataHelper('onExtensionInfoChanged',
83                               this.sendGetExtensionInfo.bind(this));
84    if (cr.isChromeOS) {
85      this.pollableDataHelpers_.systemLog =
86          new PollableDataHelper('onSystemLogChanged',
87                               this.getSystemLog.bind(this, 'syslog'));
88    }
89
90    // Setting this to true will cause messages from the browser to be ignored,
91    // and no messages will be sent to the browser, either.  Intended for use
92    // when viewing log files.
93    this.disabled_ = false;
94
95    // Interval id returned by window.setInterval for polling timer.
96    this.pollIntervalId_ = null;
97  }
98
99  cr.addSingletonGetter(BrowserBridge);
100
101  BrowserBridge.prototype = {
102
103    //--------------------------------------------------------------------------
104    // Messages sent to the browser
105    //--------------------------------------------------------------------------
106
107    /**
108     * Wraps |chrome.send|.  Doesn't send anything when disabled.
109     */
110    send: function(value1, value2) {
111      if (!this.disabled_) {
112        if (arguments.length == 1) {
113          chrome.send(value1);
114        } else if (arguments.length == 2) {
115          chrome.send(value1, value2);
116        } else {
117          throw 'Unsupported number of arguments.';
118        }
119      }
120    },
121
122    sendReady: function() {
123      this.send('notifyReady');
124      this.setPollInterval(POLL_INTERVAL_MS);
125    },
126
127    /**
128     * Some of the data we are interested is not currently exposed as a
129     * stream.  This starts polling those with active observers (visible
130     * views) every |intervalMs|.  Subsequent calls override previous calls
131     * to this function.  If |intervalMs| is 0, stops polling.
132     */
133    setPollInterval: function(intervalMs) {
134      if (this.pollIntervalId_ !== null) {
135        window.clearInterval(this.pollIntervalId_);
136        this.pollIntervalId_ = null;
137      }
138
139      if (intervalMs > 0) {
140        this.pollIntervalId_ =
141            window.setInterval(this.checkForUpdatedInfo.bind(this, false),
142                               intervalMs);
143      }
144    },
145
146    sendGetProxySettings: function() {
147      // The browser will call receivedProxySettings on completion.
148      this.send('getProxySettings');
149    },
150
151    sendReloadProxySettings: function() {
152      this.send('reloadProxySettings');
153    },
154
155    sendGetBadProxies: function() {
156      // The browser will call receivedBadProxies on completion.
157      this.send('getBadProxies');
158    },
159
160    sendGetHostResolverInfo: function() {
161      // The browser will call receivedHostResolverInfo on completion.
162      this.send('getHostResolverInfo');
163    },
164
165    sendClearBadProxies: function() {
166      this.send('clearBadProxies');
167    },
168
169    sendClearHostResolverCache: function() {
170      this.send('clearHostResolverCache');
171    },
172
173    sendClearBrowserCache: function() {
174      this.send('clearBrowserCache');
175    },
176
177    sendClearAllCache: function() {
178      this.sendClearHostResolverCache();
179      this.sendClearBrowserCache();
180    },
181
182    sendStartConnectionTests: function(url) {
183      this.send('startConnectionTests', [url]);
184    },
185
186    sendHSTSQuery: function(domain) {
187      this.send('hstsQuery', [domain]);
188    },
189
190    sendHSTSAdd: function(domain, sts_include_subdomains,
191                          pkp_include_subdomains, pins) {
192      this.send('hstsAdd', [domain, sts_include_subdomains,
193                            pkp_include_subdomains, pins]);
194    },
195
196    sendHSTSDelete: function(domain) {
197      this.send('hstsDelete', [domain]);
198    },
199
200    sendGetHttpCacheInfo: function() {
201      this.send('getHttpCacheInfo');
202    },
203
204    sendGetSocketPoolInfo: function() {
205      this.send('getSocketPoolInfo');
206    },
207
208    sendGetSessionNetworkStats: function() {
209      this.send('getSessionNetworkStats');
210    },
211
212    sendGetHistoricNetworkStats: function() {
213      this.send('getHistoricNetworkStats');
214    },
215
216    sendCloseIdleSockets: function() {
217      this.send('closeIdleSockets');
218    },
219
220    sendFlushSocketPools: function() {
221      this.send('flushSocketPools');
222    },
223
224    sendGetQuicInfo: function() {
225      this.send('getQuicInfo');
226    },
227
228    sendGetSpdySessionInfo: function() {
229      this.send('getSpdySessionInfo');
230    },
231
232    sendGetSpdyStatus: function() {
233      this.send('getSpdyStatus');
234    },
235
236    sendGetSpdyAlternateProtocolMappings: function() {
237      this.send('getSpdyAlternateProtocolMappings');
238    },
239
240    sendGetServiceProviders: function() {
241      this.send('getServiceProviders');
242    },
243
244    sendGetPrerenderInfo: function() {
245      this.send('getPrerenderInfo');
246    },
247
248    sendGetHttpPipeliningStatus: function() {
249      this.send('getHttpPipeliningStatus');
250    },
251
252    sendGetExtensionInfo: function() {
253      this.send('getExtensionInfo');
254    },
255
256    enableIPv6: function() {
257      this.send('enableIPv6');
258    },
259
260    setLogLevel: function(logLevel) {
261      this.send('setLogLevel', ['' + logLevel]);
262    },
263
264    refreshSystemLogs: function() {
265      this.send('refreshSystemLogs');
266    },
267
268    getSystemLog: function(log_key, cellId) {
269      this.send('getSystemLog', [log_key, cellId]);
270    },
271
272    importONCFile: function(fileContent, passcode) {
273      this.send('importONCFile', [fileContent, passcode]);
274    },
275
276    storeDebugLogs: function() {
277      this.send('storeDebugLogs');
278    },
279
280    setNetworkDebugMode: function(subsystem) {
281      this.send('setNetworkDebugMode', [subsystem]);
282    },
283
284    //--------------------------------------------------------------------------
285    // Messages received from the browser.
286    //--------------------------------------------------------------------------
287
288    receive: function(command, params) {
289      // Does nothing if disabled.
290      if (this.disabled_)
291        return;
292
293      // If no constants have been received, and params does not contain the
294      // constants, delay handling the data.
295      if (Constants == null && command != 'receivedConstants') {
296        this.earlyReceivedData_.push({ command: command, params: params });
297        return;
298      }
299
300      this[command](params);
301
302      // Handle any data that was received early in the order it was received,
303      // once the constants have been processed.
304      if (this.earlyReceivedData_ != null) {
305        for (var i = 0; i < this.earlyReceivedData_.length; i++) {
306          var command = this.earlyReceivedData_[i];
307          this[command.command](command.params);
308        }
309        this.earlyReceivedData_ = null;
310      }
311    },
312
313    receivedConstants: function(constants) {
314      for (var i = 0; i < this.constantsObservers_.length; i++)
315        this.constantsObservers_[i].onReceivedConstants(constants);
316    },
317
318    receivedLogEntries: function(logEntries) {
319      EventsTracker.getInstance().addLogEntries(logEntries);
320    },
321
322    receivedProxySettings: function(proxySettings) {
323      this.pollableDataHelpers_.proxySettings.update(proxySettings);
324    },
325
326    receivedBadProxies: function(badProxies) {
327      this.pollableDataHelpers_.badProxies.update(badProxies);
328    },
329
330    receivedHostResolverInfo: function(hostResolverInfo) {
331      this.pollableDataHelpers_.hostResolverInfo.update(hostResolverInfo);
332    },
333
334    receivedSocketPoolInfo: function(socketPoolInfo) {
335      this.pollableDataHelpers_.socketPoolInfo.update(socketPoolInfo);
336    },
337
338    receivedSessionNetworkStats: function(sessionNetworkStats) {
339      this.pollableDataHelpers_.sessionNetworkStats.update(sessionNetworkStats);
340    },
341
342    receivedHistoricNetworkStats: function(historicNetworkStats) {
343      this.pollableDataHelpers_.historicNetworkStats.update(
344          historicNetworkStats);
345    },
346
347    receivedQuicInfo: function(quicInfo) {
348      this.pollableDataHelpers_.quicInfo.update(quicInfo);
349    },
350
351    receivedSpdySessionInfo: function(spdySessionInfo) {
352      this.pollableDataHelpers_.spdySessionInfo.update(spdySessionInfo);
353    },
354
355    receivedSpdyStatus: function(spdyStatus) {
356      this.pollableDataHelpers_.spdyStatus.update(spdyStatus);
357    },
358
359    receivedSpdyAlternateProtocolMappings:
360        function(spdyAlternateProtocolMappings) {
361      this.pollableDataHelpers_.spdyAlternateProtocolMappings.update(
362          spdyAlternateProtocolMappings);
363    },
364
365    receivedServiceProviders: function(serviceProviders) {
366      this.pollableDataHelpers_.serviceProviders.update(serviceProviders);
367    },
368
369    receivedStartConnectionTestSuite: function() {
370      for (var i = 0; i < this.connectionTestsObservers_.length; i++)
371        this.connectionTestsObservers_[i].onStartedConnectionTestSuite();
372    },
373
374    receivedStartConnectionTestExperiment: function(experiment) {
375      for (var i = 0; i < this.connectionTestsObservers_.length; i++) {
376        this.connectionTestsObservers_[i].onStartedConnectionTestExperiment(
377            experiment);
378      }
379    },
380
381    receivedCompletedConnectionTestExperiment: function(info) {
382      for (var i = 0; i < this.connectionTestsObservers_.length; i++) {
383        this.connectionTestsObservers_[i].onCompletedConnectionTestExperiment(
384            info.experiment, info.result);
385      }
386    },
387
388    receivedCompletedConnectionTestSuite: function() {
389      for (var i = 0; i < this.connectionTestsObservers_.length; i++)
390        this.connectionTestsObservers_[i].onCompletedConnectionTestSuite();
391    },
392
393    receivedHSTSResult: function(info) {
394      for (var i = 0; i < this.hstsObservers_.length; i++)
395        this.hstsObservers_[i].onHSTSQueryResult(info);
396    },
397
398    receivedONCFileParse: function(error) {
399      for (var i = 0; i < this.crosONCFileParseObservers_.length; i++)
400        this.crosONCFileParseObservers_[i].onONCFileParse(error);
401    },
402
403    receivedStoreDebugLogs: function(status) {
404      for (var i = 0; i < this.storeDebugLogsObservers_.length; i++)
405        this.storeDebugLogsObservers_[i].onStoreDebugLogs(status);
406    },
407
408    receivedSetNetworkDebugMode: function(status) {
409      for (var i = 0; i < this.setNetworkDebugModeObservers_.length; i++)
410        this.setNetworkDebugModeObservers_[i].onSetNetworkDebugMode(status);
411    },
412
413    receivedHttpCacheInfo: function(info) {
414      this.pollableDataHelpers_.httpCacheInfo.update(info);
415    },
416
417    receivedPrerenderInfo: function(prerenderInfo) {
418      this.pollableDataHelpers_.prerenderInfo.update(prerenderInfo);
419    },
420
421    receivedHttpPipeliningStatus: function(httpPipeliningStatus) {
422      this.pollableDataHelpers_.httpPipeliningStatus.update(
423          httpPipeliningStatus);
424    },
425
426    receivedExtensionInfo: function(extensionInfo) {
427      this.pollableDataHelpers_.extensionInfo.update(extensionInfo);
428    },
429
430    getSystemLogCallback: function(systemLog) {
431      this.pollableDataHelpers_.systemLog.update(systemLog);
432    },
433
434    //--------------------------------------------------------------------------
435
436    /**
437     * Prevents receiving/sending events to/from the browser.
438     */
439    disable: function() {
440      this.disabled_ = true;
441      this.setPollInterval(0);
442    },
443
444    /**
445     * Returns true if the BrowserBridge has been disabled.
446     */
447    isDisabled: function() {
448      return this.disabled_;
449    },
450
451    /**
452     * Adds a listener of the proxy settings. |observer| will be called back
453     * when data is received, through:
454     *
455     *   observer.onProxySettingsChanged(proxySettings)
456     *
457     * |proxySettings| is a dictionary with (up to) two properties:
458     *
459     *   "original"  -- The settings that chrome was configured to use
460     *                  (i.e. system settings.)
461     *   "effective" -- The "effective" proxy settings that chrome is using.
462     *                  (decides between the manual/automatic modes of the
463     *                  fetched settings).
464     *
465     * Each of these two configurations is formatted as a string, and may be
466     * omitted if not yet initialized.
467     *
468     * If |ignoreWhenUnchanged| is true, data is only sent when it changes.
469     * If it's false, data is sent whenever it's received from the browser.
470     */
471    addProxySettingsObserver: function(observer, ignoreWhenUnchanged) {
472      this.pollableDataHelpers_.proxySettings.addObserver(observer,
473                                                          ignoreWhenUnchanged);
474    },
475
476    /**
477     * Adds a listener of the proxy settings. |observer| will be called back
478     * when data is received, through:
479     *
480     *   observer.onBadProxiesChanged(badProxies)
481     *
482     * |badProxies| is an array, where each entry has the property:
483     *   badProxies[i].proxy_uri: String identify the proxy.
484     *   badProxies[i].bad_until: The time when the proxy stops being considered
485     *                            bad. Note the time is in time ticks.
486     */
487    addBadProxiesObserver: function(observer, ignoreWhenUnchanged) {
488      this.pollableDataHelpers_.badProxies.addObserver(observer,
489                                                       ignoreWhenUnchanged);
490    },
491
492    /**
493     * Adds a listener of the host resolver info. |observer| will be called back
494     * when data is received, through:
495     *
496     *   observer.onHostResolverInfoChanged(hostResolverInfo)
497     */
498    addHostResolverInfoObserver: function(observer, ignoreWhenUnchanged) {
499      this.pollableDataHelpers_.hostResolverInfo.addObserver(
500          observer, ignoreWhenUnchanged);
501    },
502
503    /**
504     * Adds a listener of the socket pool. |observer| will be called back
505     * when data is received, through:
506     *
507     *   observer.onSocketPoolInfoChanged(socketPoolInfo)
508     */
509    addSocketPoolInfoObserver: function(observer, ignoreWhenUnchanged) {
510      this.pollableDataHelpers_.socketPoolInfo.addObserver(observer,
511                                                           ignoreWhenUnchanged);
512    },
513
514    /**
515     * Adds a listener of the network session. |observer| will be called back
516     * when data is received, through:
517     *
518     *   observer.onSessionNetworkStatsChanged(sessionNetworkStats)
519     */
520    addSessionNetworkStatsObserver: function(observer, ignoreWhenUnchanged) {
521      this.pollableDataHelpers_.sessionNetworkStats.addObserver(
522          observer, ignoreWhenUnchanged);
523    },
524
525    /**
526     * Adds a listener of persistent network session data. |observer| will be
527     * called back when data is received, through:
528     *
529     *   observer.onHistoricNetworkStatsChanged(historicNetworkStats)
530     */
531    addHistoricNetworkStatsObserver: function(observer, ignoreWhenUnchanged) {
532      this.pollableDataHelpers_.historicNetworkStats.addObserver(
533          observer, ignoreWhenUnchanged);
534    },
535
536    /**
537     * Adds a listener of the QUIC info. |observer| will be called back
538     * when data is received, through:
539     *
540     *   observer.onQuicInfoChanged(quicInfo)
541     */
542    addQuicInfoObserver: function(observer, ignoreWhenUnchanged) {
543      this.pollableDataHelpers_.quicInfo.addObserver(
544          observer, ignoreWhenUnchanged);
545    },
546
547    /**
548     * Adds a listener of the SPDY info. |observer| will be called back
549     * when data is received, through:
550     *
551     *   observer.onSpdySessionInfoChanged(spdySessionInfo)
552     */
553    addSpdySessionInfoObserver: function(observer, ignoreWhenUnchanged) {
554      this.pollableDataHelpers_.spdySessionInfo.addObserver(
555          observer, ignoreWhenUnchanged);
556    },
557
558    /**
559     * Adds a listener of the SPDY status. |observer| will be called back
560     * when data is received, through:
561     *
562     *   observer.onSpdyStatusChanged(spdyStatus)
563     */
564    addSpdyStatusObserver: function(observer, ignoreWhenUnchanged) {
565      this.pollableDataHelpers_.spdyStatus.addObserver(observer,
566                                                       ignoreWhenUnchanged);
567    },
568
569    /**
570     * Adds a listener of the AlternateProtocolMappings. |observer| will be
571     * called back when data is received, through:
572     *
573     *   observer.onSpdyAlternateProtocolMappingsChanged(
574     *       spdyAlternateProtocolMappings)
575     */
576    addSpdyAlternateProtocolMappingsObserver: function(observer,
577                                                       ignoreWhenUnchanged) {
578      this.pollableDataHelpers_.spdyAlternateProtocolMappings.addObserver(
579          observer, ignoreWhenUnchanged);
580    },
581
582    /**
583     * Adds a listener of the service providers info. |observer| will be called
584     * back when data is received, through:
585     *
586     *   observer.onServiceProvidersChanged(serviceProviders)
587     *
588     * Will do nothing if on a platform other than Windows, as service providers
589     * are only present on Windows.
590     */
591    addServiceProvidersObserver: function(observer, ignoreWhenUnchanged) {
592      if (this.pollableDataHelpers_.serviceProviders) {
593        this.pollableDataHelpers_.serviceProviders.addObserver(
594            observer, ignoreWhenUnchanged);
595      }
596    },
597
598    /**
599     * Adds a listener for the progress of the connection tests.
600     * The observer will be called back with:
601     *
602     *   observer.onStartedConnectionTestSuite();
603     *   observer.onStartedConnectionTestExperiment(experiment);
604     *   observer.onCompletedConnectionTestExperiment(experiment, result);
605     *   observer.onCompletedConnectionTestSuite();
606     */
607    addConnectionTestsObserver: function(observer) {
608      this.connectionTestsObservers_.push(observer);
609    },
610
611    /**
612     * Adds a listener for the http cache info results.
613     * The observer will be called back with:
614     *
615     *   observer.onHttpCacheInfoChanged(info);
616     */
617    addHttpCacheInfoObserver: function(observer, ignoreWhenUnchanged) {
618      this.pollableDataHelpers_.httpCacheInfo.addObserver(
619          observer, ignoreWhenUnchanged);
620    },
621
622    /**
623     * Adds a listener for the results of HSTS (HTTPS Strict Transport Security)
624     * queries. The observer will be called back with:
625     *
626     *   observer.onHSTSQueryResult(result);
627     */
628    addHSTSObserver: function(observer) {
629      this.hstsObservers_.push(observer);
630    },
631
632    /**
633     * Adds a listener for ONC file parse status. The observer will be called
634     * back with:
635     *
636     *   observer.onONCFileParse(error);
637     */
638    addCrosONCFileParseObserver: function(observer) {
639      this.crosONCFileParseObservers_.push(observer);
640    },
641
642    /**
643     * Adds a listener for storing log file status. The observer will be called
644     * back with:
645     *
646     *   observer.onStoreDebugLogs(status);
647     */
648    addStoreDebugLogsObserver: function(observer) {
649      this.storeDebugLogsObservers_.push(observer);
650    },
651
652    /**
653     * Adds a listener for network debugging mode status. The observer
654     * will be called back with:
655     *
656     *   observer.onSetNetworkDebugMode(status);
657     */
658    addSetNetworkDebugModeObserver: function(observer) {
659      this.setNetworkDebugModeObservers_.push(observer);
660    },
661
662    /**
663     * Adds a listener for the received constants event. |observer| will be
664     * called back when the constants are received, through:
665     *
666     *   observer.onReceivedConstants(constants);
667     */
668    addConstantsObserver: function(observer) {
669      this.constantsObservers_.push(observer);
670    },
671
672    /**
673     * Adds a listener for updated prerender info events
674     * |observer| will be called back with:
675     *
676     *   observer.onPrerenderInfoChanged(prerenderInfo);
677     */
678    addPrerenderInfoObserver: function(observer, ignoreWhenUnchanged) {
679      this.pollableDataHelpers_.prerenderInfo.addObserver(
680          observer, ignoreWhenUnchanged);
681    },
682
683    /**
684     * Adds a listener of HTTP pipelining status. |observer| will be called
685     * back when data is received, through:
686     *
687     *   observer.onHttpPipelineStatusChanged(httpPipeliningStatus)
688     */
689    addHttpPipeliningStatusObserver: function(observer, ignoreWhenUnchanged) {
690      this.pollableDataHelpers_.httpPipeliningStatus.addObserver(
691          observer, ignoreWhenUnchanged);
692    },
693
694    /**
695     * Adds a listener of extension information. |observer| will be called
696     * back when data is received, through:
697     *
698     *   observer.onExtensionInfoChanged(extensionInfo)
699     */
700    addExtensionInfoObserver: function(observer, ignoreWhenUnchanged) {
701      this.pollableDataHelpers_.extensionInfo.addObserver(
702          observer, ignoreWhenUnchanged);
703    },
704
705    /**
706     * Adds a listener of system log information. |observer| will be called
707     * back when data is received, through:
708     *
709     *   observer.onSystemLogChanged(systemLogInfo)
710     */
711    addSystemLogObserver: function(observer, ignoreWhenUnchanged) {
712      if (this.pollableDataHelpers_.systemLog) {
713        this.pollableDataHelpers_.systemLog.addObserver(
714            observer, ignoreWhenUnchanged);
715      }
716    },
717
718    /**
719     * If |force| is true, calls all startUpdate functions.  Otherwise, just
720     * runs updates with active observers.
721     */
722    checkForUpdatedInfo: function(force) {
723      for (var name in this.pollableDataHelpers_) {
724        var helper = this.pollableDataHelpers_[name];
725        if (force || helper.hasActiveObserver())
726          helper.startUpdate();
727      }
728    },
729
730    /**
731     * Calls all startUpdate functions and, if |callback| is non-null,
732     * calls it with the results of all updates.
733     */
734    updateAllInfo: function(callback) {
735      if (callback)
736        new UpdateAllObserver(callback, this.pollableDataHelpers_);
737      this.checkForUpdatedInfo(true);
738    }
739  };
740
741  /**
742   * This is a helper class used by BrowserBridge, to keep track of:
743   *   - the list of observers interested in some piece of data.
744   *   - the last known value of that piece of data.
745   *   - the name of the callback method to invoke on observers.
746   *   - the update function.
747   * @constructor
748   */
749  function PollableDataHelper(observerMethodName, startUpdateFunction) {
750    this.observerMethodName_ = observerMethodName;
751    this.startUpdate = startUpdateFunction;
752    this.observerInfos_ = [];
753  }
754
755  PollableDataHelper.prototype = {
756    getObserverMethodName: function() {
757      return this.observerMethodName_;
758    },
759
760    isObserver: function(object) {
761      for (var i = 0; i < this.observerInfos_.length; i++) {
762        if (this.observerInfos_[i].observer === object)
763          return true;
764      }
765      return false;
766    },
767
768    /**
769     * If |ignoreWhenUnchanged| is true, we won't send data again until it
770     * changes.
771     */
772    addObserver: function(observer, ignoreWhenUnchanged) {
773      this.observerInfos_.push(new ObserverInfo(observer, ignoreWhenUnchanged));
774    },
775
776    removeObserver: function(observer) {
777      for (var i = 0; i < this.observerInfos_.length; i++) {
778        if (this.observerInfos_[i].observer === observer) {
779          this.observerInfos_.splice(i, 1);
780          return;
781        }
782      }
783    },
784
785    /**
786     * Helper function to handle calling all the observers, but ONLY if the data
787     * has actually changed since last time or the observer has yet to receive
788     * any data. This is used for data we received from browser on an update
789     * loop.
790     */
791    update: function(data) {
792      var prevData = this.currentData_;
793      var changed = false;
794
795      // If the data hasn't changed since last time, will only need to notify
796      // observers that have not yet received any data.
797      if (!prevData || JSON.stringify(prevData) != JSON.stringify(data)) {
798        changed = true;
799        this.currentData_ = data;
800      }
801
802      // Notify the observers of the change, as needed.
803      for (var i = 0; i < this.observerInfos_.length; i++) {
804        var observerInfo = this.observerInfos_[i];
805        if (changed || !observerInfo.hasReceivedData ||
806            !observerInfo.ignoreWhenUnchanged) {
807          observerInfo.observer[this.observerMethodName_](this.currentData_);
808          observerInfo.hasReceivedData = true;
809        }
810      }
811    },
812
813    /**
814     * Returns true if one of the observers actively wants the data
815     * (i.e. is visible).
816     */
817    hasActiveObserver: function() {
818      for (var i = 0; i < this.observerInfos_.length; i++) {
819        if (this.observerInfos_[i].observer.isActive())
820          return true;
821      }
822      return false;
823    }
824  };
825
826  /**
827   * This is a helper class used by PollableDataHelper, to keep track of
828   * each observer and whether or not it has received any data.  The
829   * latter is used to make sure that new observers get sent data on the
830   * update following their creation.
831   * @constructor
832   */
833  function ObserverInfo(observer, ignoreWhenUnchanged) {
834    this.observer = observer;
835    this.hasReceivedData = false;
836    this.ignoreWhenUnchanged = ignoreWhenUnchanged;
837  }
838
839  /**
840   * This is a helper class used by BrowserBridge to send data to
841   * a callback once data from all polls has been received.
842   *
843   * It works by keeping track of how many polling functions have
844   * yet to receive data, and recording the data as it it received.
845   *
846   * @constructor
847   */
848  function UpdateAllObserver(callback, pollableDataHelpers) {
849    this.callback_ = callback;
850    this.observingCount_ = 0;
851    this.updatedData_ = {};
852
853    for (var name in pollableDataHelpers) {
854      ++this.observingCount_;
855      var helper = pollableDataHelpers[name];
856      helper.addObserver(this);
857      this[helper.getObserverMethodName()] =
858          this.onDataReceived_.bind(this, helper, name);
859    }
860  }
861
862  UpdateAllObserver.prototype = {
863    isActive: function() {
864      return true;
865    },
866
867    onDataReceived_: function(helper, name, data) {
868      helper.removeObserver(this);
869      --this.observingCount_;
870      this.updatedData_[name] = data;
871      if (this.observingCount_ == 0)
872        this.callback_(this.updatedData_);
873    }
874  };
875
876  return BrowserBridge;
877})();
878