Home | History | Annotate | Download | only in chromeos
      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  * @typedef {{name: string,
      7  *            address: string,
      8  *            paired: boolean,
      9  *            connected: boolean,
     10  *            connecting: boolean,
     11  *            connectable: boolean,
     12  *            pairing: (string|undefined),
     13  *            passkey: (number|undefined),
     14  *            pincode: (string|undefined),
     15  *            entered: (number|undefined)}}
     16  */
     17 var BluetoothDevice;
     18 
     19 cr.define('options.system.bluetooth', function() {
     20   /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel;
     21   /** @const */ var DeletableItem = options.DeletableItem;
     22   /** @const */ var DeletableItemList = options.DeletableItemList;
     23   /** @const */ var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel;
     24 
     25   /**
     26    * Bluetooth settings constants.
     27    */
     28   function Constants() {}
     29 
     30   /**
     31    * Creates a new bluetooth list item.
     32    * @param {BluetoothDevice} device Description of the Bluetooth device.
     33    * @constructor
     34    * @extends {options.DeletableItem}
     35    */
     36   function BluetoothListItem(device) {
     37     var el = cr.doc.createElement('div');
     38     el.__proto__ = BluetoothListItem.prototype;
     39     el.data = {};
     40     for (var key in device)
     41       el.data[key] = device[key];
     42     el.decorate();
     43     // Only show the close button for paired devices, but not for connecting
     44     // devices.
     45     el.deletable = device.paired && !device.connecting;
     46     return el;
     47   }
     48 
     49   BluetoothListItem.prototype = {
     50     __proto__: DeletableItem.prototype,
     51 
     52     /**
     53      * Description of the Bluetooth device.
     54      * @type {?BluetoothDevice}
     55      */
     56     data: null,
     57 
     58     /** @override */
     59     decorate: function() {
     60       DeletableItem.prototype.decorate.call(this);
     61       var label = this.ownerDocument.createElement('div');
     62       label.className = 'bluetooth-device-label';
     63       this.classList.add('bluetooth-device');
     64       // There are four kinds of devices we want to distinguish:
     65       //  * Connecting devices: in bold with a "connecting" label,
     66       //  * Connected devices: in bold,
     67       //  * Paired, not connected but connectable devices: regular and
     68       //  * Paired, not connected and not connectable devices: grayed out.
     69       this.connected = this.data.connecting ||
     70           (this.data.paired && this.data.connected);
     71       this.notconnectable = this.data.paired && !this.data.connecting &&
     72           !this.data.connected && !this.data.connectable;
     73       // "paired" devices are those that are remembered but not connected.
     74       this.paired = this.data.paired && !this.data.connected &&
     75           this.data.connectable;
     76 
     77       var content = this.data.name;
     78       // Update the device's label according to its state. A "connecting" device
     79       // can be in the process of connecting and pairing, so we check connecting
     80       // first.
     81       if (this.data.connecting) {
     82         content = loadTimeData.getStringF('bluetoothDeviceConnecting',
     83             this.data.name);
     84       }
     85       label.textContent = content;
     86       this.contentElement.appendChild(label);
     87     },
     88   };
     89 
     90   /**
     91    * Class for displaying a list of Bluetooth devices.
     92    * @constructor
     93    * @extends {options.DeletableItemList}
     94    */
     95   var BluetoothDeviceList = cr.ui.define('list');
     96 
     97   BluetoothDeviceList.prototype = {
     98     __proto__: DeletableItemList.prototype,
     99 
    100     /**
    101      * Height of a list entry in px.
    102      * @type {number}
    103      * @private
    104      */
    105     itemHeight_: 32,
    106 
    107     /**
    108      * Width of a list entry in px.
    109      * @type {number}
    110      * @private
    111      */
    112     itemWidth_: 400,
    113 
    114     /** @override */
    115     decorate: function() {
    116       DeletableItemList.prototype.decorate.call(this);
    117       // Force layout of all items even if not in the viewport to address
    118       // errors in scroll positioning when the list is hidden during initial
    119       // layout. The impact on performance should be minimal given that the
    120       // list is not expected to grow very large. Fixed height items are also
    121       // required to avoid caching incorrect sizes during layout of a hidden
    122       // list.
    123       this.autoExpands = true;
    124       this.fixedHeight = true;
    125       this.clear();
    126       this.selectionModel = new ListSingleSelectionModel();
    127     },
    128 
    129     /**
    130      * Adds a bluetooth device to the list of available devices. A check is
    131      * made to see if the device is already in the list, in which case the
    132      * existing device is updated.
    133      * @param {{name: string,
    134      *          address: string,
    135      *          paired: boolean,
    136      *          connected: boolean,
    137      *          connecting: boolean,
    138      *          connectable: boolean,
    139      *          pairing: (string|undefined),
    140      *          passkey: (number|undefined),
    141      *          pincode: (string|undefined),
    142      *          entered: (number|undefined)}} device
    143      *     Description of the bluetooth device.
    144      * @return {boolean} True if the devies was successfully added or updated.
    145      */
    146     appendDevice: function(device) {
    147       var selectedDevice = this.getSelectedDevice_();
    148       var index = this.find(device.address);
    149       if (index == undefined) {
    150         this.dataModel.push(device);
    151         this.redraw();
    152       } else {
    153         this.dataModel.splice(index, 1, device);
    154         this.redrawItem(index);
    155       }
    156       this.updateListVisibility_();
    157       if (selectedDevice)
    158         this.setSelectedDevice_(selectedDevice);
    159       return true;
    160     },
    161 
    162     /**
    163      * Forces a revailidation of the list content. Deleting a single item from
    164      * the list results in a stale cache requiring an invalidation.
    165      * @param {string=} opt_selection Optional address of device to select
    166      *     after refreshing the list.
    167      */
    168     refresh: function(opt_selection) {
    169       // TODO(kevers): Investigate if the stale cache issue can be fixed in
    170       // cr.ui.list.
    171       var selectedDevice = opt_selection ? opt_selection :
    172           this.getSelectedDevice_();
    173       this.invalidate();
    174       this.redraw();
    175       if (selectedDevice)
    176         this.setSelectedDevice_(selectedDevice);
    177     },
    178 
    179     /**
    180      * Retrieves the address of the selected device, or null if no device is
    181      * selected.
    182      * @return {(string|undefined)} Address of selected device or null.
    183      * @private
    184      */
    185     getSelectedDevice_: function() {
    186       var selection = this.selectedItem;
    187       if (selection)
    188         return selection.address;
    189       return undefined;
    190     },
    191 
    192     /**
    193      * Selects the device with the matching address.
    194      * @param {string} address The unique address of the device.
    195      * @private
    196      */
    197     setSelectedDevice_: function(address) {
    198       var index = this.find(address);
    199       if (index != undefined)
    200         this.selectionModel.selectRange(index, index);
    201     },
    202 
    203     /**
    204      * Perges all devices from the list.
    205      */
    206     clear: function() {
    207       this.dataModel = new ArrayDataModel([]);
    208       this.redraw();
    209       this.updateListVisibility_();
    210     },
    211 
    212     /**
    213      * Returns the index of the list entry with the matching address.
    214      * @param {string} address Unique address of the Bluetooth device.
    215      * @return {number|undefined} Index of the matching entry or
    216      * undefined if no match found.
    217      */
    218     find: function(address) {
    219       var size = this.dataModel.length;
    220       for (var i = 0; i < size; i++) {
    221         var entry = this.dataModel.item(i);
    222         if (entry.address == address)
    223           return i;
    224       }
    225     },
    226 
    227     /**
    228      * @override
    229      * @param {BluetoothDevice} entry
    230      */
    231     createItem: function(entry) {
    232       return new BluetoothListItem(entry);
    233     },
    234 
    235     /**
    236      * Overrides the default implementation, which is used to compute the
    237      * size of an element in the list.  The default implementation relies
    238      * on adding a placeholder item to the list and fetching its size and
    239      * position. This strategy does not work if an item is added to the list
    240      * while it is hidden, as the computed metrics will all be zero in that
    241      * case.
    242      * @return {{height: number, marginTop: number, marginBottom: number,
    243      *     width: number, marginLeft: number, marginRight: number}}
    244      *     The height and width of the item, taking margins into account,
    245      *     and the margins themselves.
    246      */
    247     measureItem: function() {
    248       return {
    249         height: this.itemHeight_,
    250         marginTop: 0,
    251         marginBottom: 0,
    252         width: this.itemWidth_,
    253         marginLeft: 0,
    254         marginRight: 0
    255       };
    256     },
    257 
    258     /**
    259      * Override the default implementation to return a predetermined size,
    260      * which in turns allows proper layout of items even if the list is hidden.
    261      * @return {{height: number, width: number}} Dimensions of a single item in
    262      *     the list of bluetooth device.
    263      * @private
    264      */
    265     getDefaultItemSize_: function() {
    266       return {
    267         height: this.itemHeight_,
    268         width: this.itemWidth_
    269       };
    270     },
    271 
    272     /**
    273      * Override base implementation of handleClick, which unconditionally
    274      * removes the item.  In this case, removal of the element is deferred
    275      * pending confirmation from the Bluetooth adapter.
    276      * @param {Event} e The click event object.
    277      * @override
    278      */
    279     handleClick: function(e) {
    280       if (this.disabled)
    281         return;
    282 
    283       var target = /** @type {HTMLElement} */(e.target);
    284       if (!target.classList.contains('row-delete-button'))
    285         return;
    286 
    287       var item = this.getListItemAncestor(target);
    288       var selected = this.selectionModel.selectedIndex;
    289       var index = this.getIndexOfListItem(item);
    290       if (item && item.deletable) {
    291         if (selected != index)
    292           this.setSelectedDevice_(item.data.address);
    293         // Device is busy until we hear back from the Bluetooth adapter.
    294         // Prevent double removal request.
    295         item.deletable = false;
    296         // TODO(kevers): Provide visual feedback that the device is busy.
    297 
    298         // Inform the bluetooth adapter that we are disconnecting or
    299         // forgetting the device.
    300         chrome.send('updateBluetoothDevice',
    301           [item.data.address, item.connected ? 'disconnect' : 'forget']);
    302       }
    303     },
    304 
    305     /** @override */
    306     deleteItemAtIndex: function(index) {
    307       var selectedDevice = this.getSelectedDevice_();
    308       this.dataModel.splice(index, 1);
    309       this.refresh(selectedDevice);
    310       this.updateListVisibility_();
    311     },
    312 
    313     /**
    314      * If the list has an associated empty list placholder then update the
    315      * visibility of the list and placeholder.
    316      * @private
    317      */
    318     updateListVisibility_: function() {
    319       var empty = this.dataModel.length == 0;
    320       var listPlaceHolderID = this.id + '-empty-placeholder';
    321       if ($(listPlaceHolderID)) {
    322         if (this.hidden != empty) {
    323           this.hidden = empty;
    324           $(listPlaceHolderID).hidden = !empty;
    325           this.refresh();
    326         }
    327       }
    328     },
    329   };
    330 
    331   cr.defineProperty(BluetoothListItem, 'connected', cr.PropertyKind.BOOL_ATTR);
    332 
    333   cr.defineProperty(BluetoothListItem, 'paired', cr.PropertyKind.BOOL_ATTR);
    334 
    335   cr.defineProperty(BluetoothListItem, 'connecting', cr.PropertyKind.BOOL_ATTR);
    336 
    337   cr.defineProperty(BluetoothListItem, 'notconnectable',
    338       cr.PropertyKind.BOOL_ATTR);
    339 
    340   return {
    341     BluetoothListItem: BluetoothListItem,
    342     BluetoothDeviceList: BluetoothDeviceList,
    343     Constants: Constants
    344   };
    345 });
    346