main.js revision 3f50c38dc070f4bb515c1b64450dae14f316474e
1// Copyright (c) 2010 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 * Dictionary of constants (initialized by browser).
7 */
8var LogEventType = null;
9var LogEventPhase = null;
10var ClientInfo = null;
11var LogSourceType = null;
12var LogLevelType = null;
13var NetError = null;
14var LoadFlag = null;
15var AddressFamily = null;
16
17/**
18 * Object to communicate between the renderer and the browser.
19 * @type {!BrowserBridge}
20 */
21var g_browser = null;
22
23/**
24 * Main entry point. called once the page has loaded.
25 */
26function onLoaded() {
27  g_browser = new BrowserBridge();
28
29  // Create the view which displays events lists, and lets you select, filter
30  // and delete them.
31  var eventsView = new EventsView('eventsListTableBody',
32                                  'filterInput',
33                                  'filterCount',
34                                  'deleteSelected',
35                                  'deleteAll',
36                                  'selectAll',
37                                  'sortById',
38                                  'sortBySource',
39                                  'sortByDescription',
40
41                                  // IDs for the details view.
42                                  'detailsTabHandles',
43                                  'detailsLogTab',
44                                  'detailsTimelineTab',
45                                  'detailsLogBox',
46                                  'detailsTimelineBox',
47
48                                  // IDs for the layout boxes.
49                                  'filterBox',
50                                  'eventsBox',
51                                  'actionBox',
52                                  'splitterBox');
53
54  // Create a view which will display info on the proxy setup.
55  var proxyView = new ProxyView('proxyTabContent',
56                                'proxyOriginalSettings',
57                                'proxyEffectiveSettings',
58                                'proxyReloadSettings',
59                                'badProxiesTableBody',
60                                'clearBadProxies',
61                                'proxyResolverLog');
62
63  // Create a view which will display information on the host resolver.
64  var dnsView = new DnsView('dnsTabContent',
65                            'hostResolverCacheTbody',
66                            'clearHostResolverCache',
67                            'hostResolverDefaultFamily',
68                            'hostResolverIPv6Disabled',
69                            'hostResolverEnableIPv6',
70                            'hostResolverCacheCapacity',
71                            'hostResolverCacheTTLSuccess',
72                            'hostResolverCacheTTLFailure');
73
74  // Create a view which will display import/export options to control the
75  // captured data.
76  var dataView = new DataView('dataTabContent', 'exportedDataText',
77                              'exportToText', 'securityStrippingCheckbox',
78                              'byteLoggingCheckbox',
79                              'passivelyCapturedCount',
80                              'activelyCapturedCount',
81                              'dataViewDeleteAll');
82
83  // Create a view which will display the results and controls for connection
84  // tests.
85  var testView = new TestView('testTabContent', 'testUrlInput',
86                              'connectionTestsForm', 'testSummary');
87
88  var httpCacheView = new HttpCacheView('httpCacheTabContent',
89                                        'httpCacheStats');
90
91  var socketsView = new SocketsView('socketsTabContent',
92                                    'socketPoolDiv',
93                                    'socketPoolGroupsDiv');
94
95  var spdyView = new SpdyView('spdyTabContent',
96                              'spdySessionNoneSpan',
97                              'spdySessionLinkSpan',
98                              'spdySessionDiv');
99
100
101  var serviceView;
102  if (g_browser.isPlatformWindows()) {
103    serviceView = new ServiceProvidersView('serviceProvidersTab',
104                                           'serviceProvidersTabContent',
105                                           'serviceProvidersTbody',
106                                           'namespaceProvidersTbody');
107  }
108
109  // Create a view which lets you tab between the different sub-views.
110  var categoryTabSwitcher = new TabSwitcherView('categoryTabHandles');
111
112  // Populate the main tabs.
113  categoryTabSwitcher.addTab('eventsTab', eventsView, false);
114  categoryTabSwitcher.addTab('proxyTab', proxyView, false);
115  categoryTabSwitcher.addTab('dnsTab', dnsView, false);
116  categoryTabSwitcher.addTab('socketsTab', socketsView, false);
117  categoryTabSwitcher.addTab('spdyTab', spdyView, false);
118  categoryTabSwitcher.addTab('httpCacheTab', httpCacheView, false);
119  categoryTabSwitcher.addTab('dataTab', dataView, false);
120  if (g_browser.isPlatformWindows())
121    categoryTabSwitcher.addTab('serviceProvidersTab', serviceView, false);
122  categoryTabSwitcher.addTab('testTab', testView, false);
123
124  // Build a map from the anchor name of each tab handle to its "tab ID".
125  // We will consider navigations to the #hash as a switch tab request.
126  var anchorMap = {};
127  var tabIds = categoryTabSwitcher.getAllTabIds();
128  for (var i = 0; i < tabIds.length; ++i) {
129    var aNode = document.getElementById(tabIds[i]);
130    anchorMap[aNode.hash] = tabIds[i];
131  }
132  // Default the empty hash to the data tab.
133  anchorMap['#'] = anchorMap[''] = 'dataTab';
134
135  window.onhashchange = onUrlHashChange.bind(null, anchorMap,
136                                             categoryTabSwitcher);
137
138  // Make this category tab widget the primary view, that fills the whole page.
139  var windowView = new WindowView(categoryTabSwitcher);
140
141  // Trigger initial layout.
142  windowView.resetGeometry();
143
144  // Select the initial view based on the current URL.
145  window.onhashchange();
146
147  // Tell the browser that we are ready to start receiving log events.
148  g_browser.sendReady();
149}
150
151/**
152 * This class provides a "bridge" for communicating between the javascript and
153 * the browser.
154 *
155 * @constructor
156 */
157function BrowserBridge() {
158  // List of observers for various bits of browser state.
159  this.logObservers_ = [];
160  this.connectionTestsObservers_ = [];
161
162  this.pollableDataHelpers_ = {};
163  this.pollableDataHelpers_.proxySettings =
164      new PollableDataHelper('onProxySettingsChanged',
165                             this.sendGetProxySettings.bind(this));
166  this.pollableDataHelpers_.badProxies =
167      new PollableDataHelper('onBadProxiesChanged',
168                             this.sendGetBadProxies.bind(this));
169  this.pollableDataHelpers_.httpCacheInfo =
170      new PollableDataHelper('onHttpCacheInfoChanged',
171                             this.sendGetHttpCacheInfo.bind(this));
172  this.pollableDataHelpers_.hostResolverInfo =
173      new PollableDataHelper('onHostResolverInfoChanged',
174                             this.sendGetHostResolverInfo.bind(this));
175  this.pollableDataHelpers_.socketPoolInfo =
176      new PollableDataHelper('onSocketPoolInfoChanged',
177                             this.sendGetSocketPoolInfo.bind(this));
178  this.pollableDataHelpers_.spdySessionInfo =
179      new PollableDataHelper('onSpdySessionInfoChanged',
180                             this.sendGetSpdySessionInfo.bind(this));
181  if (this.isPlatformWindows()) {
182    this.pollableDataHelpers_.serviceProviders =
183        new PollableDataHelper('onServiceProvidersChanged',
184                               this.sendGetServiceProviders.bind(this));
185  }
186
187  // Cache of the data received.
188  this.numPassivelyCapturedEvents_ = 0;
189  this.capturedEvents_ = [];
190
191  // Next unique id to be assigned to a log entry without a source.
192  // Needed to simplify deletion, identify associated GUI elements, etc.
193  this.nextSourcelessEventId_ = -1;
194}
195
196/*
197 * Takes the current hash in form of "#tab&param1=value1&param2=value2&...".
198 * Puts the parameters in an object, and passes the resulting object to
199 * |categoryTabSwitcher|.  Uses tab and |anchorMap| to find a tab ID,
200 * which it also passes to the tab switcher.
201 *
202 * Parameters and values are decoded with decodeURIComponent().
203 */
204function onUrlHashChange(anchorMap, categoryTabSwitcher) {
205  var parameters = window.location.hash.split('&');
206
207  var tabId = anchorMap[parameters[0]];
208  if (!tabId)
209    return;
210
211  // Split each string except the first around the '='.
212  var paramDict = null;
213  for (var i = 1; i < parameters.length; i++) {
214    var paramStrings = parameters[i].split('=');
215    if (paramStrings.length != 2)
216      continue;
217    if (paramDict == null)
218      paramDict = {};
219    var key = decodeURIComponent(paramStrings[0]);
220    var value = decodeURIComponent(paramStrings[1]);
221    paramDict[key] = value;
222  }
223
224  categoryTabSwitcher.switchToTab(tabId, paramDict);
225}
226
227/**
228 * Delay in milliseconds between updates of certain browser information.
229 */
230BrowserBridge.POLL_INTERVAL_MS = 5000;
231
232//------------------------------------------------------------------------------
233// Messages sent to the browser
234//------------------------------------------------------------------------------
235
236BrowserBridge.prototype.sendReady = function() {
237  chrome.send('notifyReady');
238
239  // Some of the data we are interested is not currently exposed as a stream,
240  // so we will poll the browser to find out when it changes and then notify
241  // the observers.
242  window.setInterval(this.checkForUpdatedInfo.bind(this, false),
243                     BrowserBridge.POLL_INTERVAL_MS);
244};
245
246BrowserBridge.prototype.isPlatformWindows = function() {
247  return /Win/.test(navigator.platform);
248};
249
250BrowserBridge.prototype.sendGetProxySettings = function() {
251  // The browser will call receivedProxySettings on completion.
252  chrome.send('getProxySettings');
253};
254
255BrowserBridge.prototype.sendReloadProxySettings = function() {
256  chrome.send('reloadProxySettings');
257};
258
259BrowserBridge.prototype.sendGetBadProxies = function() {
260  // The browser will call receivedBadProxies on completion.
261  chrome.send('getBadProxies');
262};
263
264BrowserBridge.prototype.sendGetHostResolverInfo = function() {
265  // The browser will call receivedHostResolverInfo on completion.
266  chrome.send('getHostResolverInfo');
267};
268
269BrowserBridge.prototype.sendClearBadProxies = function() {
270  chrome.send('clearBadProxies');
271};
272
273BrowserBridge.prototype.sendClearHostResolverCache = function() {
274  chrome.send('clearHostResolverCache');
275};
276
277BrowserBridge.prototype.sendStartConnectionTests = function(url) {
278  chrome.send('startConnectionTests', [url]);
279};
280
281BrowserBridge.prototype.sendGetHttpCacheInfo = function() {
282  chrome.send('getHttpCacheInfo');
283};
284
285BrowserBridge.prototype.sendGetSocketPoolInfo = function() {
286  chrome.send('getSocketPoolInfo');
287};
288
289BrowserBridge.prototype.sendGetSpdySessionInfo = function() {
290  chrome.send('getSpdySessionInfo');
291};
292
293BrowserBridge.prototype.sendGetServiceProviders = function() {
294  chrome.send('getServiceProviders');
295};
296
297BrowserBridge.prototype.enableIPv6 = function() {
298  chrome.send('enableIPv6');
299};
300
301BrowserBridge.prototype.setLogLevel = function(logLevel) {
302  chrome.send('setLogLevel', ['' + logLevel]);
303}
304
305//------------------------------------------------------------------------------
306// Messages received from the browser
307//------------------------------------------------------------------------------
308
309BrowserBridge.prototype.receivedLogEntries = function(logEntries) {
310  for (var e = 0; e < logEntries.length; ++e) {
311    var logEntry = logEntries[e];
312
313    // Assign unique ID, if needed.
314    if (logEntry.source.id == 0) {
315      logEntry.source.id = this.nextSourcelessEventId_;
316      --this.nextSourcelessEventId_;
317    }
318    this.capturedEvents_.push(logEntry);
319    for (var i = 0; i < this.logObservers_.length; ++i)
320      this.logObservers_[i].onLogEntryAdded(logEntry);
321  }
322};
323
324BrowserBridge.prototype.receivedLogEventTypeConstants = function(constantsMap) {
325  LogEventType = constantsMap;
326};
327
328BrowserBridge.prototype.receivedClientInfo =
329function(info) {
330  ClientInfo = info;
331};
332
333BrowserBridge.prototype.receivedLogEventPhaseConstants =
334function(constantsMap) {
335  LogEventPhase = constantsMap;
336};
337
338BrowserBridge.prototype.receivedLogSourceTypeConstants =
339function(constantsMap) {
340  LogSourceType = constantsMap;
341};
342
343BrowserBridge.prototype.receivedLogLevelConstants =
344function(constantsMap) {
345  LogLevelType = constantsMap;
346};
347
348BrowserBridge.prototype.receivedLoadFlagConstants = function(constantsMap) {
349  LoadFlag = constantsMap;
350};
351
352BrowserBridge.prototype.receivedNetErrorConstants = function(constantsMap) {
353  NetError = constantsMap;
354};
355
356BrowserBridge.prototype.receivedAddressFamilyConstants =
357function(constantsMap) {
358  AddressFamily = constantsMap;
359};
360
361BrowserBridge.prototype.receivedTimeTickOffset = function(timeTickOffset) {
362  this.timeTickOffset_ = timeTickOffset;
363};
364
365BrowserBridge.prototype.receivedProxySettings = function(proxySettings) {
366  this.pollableDataHelpers_.proxySettings.update(proxySettings);
367};
368
369BrowserBridge.prototype.receivedBadProxies = function(badProxies) {
370  this.pollableDataHelpers_.badProxies.update(badProxies);
371};
372
373BrowserBridge.prototype.receivedHostResolverInfo =
374function(hostResolverInfo) {
375  this.pollableDataHelpers_.hostResolverInfo.update(hostResolverInfo);
376};
377
378BrowserBridge.prototype.receivedSocketPoolInfo = function(socketPoolInfo) {
379  this.pollableDataHelpers_.socketPoolInfo.update(socketPoolInfo);
380};
381
382BrowserBridge.prototype.receivedSpdySessionInfo = function(spdySessionInfo) {
383  this.pollableDataHelpers_.spdySessionInfo.update(spdySessionInfo);
384};
385
386BrowserBridge.prototype.receivedServiceProviders = function(serviceProviders) {
387  this.pollableDataHelpers_.serviceProviders.update(serviceProviders);
388};
389
390BrowserBridge.prototype.receivedPassiveLogEntries = function(entries) {
391  // Due to an expected race condition, it is possible to receive actively
392  // captured log entries before the passively logged entries are received.
393  //
394  // When that happens, we create a copy of the actively logged entries, delete
395  // all entries, and, after handling all the passively logged entries, add back
396  // the deleted actively logged entries.
397  var earlyActivelyCapturedEvents = this.capturedEvents_.slice(0);
398  if (earlyActivelyCapturedEvents.length > 0)
399    this.deleteAllEvents();
400
401  this.numPassivelyCapturedEvents_ = entries.length;
402  for (var i = 0; i < entries.length; ++i)
403    entries[i].wasPassivelyCaptured = true;
404  this.receivedLogEntries(entries);
405
406  // Add back early actively captured events, if any.
407  if (earlyActivelyCapturedEvents.length)
408    this.receivedLogEntries(earlyActivelyCapturedEvents);
409};
410
411
412BrowserBridge.prototype.receivedStartConnectionTestSuite = function() {
413  for (var i = 0; i < this.connectionTestsObservers_.length; ++i)
414    this.connectionTestsObservers_[i].onStartedConnectionTestSuite();
415};
416
417BrowserBridge.prototype.receivedStartConnectionTestExperiment = function(
418    experiment) {
419  for (var i = 0; i < this.connectionTestsObservers_.length; ++i) {
420    this.connectionTestsObservers_[i].onStartedConnectionTestExperiment(
421        experiment);
422  }
423};
424
425BrowserBridge.prototype.receivedCompletedConnectionTestExperiment =
426function(info) {
427  for (var i = 0; i < this.connectionTestsObservers_.length; ++i) {
428    this.connectionTestsObservers_[i].onCompletedConnectionTestExperiment(
429        info.experiment, info.result);
430  }
431};
432
433BrowserBridge.prototype.receivedCompletedConnectionTestSuite = function() {
434  for (var i = 0; i < this.connectionTestsObservers_.length; ++i)
435    this.connectionTestsObservers_[i].onCompletedConnectionTestSuite();
436};
437
438BrowserBridge.prototype.receivedHttpCacheInfo = function(info) {
439  this.pollableDataHelpers_.httpCacheInfo.update(info);
440};
441
442//------------------------------------------------------------------------------
443
444/**
445 * Adds a listener of log entries. |observer| will be called back when new log
446 * data arrives, through:
447 *
448 *   observer.onLogEntryAdded(logEntry)
449 */
450BrowserBridge.prototype.addLogObserver = function(observer) {
451  this.logObservers_.push(observer);
452};
453
454/**
455 * Adds a listener of the proxy settings. |observer| will be called back when
456 * data is received, through:
457 *
458 *   observer.onProxySettingsChanged(proxySettings)
459 *
460 * |proxySettings| is a dictionary with (up to) two properties:
461 *
462 *   "original"  -- The settings that chrome was configured to use
463 *                  (i.e. system settings.)
464 *   "effective" -- The "effective" proxy settings that chrome is using.
465 *                  (decides between the manual/automatic modes of the
466 *                  fetched settings).
467 *
468 * Each of these two configurations is formatted as a string, and may be
469 * omitted if not yet initialized.
470 *
471 * TODO(eroman): send a dictionary instead.
472 */
473BrowserBridge.prototype.addProxySettingsObserver = function(observer) {
474  this.pollableDataHelpers_.proxySettings.addObserver(observer);
475};
476
477/**
478 * Adds a listener of the proxy settings. |observer| will be called back when
479 * data is received, through:
480 *
481 *   observer.onBadProxiesChanged(badProxies)
482 *
483 * |badProxies| is an array, where each entry has the property:
484 *   badProxies[i].proxy_uri: String identify the proxy.
485 *   badProxies[i].bad_until: The time when the proxy stops being considered
486 *                            bad. Note the time is in time ticks.
487 */
488BrowserBridge.prototype.addBadProxiesObserver = function(observer) {
489  this.pollableDataHelpers_.badProxies.addObserver(observer);
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 */
498BrowserBridge.prototype.addHostResolverInfoObserver = function(observer) {
499  this.pollableDataHelpers_.hostResolverInfo.addObserver(observer);
500};
501
502/**
503 * Adds a listener of the socket pool. |observer| will be called back
504 * when data is received, through:
505 *
506 *   observer.onSocketPoolInfoChanged(socketPoolInfo)
507 */
508BrowserBridge.prototype.addSocketPoolInfoObserver = function(observer) {
509  this.pollableDataHelpers_.socketPoolInfo.addObserver(observer);
510};
511
512/**
513 * Adds a listener of the SPDY info. |observer| will be called back
514 * when data is received, through:
515 *
516 *   observer.onSpdySessionInfoChanged(spdySessionInfo)
517 */
518BrowserBridge.prototype.addSpdySessionInfoObserver = function(observer) {
519  this.pollableDataHelpers_.spdySessionInfo.addObserver(observer);
520};
521
522/**
523 * Adds a listener of the service providers info. |observer| will be called
524 * back when data is received, through:
525 *
526 *   observer.onServiceProvidersChanged(serviceProviders)
527 */
528BrowserBridge.prototype.addServiceProvidersObserver = function(observer) {
529  this.pollableDataHelpers_.serviceProviders.addObserver(observer);
530};
531
532/**
533 * Adds a listener for the progress of the connection tests.
534 * The observer will be called back with:
535 *
536 *   observer.onStartedConnectionTestSuite();
537 *   observer.onStartedConnectionTestExperiment(experiment);
538 *   observer.onCompletedConnectionTestExperiment(experiment, result);
539 *   observer.onCompletedConnectionTestSuite();
540 */
541BrowserBridge.prototype.addConnectionTestsObserver = function(observer) {
542  this.connectionTestsObservers_.push(observer);
543};
544
545/**
546 * Adds a listener for the http cache info results.
547 * The observer will be called back with:
548 *
549 *   observer.onHttpCacheInfoChanged(info);
550 */
551BrowserBridge.prototype.addHttpCacheInfoObserver = function(observer) {
552  this.pollableDataHelpers_.httpCacheInfo.addObserver(observer);
553};
554
555/**
556 * The browser gives us times in terms of "time ticks" in milliseconds.
557 * This function converts the tick count to a Date() object.
558 *
559 * @param {String} timeTicks.
560 * @returns {Date} The time that |timeTicks| represents.
561 */
562BrowserBridge.prototype.convertTimeTicksToDate = function(timeTicks) {
563  // Note that the subtraction by 0 is to cast to a number (probably a float
564  // since the numbers are big).
565  var timeStampMs = (this.timeTickOffset_ - 0) + (timeTicks - 0);
566  var d = new Date();
567  d.setTime(timeStampMs);
568  return d;
569};
570
571/**
572 * Returns a list of all captured events.
573 */
574BrowserBridge.prototype.getAllCapturedEvents = function() {
575  return this.capturedEvents_;
576};
577
578/**
579 * Returns the number of events that were captured while we were
580 * listening for events.
581 */
582BrowserBridge.prototype.getNumActivelyCapturedEvents = function() {
583  return this.capturedEvents_.length - this.numPassivelyCapturedEvents_;
584};
585
586/**
587 * Returns the number of events that were captured passively by the
588 * browser prior to when the net-internals page was started.
589 */
590BrowserBridge.prototype.getNumPassivelyCapturedEvents = function() {
591  return this.numPassivelyCapturedEvents_;
592};
593
594/**
595 * Deletes captured events with source IDs in |sourceIds|.
596 */
597BrowserBridge.prototype.deleteEventsBySourceId = function(sourceIds) {
598  var sourceIdDict = {};
599  for (var i = 0; i < sourceIds.length; i++)
600    sourceIdDict[sourceIds[i]] = true;
601
602  var newEventList = [];
603  for (var i = 0; i < this.capturedEvents_.length; ++i) {
604    var id = this.capturedEvents_[i].source.id;
605    if (id in sourceIdDict) {
606      if (this.capturedEvents_[i].wasPassivelyCaptured)
607        --this.numPassivelyCapturedEvents_;
608      continue;
609    }
610    newEventList.push(this.capturedEvents_[i]);
611  }
612  this.capturedEvents_ = newEventList;
613
614  for (var i = 0; i < this.logObservers_.length; ++i)
615    this.logObservers_[i].onLogEntriesDeleted(sourceIds);
616};
617
618/**
619 * Deletes all captured events.
620 */
621BrowserBridge.prototype.deleteAllEvents = function() {
622  this.capturedEvents_ = [];
623  this.numPassivelyCapturedEvents_ = 0;
624  for (var i = 0; i < this.logObservers_.length; ++i)
625    this.logObservers_[i].onAllLogEntriesDeleted();
626};
627
628/**
629 * If |force| is true, calls all startUpdate functions.  Otherwise, just
630 * runs updates with active observers.
631 */
632BrowserBridge.prototype.checkForUpdatedInfo = function(force) {
633  for (name in this.pollableDataHelpers_) {
634    var helper = this.pollableDataHelpers_[name];
635    if (force || helper.hasActiveObserver())
636      helper.startUpdate();
637  }
638};
639
640/**
641 * Calls all startUpdate functions and, if |callback| is non-null,
642 * calls it with the results of all updates.
643 */
644BrowserBridge.prototype.updateAllInfo = function(callback) {
645  if (callback)
646    new UpdateAllObserver(callback, this.pollableDataHelpers_);
647  this.checkForUpdatedInfo(true);
648};
649
650/**
651 * This is a helper class used by BrowserBridge, to keep track of:
652 *   - the list of observers interested in some piece of data.
653 *   - the last known value of that piece of data.
654 *   - the name of the callback method to invoke on observers.
655 *   - the update function.
656 * @constructor
657 */
658function PollableDataHelper(observerMethodName, startUpdateFunction) {
659  this.observerMethodName_ = observerMethodName;
660  this.startUpdate = startUpdateFunction;
661  this.observerInfos_ = [];
662}
663
664PollableDataHelper.prototype.getObserverMethodName = function() {
665  return this.observerMethodName_;
666};
667
668/**
669 * This is a helper class used by PollableDataHelper, to keep track of
670 * each observer and whether or not it has received any data.  The
671 * latter is used to make sure that new observers get sent data on the
672 * update following their creation.
673 * @constructor
674 */
675function ObserverInfo(observer) {
676  this.observer = observer;
677  this.hasReceivedData = false;
678}
679
680PollableDataHelper.prototype.addObserver = function(observer) {
681  this.observerInfos_.push(new ObserverInfo(observer));
682};
683
684PollableDataHelper.prototype.removeObserver = function(observer) {
685  for (var i = 0; i < this.observerInfos_.length; ++i) {
686    if (this.observerInfos_[i].observer == observer) {
687      this.observerInfos_.splice(i, 1);
688      return;
689    }
690  }
691};
692
693/**
694 * Helper function to handle calling all the observers, but ONLY if the data has
695 * actually changed since last time or the observer has yet to receive any data.
696 * This is used for data we received from browser on an update loop.
697 */
698PollableDataHelper.prototype.update = function(data) {
699  var prevData = this.currentData_;
700  var changed = false;
701
702  // If the data hasn't changed since last time, will only need to notify
703  // observers that have not yet received any data.
704  if (!prevData || JSON.stringify(prevData) != JSON.stringify(data)) {
705    changed = true;
706    this.currentData_ = data;
707  }
708
709  // Notify the observers of the change, as needed.
710  for (var i = 0; i < this.observerInfos_.length; ++i) {
711    var observerInfo = this.observerInfos_[i];
712    if (changed || !observerInfo.hasReceivedData) {
713      observerInfo.observer[this.observerMethodName_](this.currentData_);
714      observerInfo.hasReceivedData = true;
715    }
716  }
717};
718
719/**
720 * Returns true if one of the observers actively wants the data
721 * (i.e. is visible).
722 */
723PollableDataHelper.prototype.hasActiveObserver = function() {
724  for (var i = 0; i < this.observerInfos_.length; ++i) {
725    if (this.observerInfos_[i].observer.isActive())
726      return true;
727  }
728  return false;
729};
730
731/**
732 * This is a helper class used by BrowserBridge to send data to
733 * a callback once data from all polls has been received.
734 *
735 * It works by keeping track of how many polling functions have
736 * yet to receive data, and recording the data as it it received.
737 *
738 * @constructor
739 */
740function UpdateAllObserver(callback, pollableDataHelpers) {
741  this.callback_ = callback;
742  this.observingCount_ = 0;
743  this.updatedData_ = {};
744
745  for (name in pollableDataHelpers) {
746    ++this.observingCount_;
747    var helper = pollableDataHelpers[name];
748    helper.addObserver(this);
749    this[helper.getObserverMethodName()] =
750        this.onDataReceived_.bind(this, helper, name);
751  }
752}
753
754UpdateAllObserver.prototype.isActive = function() {
755  return true;
756};
757
758UpdateAllObserver.prototype.onDataReceived_ = function(helper, name, data) {
759  helper.removeObserver(this);
760  --this.observingCount_;
761  this.updatedData_[name] = data;
762  if (this.observingCount_ == 0)
763    this.callback_(this.updatedData_);
764};
765