bluetooth: Componentize device list in chrome://bluetooth-internals.

Separates device list HTML management code from device handling code.
Adds DeviceTable for management of device table UI.
Adds observable DeviceCollection for dynamic updates of DeviceTable.
Adds AdapterBroker to handle adapter events and simplifier Adapter
  service communication.

BUG=651282
CQ_INCLUDE_TRYBOTS=master.tryserver.chromium.linux:closure_compilation
TBR=dbeam@chromium.org

Review-Url: https://codereview.chromium.org/2446823002
Cr-Commit-Position: refs/heads/master@{#430458}
diff --git a/chrome/browser/browser_resources.grd b/chrome/browser/browser_resources.grd
index cbef592..6590f23 100644
--- a/chrome/browser/browser_resources.grd
+++ b/chrome/browser/browser_resources.grd
@@ -104,11 +104,15 @@
         <include name="IDR_ABOUT_SYS_HTML" file="resources\about_sys\about_sys.html" flattenhtml="true" type="BINDATA" />
       </if>
       <include name="IDR_AD_NETWORK_HASHES" file="resources\ad_networks.dat" type="BINDATA" />
-      <include name="IDR_BLUETOOTH_INTERNALS_CSS" file="resources\bluetooth_internals\bluetooth_internals.css" type="BINDATA" compress="gzip" />
-      <include name="IDR_BLUETOOTH_INTERNALS_HTML" file="resources\bluetooth_internals\bluetooth_internals.html" flattenhtml="true" allowexternalscript="true" type="BINDATA" compress="gzip" />
-      <include name="IDR_BLUETOOTH_INTERNALS_JS" file="resources\bluetooth_internals\bluetooth_internals.js" type="BINDATA" compress="gzip" />
       <include name="IDR_BLUETOOTH_ADAPTER_MOJO_JS" file="${root_gen_dir}\device\bluetooth\public\interfaces\adapter.mojom.js" use_base_dir="false" type="BINDATA" compress="gzip" />
       <include name="IDR_BLUETOOTH_DEVICE_MOJO_JS" file="${root_gen_dir}\device\bluetooth\public\interfaces\device.mojom.js" use_base_dir="false" type="BINDATA" compress="gzip" />
+      <include name="IDR_BLUETOOTH_INTERNALS_CSS" file="resources\bluetooth_internals\bluetooth_internals.css" type="BINDATA" compress="gzip" />
+      <include name="IDR_BLUETOOTH_INTERNALS_ADAPTER_BROKER_JS" file="resources\bluetooth_internals\adapter_broker.js" type="BINDATA" compress="gzip" />
+      <include name="IDR_BLUETOOTH_INTERNALS_DEVICE_COLLECTION_JS" file="resources\bluetooth_internals\device_collection.js" type="BINDATA" compress="gzip" />
+      <include name="IDR_BLUETOOTH_INTERNALS_DEVICE_TABLE_JS" file="resources\bluetooth_internals\device_table.js" type="BINDATA" compress="gzip" />
+      <include name="IDR_BLUETOOTH_INTERNALS_HTML" file="resources\bluetooth_internals\bluetooth_internals.html" flattenhtml="true" allowexternalscript="true" type="BINDATA" compress="gzip" />
+      <include name="IDR_BLUETOOTH_INTERNALS_INTERFACES_JS" file="resources\bluetooth_internals\interfaces.js" type="BINDATA" compress="gzip" />
+      <include name="IDR_BLUETOOTH_INTERNALS_JS" file="resources\bluetooth_internals\bluetooth_internals.js" type="BINDATA" compress="gzip" />
       <include name="IDR_BOOKMARKS_MANIFEST" file="resources\bookmark_manager\manifest.json" type="BINDATA" />
       <if expr="is_posix and not is_macosx and not is_ios">
         <include name="IDR_CERTIFICATE_VIEWER_HTML" file="resources\certificate_viewer.html" type="BINDATA" />
diff --git a/chrome/browser/resources/bluetooth_internals/adapter_broker.js b/chrome/browser/resources/bluetooth_internals/adapter_broker.js
new file mode 100644
index 0000000..c17c89f
--- /dev/null
+++ b/chrome/browser/resources/bluetooth_internals/adapter_broker.js
@@ -0,0 +1,149 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * Javascript for AdapterBroker, served from
+ *     chrome://bluetooth-internals/.
+ */
+cr.define('adapter_broker', function() {
+  /**
+   * The proxy class of an adapter and router of adapter events.
+   * Exposes an EventTarget interface that allows other object to subscribe to
+   * to specific AdapterClient events.
+   * Provides proxy access to Adapter functions. Converts parameters to Mojo
+   * handles and back when necessary.
+   * @constructor
+   * @extends {cr.EventTarget}
+   * @param {!interfaces.BluetoothAdapter.Adapter.proxyClass} adapter
+   */
+  var AdapterBroker = function(adapter) {
+    this.adapter_ = adapter;
+    this.adapterClient_ = new AdapterClient(this);
+    this.setClient(this.adapterClient_);
+  };
+
+  AdapterBroker.prototype = {
+    __proto__: cr.EventTarget.prototype,
+
+    /**
+     * Sets client of Adapter service.
+     * @param {interfaces.BluetoothAdapter.AdapterClient} adapterClient
+     */
+    setClient: function(adapterClient) {
+      this.adapter_.setClient(interfaces.Connection.bindStubDerivedImpl(
+          adapterClient));
+    },
+
+    /**
+     * Gets an array of currently detectable devices from the Adapter service.
+     * @return {Array<interfaces.BluetoothDevice.DeviceInfo>}
+     */
+    getDevices: function() {
+      return this.adapter_.getDevices();
+    },
+
+    /**
+     * Gets the current state of the Adapter.
+     * @return {interfaces.BluetoothAdapter.AdapterInfo}
+     */
+    getInfo: function() {
+      return this.adapter_.getInfo();
+    }
+  };
+
+  /**
+   * The implementation of AdapterClient in
+   * device/bluetooth/public/interfaces/adapter.mojom. Dispatches events
+   * through AdapterBroker to notify client objects of changes to the Adapter
+   * service.
+   * @constructor
+   * @param {!AdapterBroker} adapterBroker Broker to dispatch events through.
+   */
+  var AdapterClient = function(adapterBroker) {
+    this.adapterBroker_ = adapterBroker;
+  };
+
+  AdapterClient.prototype = {
+    /**
+     * Fires deviceadded event.
+     * @param {!interfaces.BluetoothDevice.DeviceInfo} deviceInfo
+     */
+    deviceAdded: function(deviceInfo) {
+      var event = new CustomEvent('deviceadded', {
+        detail: {
+          deviceInfo: deviceInfo
+        }
+      });
+      this.adapterBroker_.dispatchEvent(event);
+    },
+
+    /**
+     * Fires deviceremoved event.
+     * @param {!interfaces.BluetoothDevice.DeviceInfo} deviceInfo
+     */
+    deviceRemoved: function(deviceInfo) {
+      var event = new CustomEvent('deviceremoved', {
+        detail: {
+          deviceInfo: deviceInfo
+        }
+      });
+      this.adapterBroker_.dispatchEvent(event);
+    },
+
+    /**
+     * Fires devicechanged event.
+     * @param {!interfaces.BluetoothDevice.DeviceInfo} deviceInfo
+     */
+    deviceChanged: function(deviceInfo) {
+      var event = new CustomEvent('devicechanged', {
+        detail: {
+          deviceInfo: deviceInfo
+        }
+      });
+      this.adapterBroker_.dispatchEvent(event);
+    }
+  };
+
+  var adapterBroker = null;
+
+  /**
+   * Initializes an AdapterBroker if one doesn't exist.
+   * @return {Promise<AdapterBroker>} resolves with AdapterBroker,
+   *     rejects if Bluetooth is not supported.
+   */
+  function getAdapterBroker() {
+    if (adapterBroker) {
+      return Promise.resolve(adapterBroker);
+    }
+
+    return interfaces.importInterfaces().then(function(adapter) {
+      // Hook up the instance properties.
+      AdapterClient.prototype.__proto__ =
+          interfaces.BluetoothAdapter.AdapterClient.stubClass.prototype;
+
+      var adapterFactory = interfaces.Connection.bindHandleToProxy(
+          interfaces.FrameInterfaces.getInterface(
+              interfaces.BluetoothAdapter.AdapterFactory.name),
+          interfaces.BluetoothAdapter.AdapterFactory);
+
+      // Get an Adapter service.
+      return adapterFactory.getAdapter();
+    }).then(function(response) {
+      if (!response.adapter) {
+        throw new Error('Bluetooth Not Supported on this platform.');
+      }
+
+      var adapter = interfaces.Connection.bindHandleToProxy(
+          response.adapter,
+          interfaces.BluetoothAdapter.Adapter);
+
+      adapterBroker = new AdapterBroker(adapter);
+      return adapterBroker;
+    });
+  }
+
+  return {
+    getAdapterBroker: getAdapterBroker,
+  };
+});
diff --git a/chrome/browser/resources/bluetooth_internals/bluetooth_internals.html b/chrome/browser/resources/bluetooth_internals/bluetooth_internals.html
index 28b85ba..2a6760c 100644
--- a/chrome/browser/resources/bluetooth_internals/bluetooth_internals.html
+++ b/chrome/browser/resources/bluetooth_internals/bluetooth_internals.html
@@ -8,8 +8,17 @@
   <title>Bluetooth Internals</title>
   <link rel="stylesheet" href="chrome://resources/css/text_defaults_md.css">
   <link rel="stylesheet" href="bluetooth_internals.css">
+  <link rel="import" href="chrome://resources/html/cr/ui.html">
+
   <script src="chrome://resources/js/assert.js"></script>
+  <script src="chrome://resources/js/cr/event_target.js"></script>
+  <script src="chrome://resources/js/cr/ui/array_data_model.js"></script>
   <script src="chrome://resources/js/util.js"></script>
+
+  <script src="interfaces.js"></script>
+  <script src="adapter_broker.js"></script>
+  <script src="device_collection.js"></script>
+  <script src="device_table.js"></script>
   <script src="bluetooth_internals.js"></script>
 </head>
 
@@ -19,25 +28,20 @@
       Bluetooth Internals
     </div>
   </header>
-  <table id="device-table">
-    <thead>
-      <tr>
-        <th>Name</th>
-        <th>Address</th>
-        <th>Latest RSSI</th>
-      </tr>
-    </thead>
-    <tbody id="device-list">
-    </tbody>
-  </table>
 </body>
 
-<template id="device-row-template">
-  <tr class="device-row">
-    <td class="device-name" data-label="Name"></td>
-    <td class="device-address" data-label="Address"></td>
-    <td class="device-rssi" data-label="Latest RSSI">Unknown</td>
-  </tr>
+<template id="table-template">
+  <table>
+    <thead>
+      <tr>
+        <th data-field="name_for_display">Name</th>
+        <th data-field="address">Address</th>
+        <th data-field="rssi.value">Latest RSSI</th>
+      </tr>
+    </thead>
+    <tbody class="table-body">
+    </tbody>
+  </table>
 </template>
 
 </html>
\ No newline at end of file
diff --git a/chrome/browser/resources/bluetooth_internals/bluetooth_internals.js b/chrome/browser/resources/bluetooth_internals/bluetooth_internals.js
index 93a7393..4057293 100644
--- a/chrome/browser/resources/bluetooth_internals/bluetooth_internals.js
+++ b/chrome/browser/resources/bluetooth_internals/bluetooth_internals.js
@@ -7,128 +7,42 @@
  *     chrome://bluetooth-internals/.
  */
 
-(function() {
-  var adapter, adapterClient, bluetoothAdapter, bluetoothDevice, connection;
-
-  var REMOVED_CSS = 'removed';
-
-  /*
-   * Data model for a cached device.
-   * @constructor
-   * @param {!bluetoothDevice.DeviceInfo} info
-   */
-  var Device = function(info) { this.info = info; };
-
-  /**
-   * The implementation of AdapterClient in
-   *     device/bluetooth/public/interfaces/adapter.mojom. This also manages the
-   *     client-side collection of devices.
-   * @constructor
-   */
-  var AdapterClient = function() { this.devices_ = new Map(); };
-  AdapterClient.prototype = {
-    /**
-     * Caches the device info and updates the device list.
-     * @param {!bluetoothDevice.DeviceInfo} deviceInfo
-     */
-    deviceAdded: function(deviceInfo) {
-      if (this.devices_.has(deviceInfo.address)) {
-        var deviceElement = $(deviceInfo.address);
-        deviceElement.classList.remove(REMOVED_CSS);
-      } else {
-        this.devices_.set(deviceInfo.address, new Device(deviceInfo));
-
-        var deviceRowTemplate = $('device-row-template');
-        var deviceRow = document.importNode(
-            deviceRowTemplate.content.children[0], true /* deep */);
-        deviceRow.id = deviceInfo.address;
-
-        var deviceList = $('device-list');
-        deviceList.appendChild(deviceRow);
-      }
-
-      this.deviceChanged(deviceInfo);
-    },
-
-    /**
-     * Marks device as removed.
-     * @param {!bluetoothDevice.DeviceInfo} deviceInfo
-     */
-    deviceRemoved: function(deviceInfo) {
-      $(deviceInfo.address).classList.add(REMOVED_CSS);
-    },
-
-    /**
-     * Updates cached device and updates the device list.
-     * @param {!bluetoothDevice.DeviceInfo} deviceInfo
-     */
-    deviceChanged: function(deviceInfo) {
-      console.log(new Date(), deviceInfo);
-
-      assert(this.devices_.has(deviceInfo.address), 'Device does not exist.');
-
-      this.devices_.get(deviceInfo.address).info = deviceInfo;
-
-      var deviceRow = $(deviceInfo.address);
-      deviceRow.querySelector('.device-name').textContent =
-          deviceInfo.name_for_display;
-      deviceRow.querySelector('.device-address').textContent =
-          deviceInfo.address;
-
-      var rssi = (deviceInfo.rssi && deviceInfo.rssi.value) ||
-          deviceRow.querySelector('.device-rssi').textContent;
-      deviceRow.querySelector('.device-rssi').textContent = rssi;
-    }
-  };
-
-  /**
-   * Initializes Mojo proxies for page and Bluetooth services.
-   * @return {!Promise} resolves if adapter is acquired, rejects if Bluetooth
-   *     is not supported.
-   */
-  function initializeProxies() {
-    return importModules([
-      'content/public/renderer/frame_interfaces',
-      'device/bluetooth/public/interfaces/adapter.mojom',
-      'device/bluetooth/public/interfaces/device.mojom',
-      'mojo/public/js/connection',
-    ]).then(function([frameInterfaces, ...modules]) {
-      // Destructure here to assign global variables.
-      [bluetoothAdapter, bluetoothDevice, connection] = modules;
-
-      // Hook up the instance properties.
-      AdapterClient.prototype.__proto__ =
-          bluetoothAdapter.AdapterClient.stubClass.prototype;
-
-      var adapterFactory = connection.bindHandleToProxy(
-          frameInterfaces.getInterface(bluetoothAdapter.AdapterFactory.name),
-          bluetoothAdapter.AdapterFactory);
-
-      // Get an Adapter service.
-      return adapterFactory.getAdapter();
-    }).then(function(response) {
-      if (!response.adapter) {
-        throw new Error('Bluetooth Not Supported on this platform.');
-      }
-
-      adapter = connection.bindHandleToProxy(response.adapter,
-                                             bluetoothAdapter.Adapter);
-
-      // Create a message pipe and bind one end to client
-      // implementation and the other to the Adapter service.
-      adapterClient = new AdapterClient();
-      adapter.setClient(connection.bindStubDerivedImpl(adapterClient));
-    });
-  }
-
-  document.addEventListener('DOMContentLoaded', function() {
-    initializeProxies()
-      .then(function() { return adapter.getInfo(); })
+cr.define('bluetooth_internals', function() {
+  function initializeViews() {
+    var adapterBroker = null;
+    adapter_broker.getAdapterBroker()
+      .then(function(broker) { adapterBroker = broker; })
+      .then(function() { return adapterBroker.getInfo(); })
       .then(function(response) { console.log('adapter', response.info); })
-      .then(function() { return adapter.getDevices(); })
+      .then(function() { return adapterBroker.getDevices(); })
       .then(function(response) {
-        response.devices.forEach(adapterClient.deviceAdded, adapterClient);
+        // Hook up device collection events.
+        var devices = new device_collection.DeviceCollection([]);
+        adapterBroker.addEventListener('deviceadded', function(event) {
+          devices.addOrUpdate(event.detail.deviceInfo);
+        });
+        adapterBroker.addEventListener('devicechanged', function(event) {
+          devices.addOrUpdate(event.detail.deviceInfo);
+        });
+        adapterBroker.addEventListener('deviceremoved', function(event) {
+          devices.remove(event.detail.deviceInfo);
+        });
+
+        response.devices.forEach(devices.addOrUpdate,
+                                 devices /* this */);
+
+        var deviceTable = new device_table.DeviceTable();
+        deviceTable.setDevices(devices);
+        document.body.appendChild(deviceTable);
       })
       .catch(function(error) { console.error(error); });
-  });
-})();
+  }
+
+  return {
+    initializeViews: initializeViews
+  };
+
+});
+
+document.addEventListener(
+    'DOMContentLoaded', bluetooth_internals.initializeViews);
diff --git a/chrome/browser/resources/bluetooth_internals/device_collection.js b/chrome/browser/resources/bluetooth_internals/device_collection.js
new file mode 100644
index 0000000..4d86d99
--- /dev/null
+++ b/chrome/browser/resources/bluetooth_internals/device_collection.js
@@ -0,0 +1,85 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * Javascript for DeviceCollection, served from
+ *     chrome://bluetooth-internals/.
+ */
+
+cr.define('device_collection', function() {
+  /*
+   * Collection of devices. Extends ArrayDataModel which provides a set of
+   * functions and events that notifies observers when the collection changes.
+   * @constructor
+   * @param {!Array<device_collection.Device>} array The starting collection of
+   *     devices.
+   * @extends {cr.ui.ArrayDataModel}
+   */
+  var DeviceCollection = function(array) {
+    cr.ui.ArrayDataModel.call(this, array);
+  };
+  DeviceCollection.prototype = {
+    __proto__: cr.ui.ArrayDataModel.prototype,
+
+    /**
+     * Finds the Device in the collection with the matching address.
+     * @param {string} address
+     */
+    getByAddress: function(address) {
+      for (var i = 0; i < this.length; i++) {
+        var device = this.item(i);
+        if (address == device.info.address)
+          return device;
+      }
+      return null;
+    },
+
+    /**
+     * Adds or updates a Device with new DeviceInfo.
+     * @param {!interfaces.BluetoothDevice.DeviceInfo} deviceInfo
+     */
+    addOrUpdate: function(deviceInfo) {
+      var oldDevice = this.getByAddress(deviceInfo.address);
+      if (oldDevice) {
+        // Update rssi if it's valid
+        var rssi = (deviceInfo.rssi && deviceInfo.rssi.value) ||
+            (oldDevice.info.rssi && oldDevice.info.rssi.value);
+
+        oldDevice.info = deviceInfo;
+        oldDevice.info.rssi = { value: rssi };
+        oldDevice.removed = false;
+
+        this.updateIndex(this.indexOf(oldDevice));
+      } else {
+        this.push(new Device(deviceInfo));
+      }
+    },
+
+    /**
+     * Marks the Device as removed.
+     * @param {!interfaces.bluetoothDevice.DeviceInfo} deviceInfo
+     */
+    remove: function(deviceInfo) {
+      var device = this.getByAddress(deviceInfo.address);
+      assert(device, 'Device does not exist.');
+      device.removed = true;
+      this.updateIndex(this.indexOf(device));
+    }
+  };
+
+  /*
+   * Data model for a cached device.
+   * @constructor
+   * @param {!interfaces.BluetoothDevice.DeviceInfo} info
+   */
+  var Device = function(info) {
+    this.info = info;
+    this.removed = false;
+  };
+
+  return {
+    Device: Device,
+    DeviceCollection: DeviceCollection,
+  };
+});
diff --git a/chrome/browser/resources/bluetooth_internals/device_table.js b/chrome/browser/resources/bluetooth_internals/device_table.js
new file mode 100644
index 0000000..b4db9b0
--- /dev/null
+++ b/chrome/browser/resources/bluetooth_internals/device_table.js
@@ -0,0 +1,148 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * Javascript for DeviceTable UI, served from
+ *     chrome://bluetooth-internals/.
+ */
+
+cr.define('device_table', function() {
+  var REMOVED_CSS = 'removed';
+
+  /**
+   * A table that lists the devices and responds to changes in the given
+   *     DeviceCollection.
+   * @constructor
+   * @extends {HTMLTableElement}
+   */
+  var DeviceTable = cr.ui.define(function() {
+    // @type {Array<device_collection.Device>}
+    this.devices_ = null;
+
+    return document.importNode($('table-template').content.children[0],
+                               true /* deep */);
+  });
+
+  DeviceTable.prototype = {
+    __proto__: HTMLTableElement.prototype,
+
+    /**
+     * Decorates an element as a UI element class. Caches references to the
+     *    table body and headers.
+     */
+    decorate: function() {
+      this.body_ = this.tBodies[0];
+      this.headers_ = this.tHead.rows[0].cells;
+    },
+
+    /**
+     * Sets the tables device collection.
+     * @param {!device_collection.DeviceCollection} deviceCollection
+     */
+    setDevices: function(deviceCollection) {
+      assert(!this.devices_, 'Devices can only be set once.');
+
+      this.devices_ = deviceCollection;
+      this.devices_.addEventListener('sorted', this.redraw_.bind(this));
+      this.devices_.addEventListener('change', this.handleChange_.bind(this));
+      this.devices_.addEventListener('splice', this.handleSplice_.bind(this));
+
+      this.redraw_();
+    },
+
+    /**
+     * Updates table row on change event of the device collection.
+     * @private
+     * @param {!CustomEvent} event
+     */
+    handleChange_: function(event) {
+      this.updateRow_(this.devices_.item(event.index), event.index);
+    },
+
+    /**
+     * Updates table row on splice event of the device collection.
+     * @private
+     * @param {!CustomEvent} event
+     */
+    handleSplice_: function(event) {
+      event.removed.forEach(function() {
+        this.body_.deleteRow(event.index);
+      }, this);
+
+      event.added.forEach(function(device, index) {
+        this.insertRow_(device, event.index + index);
+      }, this);
+    },
+
+    /**
+     * Inserts a new row at |index| and updates it with info from |device|.
+     * @private
+     * @param {!device_collection.Device} device
+     * @param {?number} index
+     */
+    insertRow_: function(device, index) {
+      var row = this.body_.insertRow(index);
+      row.id = device.info.address;
+
+      for (var i = 0; i < this.headers_.length; i++) {
+        row.insertCell();
+      }
+
+      this.updateRow_(device, row.sectionRowIndex);
+    },
+
+    /**
+     * Deletes and recreates the table using the cached |devices_|.
+     * @private
+     */
+    redraw_: function() {
+      this.removeChild(this.body_);
+      this.appendChild(document.createElement('tbody'));
+      this.body_ = this.tBodies[0];
+      this.body_.classList.add('table-body');
+
+      for (var i = 0; i < this.devices_.length; i++) {
+        this.insertRow_(this.devices_.item(i));
+      }
+    },
+
+    /**
+     * Updates the row at |index| with the info from |device|.
+     * @private
+     * @param {!device_collection.Device} device
+     * @param {number} index
+     */
+    updateRow_: function(device, index) {
+      assert(this.body_.rows[index], 'Row ' + index + ' is not in the table.');
+      var row = this.body_.rows[index];
+
+      if (device.removed) {
+        row.classList.add(REMOVED_CSS);
+      } else {
+        row.classList.remove(REMOVED_CSS);
+      }
+
+      // Update the properties based on the header field path.
+      for (var i = 0; i < this.headers_.length; i++) {
+        var header = this.headers_[i];
+        var propName = header.dataset.field;
+
+        var parts = propName.split('.');
+        var obj = device.info;
+        while (obj != null && parts.length > 0) {
+          var part = parts.shift();
+          obj = obj[part];
+        }
+
+        var cell = row.cells[i];
+        cell.textContent = obj || 'Unknown';
+        cell.dataset.label = header.textContent;
+      }
+    },
+  };
+
+  return {
+    DeviceTable: DeviceTable,
+  };
+});
diff --git a/chrome/browser/resources/bluetooth_internals/interfaces.js b/chrome/browser/resources/bluetooth_internals/interfaces.js
new file mode 100644
index 0000000..391deac
--- /dev/null
+++ b/chrome/browser/resources/bluetooth_internals/interfaces.js
@@ -0,0 +1,39 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * Javascript for Mojo interface helpers, served from
+ *     chrome://bluetooth-internals/.
+ */
+
+cr.define('interfaces', function() {
+  /**
+   * Imports Mojo interfaces and adds them to window.interfaces.
+   * @return {Promise}
+   */
+  function importInterfaces() {
+    return importModules([
+      'content/public/renderer/frame_interfaces',
+      'device/bluetooth/public/interfaces/adapter.mojom',
+      'device/bluetooth/public/interfaces/device.mojom',
+      'mojo/public/js/connection',
+    ]).then(function([frameInterfaces, bluetoothAdapter, bluetoothDevice,
+        connection]) {
+      Object.assign(interfaces, {
+        BluetoothAdapter: bluetoothAdapter,
+        BluetoothDevice: bluetoothDevice,
+        Connection: connection,
+        FrameInterfaces: frameInterfaces,
+      });
+    });
+  }
+
+  return {
+    BluetoothAdapter: {},
+    BluetoothDevice: {},
+    Connection: {},
+    FrameInterfaces: {},
+    importInterfaces: importInterfaces,
+  };
+});
diff --git a/chrome/browser/ui/webui/bluetooth_internals/bluetooth_internals_ui.cc b/chrome/browser/ui/webui/bluetooth_internals/bluetooth_internals_ui.cc
index b2cea0e..c3085c4 100644
--- a/chrome/browser/ui/webui/bluetooth_internals/bluetooth_internals_ui.cc
+++ b/chrome/browser/ui/webui/bluetooth_internals/bluetooth_internals_ui.cc
@@ -20,6 +20,14 @@
                                IDR_BLUETOOTH_INTERNALS_CSS);
   html_source->AddResourcePath("bluetooth_internals.js",
                                IDR_BLUETOOTH_INTERNALS_JS);
+  html_source->AddResourcePath("device_collection.js",
+                               IDR_BLUETOOTH_INTERNALS_DEVICE_COLLECTION_JS);
+  html_source->AddResourcePath("device_table.js",
+                               IDR_BLUETOOTH_INTERNALS_DEVICE_TABLE_JS);
+  html_source->AddResourcePath("interfaces.js",
+                               IDR_BLUETOOTH_INTERNALS_INTERFACES_JS);
+  html_source->AddResourcePath("adapter_broker.js",
+                               IDR_BLUETOOTH_INTERNALS_ADAPTER_BROKER_JS);
   html_source->AddResourcePath(
       "device/bluetooth/public/interfaces/adapter.mojom",
       IDR_BLUETOOTH_ADAPTER_MOJO_JS);