Source: modules/bluetooth/bluetooth_connection_manager.js

/**
 * BluetoothConnectionManager:
 * BluetoothConnectionManager only update state and does not involve in any UI
 *   logic.
 *
 * @module BluetoothConnectionManager
 */
define(function(require) {
  'use strict';

  var AdapterManager = require('modules/bluetooth/bluetooth_adapter_manager');
  var AsyncStorage = require('shared/async_storage');

  var _debug = false;
  var Debug = function() {};
  if (_debug) {
    Debug = function btam_debug(msg) {
      console.log('--> [BluetoothConnectionManager]: ' + msg);
    };
  }

  /**
   * @alias module:modules/bluetooth/BluetoothConnectionManager
   * @requires module:modules/bluetooth/bluetooth_adapter_manager
   * @requires module:shared/async_storage
   * @return {BluetoothConnectionManager}
   */
  var BluetoothConnectionManager = {
    /**
     * The profiles of connected device that we are defined here.
     *
     * @public
     * @memberOf BluetoothConnectionManager
     * @type {Object}
     */
    Profiles: {
      'hfp': 0x111E,  // Handsfree
      'a2dp': 0x110D  // Advanced Audio Distribution Devices
    },

    /**
     * The address of device that we are trying to connect.
     *
     * @public
     * @memberOf BluetoothConnectionManager
     * @type {String}
     */
    connectingAddress: null,

    /**
     * A object that we cache the connected devices information(address, device,
     * connectedProfiles). It will be inited while default adapter is ready.
     * And these information is coming from profile events.
     * Each connected device is hashed by device address.
     *
     * EX:
     * _connectedDevicesInfo = {
     *   'AA:BB:CC:00:11:22': {
     *     'device': DeviceObject,
     *     'connectedProfiles': {
     *       'hfp': true,
     *       'a2dp': false
     *     }
     *   }
     * };
     *
     * @private
     * @memberOf BluetoothConnectionManager
     * @type {Object}
     */
    _connectedDevicesInfo: {},

    /**
     * An instance to maintain that we have created a promise to get connected
     * devices.
     *
     * @access public
     * @memberOf BluetoothConnectionManager
     * @return {Promise}
     */
     _getConnectedDevicesPromise: null,

    /**
     * The object maintains listeners' callback per property name.
     * Each listener would be called as following definition.
     * 'connecting' - be called when device is connecting.
     * 'connected': - be called when device is connected.
     * 'disconnected': - be called when device is disconnected.
     * 'profileChanged': - be called when profile is changed.
     *
     * @memberOf BluetoothConnectionManager
     * @type {Object}
     */
    _listeners: {
      'connecting': [],
      'connected': [],
      'disconnected': [],
      'profileChanged': []
    },

    /**
     * Default adapter of Bluetooth.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @type {BluetoothAdapter}
     */
    _defaultAdapter: null,

    /**
     * Init BluetoothConnectionManager module.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     */
    _init: function btcm__init() {
      // Observe 'defaultAdapter' property for reaching default adapter.
      AdapterManager.observe('defaultAdapter',
                             this._onDefaultAdapterChanged.bind(this));
      this._onDefaultAdapterChanged(AdapterManager.defaultAdapter);
    },

    /**
     * 'defaultAdapter' change event handler from adapter manager for
     * updating adapter immediately.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @param {BluetoothAdapter} newAdapter
     * @param {BluetoothAdapter} oldAdapter
     */
    _onDefaultAdapterChanged:
    function btcm__onDefaultAdapterChanged(newAdapter, oldAdapter) {
      Debug('_onDefaultAdapterChanged(): newAdapter = ' + newAdapter);
      Debug('_onDefaultAdapterChanged(): oldAdapter = ' + oldAdapter);

      // save default adapter
      this._defaultAdapter = newAdapter;

      if (oldAdapter) {
        // unwatch event since the old adapter is no longer usefull
        this._unwatchProfilesStatuschanged(oldAdapter);
        this._unwatchDefaultAdapterOnattributechanged(oldAdapter);
      }

      if (newAdapter) {
        // watch event since the new adapter is ready to access
        this._watchProfilesStatuschanged(newAdapter);
        this._watchDefaultAdapterOnattributechanged(newAdapter);
        // restore connection
        if (newAdapter.state === 'enabled') {
          this._restoreConnection();
        }
      } else {
        // reset properties only
        this._resetConnectionInfo();
      }
    },

    /**
     * Return the cache of connected devices in ConnectionManager.
     *
     * @access public
     * @memberOf BluetoothConnectionManager
     * @return {Promise}
     */
    getConnectedDevices: function btcm_getConnectedDevices() {
      if (!this._getConnectedDevicesPromise) {
        this._getConnectedDevicesPromise =
          this._initConnectedDevicesInfo().then(() => {
            Debug('getConnectedDevices(): resolved with latest cache = ' +
                  JSON.stringify(this._connectedDevicesInfo));
            return this._connectedDevicesInfo;
        }, (reason) => {
          Debug('getConnectedDevices(): rejected with reason = ' + reason);
          this._getConnectedDevicesPromise = null;
        });
      }
      return this._getConnectedDevicesPromise;
    },

    /**
     * Init cache of connected device and save it in cache.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @return {Promise}
     */
    _initConnectedDevicesInfo: function btcm__initConnectedDevicesInfo() {
      if (!this._defaultAdapter) {
        return Promise.reject('default adapter is not existed!!');
      }
      // Init connection status and profile from platform.
      // Then, save these connected device information in cache.
      return this._getConnectedDevicesFromPlatform().then(
      (connectedDevicesByProfile) => {
        this._constructDeviceItemsMap(connectedDevicesByProfile);
      }, (reason) => {
        Debug('_initConnectedDevicesInfo(): rejected in ' + 
              '_getConnectedDevicesFromPlatform');
        return Promise.reject(reason);
      });
    },

    /**
     * The method will update each device item in maintaining map.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @param {Object} connectedDevices
     */
    _constructDeviceItemsMap:
    function btcm__constructDeviceItemsMap(connectedDevices) {
      Debug('_constructDeviceItemsMap(): connectedDevices = ' + 
            JSON.stringify(connectedDevices));
      if (!connectedDevices) {
        // Return empty object while there is no any connected devices.
        Debug('_constructDeviceItemsMap(): early return with empty object');
        return;
      }
      
      Object.keys(this.Profiles).map((profileID) => {
        connectedDevices[profileID].forEach((connectedDevice) => {
          var connectionDeviceInfo = {
            address: connectedDevice.address,
            connected: true,
            profileID: profileID,
            device: connectedDevice
          };
          Debug('_constructDeviceItemsMap(): connectionDeviceInfo = ' + 
                JSON.stringify(connectionDeviceInfo));
          this._initConnectedDevicesCache(connectionDeviceInfo);
        });
      });
    },

    /**
     * Init the cache which is maintained for connection devices.
     * And the input is gotten from platform API adapter.getConnectedDevices().
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @param {Object} options
     * @param {String} options.address - address of the device
     * @param {Boolean} options.connected - is connected or not
     * @param {Object} options.profileID - profile ID of the connection type
     * @param {Object} options.device - connect device, Bluetooth Object
     */
    _initConnectedDevicesCache: 
    function btcm__initConnectedDevicesCache(options) {
      Debug('_initConnectedDevicesCache(): options = ' + 
            JSON.stringify(options));
      // hash by device address
      var info = this._connectedDevicesInfo[options.address];
      if (info) {
        // Already have profiles, update it for other profile.
        info.connectedProfiles[options.profileID] = options.connected;
      } else {
        // Not have profiles yet, create for it.
        // If options.device is existed, save the connected device.
        // Otherwise, given null in this property.
        var connectedDevice = (options.device) ? options.device : null;
        info = {
          'device': connectedDevice, 
          'connectedProfiles': {}
        };
        info.connectedProfiles[options.profileID] = options.connected;
      }
      // Save the device/profile in map.
      this._connectedDevicesInfo[options.address] = info;

      // If there is no profile connected, 
      // remove the device item from cache since it is already disconnected.
      var dataToCheckConnectedProfile = {
        address: options.address,
        connectedDevices: this._connectedDevicesInfo
      };
      if (!this._hasConnectedProfileByAddress(dataToCheckConnectedProfile)) {
        delete this._connectedDevicesInfo[options.address];
      }
      // Return the latest cache which is just updated here.
      Debug('_initConnectedDevicesCache(): this._connectedDevicesInfo = ' + 
            JSON.stringify(this._connectedDevicesInfo));
      return this._connectedDevicesInfo;
    },

    /**
     * Update the cache which is maintained for connection devices.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @param {Object} options
     * @param {String} options.address - address of the device
     * @param {Boolean} options.connected - is connected or not
     * @param {Object} options.profileID - profile ID of the connection type
     * @param {Object} options.device - connect device, Bluetooth Object
     */
    _updateConnectedDevices: 
    function btcm__updateConnectedDevices(options) {
      return this.getConnectedDevices().then((connectedDevicesInfo) => {
        Debug('_updateConnectedDevices(): connectedDevicesInfo = ' + 
              JSON.stringify(connectedDevicesInfo));

        // hash by device address
        var info = 
          (connectedDevicesInfo) ? connectedDevicesInfo[options.address] : null;
        if (info) {
          // Already have profiles, update it for other profile.
          info.connectedProfiles[options.profileID] = options.connected;
        } else {
          // Not have profiles yet, create for it.
          // If options.device is existed, save the connected device.
          // Otherwise, given null in this property.
          var connectedDevice = (options.device) ? options.device : null;
          info = {
            'device': connectedDevice, 
            'connectedProfiles': {}
          };
          info.connectedProfiles[options.profileID] = options.connected;
        }
        // Save the device/profile in map.
        this._connectedDevicesInfo[options.address] = info;

        // If there is no profile connected, 
        // remove the device item from cache since it is already disconnected.
        var dataToCheckConnectedProfile = {
          address: options.address,
          connectedDevices: this._connectedDevicesInfo
        };
        if (!this._hasConnectedProfileByAddress(dataToCheckConnectedProfile)) {
          delete this._connectedDevicesInfo[options.address];
        }
        // Return the latest cache which is just updated here.
        Debug('_updateConnectedDevices(): this._connectedDevicesInfo = ' + 
              JSON.stringify(this._connectedDevicesInfo));
        return Promise.resolve(this._connectedDevicesInfo);
      }, () => {
        Debug('_updateConnectedDevices(): rejected with some exception');
        return Promise.reject('rejected with some exception');
      });
    },

    /**
     * Only reset properties since there is no available default adapter.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     */
    _resetConnectionInfo: function btcm__resetConnectionInfo() {
      // Reset connection status.
      this.connectingAddress = null;
      // Clean up the instance to get connected devices 
      // while new adapter is ready.
      this._getConnectedDevicesPromise = null;
    },

    /**
     * Watch 'onattributechanged' event from default adapter for watching
     * adapter enabled/disabled status.
     *
     * Description of 'onattributechanged' event:
     * A handler to trigger when one of the local bluetooth adapter's properties
     * has changed. Note access to the changed property in this event handler
     * would get the updated value.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @param {BluetoothAdapter} adapter
     */
    _watchDefaultAdapterOnattributechanged:
    function btcm__watchDefaultAdapterOnattributechanged(adapter) {
      adapter.addEventListener('attributechanged', 
        this._onAdapterAttributeChanged.bind(this, adapter));
    },

    /**
     * Unwatch 'onattributechanged' event from default adapter since adapter is
     * removed.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @param {BluetoothAdapter} adapter
     */
    _unwatchDefaultAdapterOnattributechanged:
    function btcm__unwatchDefaultAdapterOnattributechanged(adapter) {
      adapter.removeEventListener('attributechanged', 
        this._onAdapterAttributeChanged);
    },

    /**
     * 'onattributechanged' event handler from default adapter for reaching
     * adapter enabled/disabled status.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @param {BluetoothAdapter} adapter
     * @param {event} evt
     */
    _onAdapterAttributeChanged:
    function btcm__onAdapterAttributeChanged(adapter, evt) {
      for (var i in evt.attrs) {
        Debug('_onAdapterAttributeChanged(): ' + evt.attrs[i]);
        switch (evt.attrs[i]) {
          case 'state':
            if (adapter.state === 'enabled') {
              // Restore connection while default adapter state is enabled.
              this._restoreConnection();
            }
            break;
          default:
            break;
        }
      }
    },

    /**
     * Watch every of profile events('onhfpstatuschanged','ona2dpstatuschanged')
     * from default adapter for updating device connected status immediately.
     *
     * Description of 'onhfpstatuschanged' event:
     * Specifies an event listener to receive hfpstatuschanged events.
     * Those events occur when an HFP connection status changes.
     *
     * Description of 'ona2dpstatuschanged' event:
     * Specifies an event listener to receive a2dpstatuschanged events.
     * Those events occur when an A2DP connection status changes.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @param {BluetoothAdapter} adapter
     */
    _watchProfilesStatuschanged: 
    function btcm__watchProfilesStatuschanged(adapter) {
      var eventName;
      for (var profileID in this.Profiles) {
        eventName = 'on' + profileID + 'statuschanged';
        adapter[eventName] = 
          this._onProfileStatuschangeHandler.bind(this, profileID);
      }
    },

    /**
     * Unwatch every of profile events('onhfpstatuschanged', 
     * 'ona2dpstatuschanged') from default adapter since adapter is removed.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @param {BluetoothAdapter} adapter
     */
    _unwatchProfilesStatuschanged:
    function btc__unwatchProfilesStatuschanged(adapter) {
      var eventName;
      for (var profileID in this.Profiles) {
        eventName = 'on' + profileID + 'statuschanged';
        adapter[eventName] = null;
      }
    },

    /**
     * 'onhfpstatuschanged', 'ona2dpstatuschanged' events handler from 
     * default adapter for updating device connected status.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @param {String} profileID
     * @param {event} evt
     */
    _onProfileStatuschangeHandler: 
    function btcm__onProfileStatuschangeHandler(profileID, evt) {
      Debug('_onProfileStatuschangeHandler(): profileID = ' + profileID + 
            ', evt.address = ' + evt.address + ', evt.status = ' + evt.status);
      var options = {
        address: evt.address,
        connected: evt.status,
        profileID: profileID
      };
      // Update connection status.
      this._updateConnectionStatus(options);
    },

    /**
     * Query async storage to restore connection.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     */
    _restoreConnection: function btcm__restoreConnection() {
      if (!this._defaultAdapter) {
        return;
      }

      // Reconnect the one kept in the async storage.
      AsyncStorage.getItem('device.connected', (address) => {
        if (!address) {
          return;
        }

        // Make sure the restore device is already connected or not.
        this.getConnectedDevices().then((connectedDevices) => {
          if (connectedDevices && connectedDevices[address]) {
            // Do an early return since the restore device is connected already.
            Debug('_restoreConnection(): early return cause connected already');
            return;
          }

          // Get the device which is wanted to connect from paired devices.
          var restoreDevice = this._getPairedDeviceByAddress(address);
          if (restoreDevice) {
            this.connectingAddress = restoreDevice.address;
            // Fire 'connecting' event.
            var event = {
              type: 'connecting',
              detail: {
                address: restoreDevice.address  
              }
            };
            this._emitEvent(event);
            this._connect(restoreDevice).then(() => {
              Debug('_restoreConnection(): restore connection successfully');
            }, (reason) => {
              Debug('_restoreConnection(): restore connection failed, ' +
                    'reason = ' + reason);
              // No available profiles are connected. Reset connecting address. 
              this.connectingAddress = null;
              // Then, fire 'disconnected' event.
              event = {
                type: 'disconnected',
                detail: {
                  address: restoreDevice.address  
                }
              };
              this._emitEvent(event);
            });
          }
        });
      });
    },

    /**
     * Record connected device so if Bluetooth is turned off and then on 
     * we can restore the connection.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @param {String} action - to set or remove item for recording connection
     * @param {String} address - the address of connected device
     */
    _recordConnection: function btcm__recordConnection(action, address) {
      if (action === 'set') {
        // record connected device so if Bluetooth is turned off and then on
        // we can restore the connection
        AsyncStorage.setItem('device.connected', address);
        Debug('_recordConnection(): set item');
      } else if ((action === 'remove') && 
                 (this._defaultAdapter.state === 'enabled')) {
        // Clean up the connected device from async storage
        // which is recorded before.
        // Only remove the record while Bluetooth state is enabled.
        // Because the request also comes while Bluetooth is turned off.
        AsyncStorage.removeItem('device.connected');
        Debug('_recordConnection(): remove item');
      }
    },

    /**
     * Update connection status.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @param {Object} options
     * @param {String} options.address - address of the device
     * @param {Boolean} options.connected - is connected or not
     * @param {Object} options.profileID - profile ID of the connection type
     */
    _updateConnectionStatus: function btcm___updateConnectionStatus(options) {
      Debug('_updateConnectionStatus(): address = ' + options.address + 
                                       ', connected = ' + options.connected +
                                       ', profileID = ' + options.profileID);
      this.connectingAddress = null;
      
      // Update the profile in the cache of connected device info.
      this._updateConnectedDevices(options).then((connectedDevicesInfo) => {
        Debug('_updateConnectionStatus(): _updateConnectedDevices() ' + 
              'resolved with connectedDevicesInfo = ' + 
              JSON.stringify(connectedDevicesInfo));
        // Prepare latest data to check connected profile.
        var dataToCheckConnectedProfile = {
          address: options.address,
          connectedDevices: connectedDevicesInfo
        };
        // Fire 'connected'/'disconnected' event according to 
        // the connection profile. Then, record connection.
        var event;
        if (options.connected) {
          // Fire 'connected' event.
          event = {
            type: 'connected',
            detail: {
              address: options.address
            }
          };
          this._emitEvent(event);
          // Record connection.
          this._recordConnection('set', options.address);
        } else {
          // If there is no profile connected, 
          // we have to remove the record connection.
          // And fire 'disconnected' event for outer modules.
          if (!this._hasConnectedProfileByAddress(
              dataToCheckConnectedProfile)) {
            // Record connection. Only remove the record while Bluetooth state 
            // is enabled. Because the event also comes while Bluetooth is 
            // turned off.
            if (this._defaultAdapter.state === 'enabled') {
              this._recordConnection('remove');  
            }
            // Fire 'disconnected' event.
            event = {
              type: 'disconnected',
              detail: {
                address: options.address
              }
            };
            this._emitEvent(event);
          }
        }

        // Fire 'profileChanged' event.
        var newProfiles;
        if (!this._hasConnectedProfileByAddress(dataToCheckConnectedProfile)) {
          newProfiles = null;
        } else {
          newProfiles = connectedDevicesInfo[options.address].connectedProfiles;
        }
        event = {
          type: 'profileChanged',
          detail: {
            address: options.address,
            profiles: newProfiles
          }
        };
        this._emitEvent(event);
      }, (reason) => {
        Debug('_updateConnectionStatus(): miss to update in rejected case, ' + 
              'reason = ' + reason);
      });
    },

    /**
     * It provides a convenient function for panel to connect with a device.
     * And the panel no need to care about connected device currently.
     * The method will disconnect current connected device first.
     * Then, it will start connecting a paired device with the device's adapter.
     *
     * @access public
     * @memberOf BluetoothConnectionManager
     * @param {BluetoothDevice} device
     * @return {Promise}
     */
    connect: function btcm_connect(device) {
      if (!this._defaultAdapter) {
        return Promise.reject('default adapter is not existed!!');
      }

      // Disconnect current connected device first.
      Debug('connect(): Want to connect device address = ' + device.address +
            ', name = ' + device.name);

      var connectedDevices = [];
      return this.getConnectedDevices().then((connectedDevicesInfo) => {
        for (var address in connectedDevicesInfo) {
          if (connectedDevicesInfo[address].device !== null) {
            connectedDevices.push(connectedDevicesInfo[address].device);
            Debug('connect(): push device cache in queue = ' + 
                  JSON.stringify(connectedDevices));
          } else {
            var regetPairedDevice = this._getPairedDeviceByAddress(address);
            connectedDevices.push(regetPairedDevice);
            Debug('connect(): push _getPairedDeviceByAddress in queue = ' + 
                  JSON.stringify(regetPairedDevice));
          }
        }

        Debug('connect(): Will disconnect these connected devices = ' + 
              JSON.stringify(connectedDevices));

        // Disconnect these connected device before 
        // service to connect with new device.
        return Promise.all(connectedDevices.map((connectedDevice) => {
          return this.disconnect(connectedDevice);
        })).then(() => {
          // All connected devices is disconnected. 
          // We can start to connect the new request.
          Debug('connect(): Start to connect with wanted device address = ' + 
                device.address);
          return this._connect(device).then(() => {
            Debug('connect(): Resolved');
          }, (reason) => {
            Debug('connect(): reason = ' + reason);
            return Promise.reject(reason);
          });
        });
      });
    },

    /**
     * The method will connect the input device with the device's adapter.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @param {BluetoothDevice} device
     * @returns {Promise}
     */
    _connect: function btcm__connect(device) {
      if (!this._defaultAdapter) {
        return Promise.reject('default adapter is not existed!!');
      }

      // Save the connecting address
      this.connectingAddress = device.address;
      // Fire 'connecting' event.
      var event = {
        type: 'connecting',
        detail: {
          address: device.address
        }
      };
      this._emitEvent(event);
      Debug('_connect(): before start to connect, stop discovery first');

      // Note on Bluedroid stack, discovery has to be stopped before connect
      // (i.e., call stopDiscovery() before connect()) 
      // otherwise stack callbacks with connect failure.
      return this._defaultAdapter.stopDiscovery().then(() => {
        Debug('_connect(): start connecting device, ' + 
              'address = ' + device.address);
        return this._defaultAdapter.connect(device).then(() => {
          Debug('_connect(): resolve, already connected with address = ' + 
                device.address);
        }, () => {
          // No available profiles are connected. Reset connecting address. 
          this.connectingAddress = null;
          // Fire 'disconnected' event.
          event = {
            type: 'disconnected',
            detail: {
              address: device.address
            }
          };
          this._emitEvent(event);
          Debug('_connect(): reject with connection failed');
          return Promise.reject('connection failed');
        });
      }, () => {
        // Cannot connect with the device since stopDiscovery failed. 
        this.connectingAddress = null;
        // Fire 'disconnected' event.
        event = {
          type: 'disconnected',
          detail: {
            address: device.address
          }
        };
        this._emitEvent(event);
        Debug('_connect(): reject with stop discovery failed');
        return Promise.reject('stop discovery failed');
      });
    },

    /**
     * The method will disconnect the input device with the device's adapter.
     *
     * @access public
     * @memberOf BluetoothConnectionManager
     * @param {BluetoothDevice} device
     * @returns {Promise}
     */
    disconnect: function btcm_disconnect(device) {
      if (!this._defaultAdapter) {
        return Promise.reject('default adapter is not existed!!');
      }

      return this._defaultAdapter.disconnect(device).then(() => {
        Debug('disconnect(): onsuccess(): resolve');
      }, () => {
        Debug('disconnect(): onerror(): reject with disconnect failed');
        return Promise.reject('disconnect failed');
      });
    },

    /**
     * The method will get all connected devices profiles 
     * which we are interested in. Profile: HFP, A2DP.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @returns {Promise} resolve: connectedDevices
     * @returns {Promise} reject: reason
     */
    _getConnectedDevicesFromPlatform: 
    function btcm__getConnectedDevicesFromPlatform() {
      // Get connected device via profiles HFP, A2DP
      return Promise.all(Object.keys(this.Profiles).map((profile) => {
        return this._getConnectedDevicesByProfile(this.Profiles[profile]);
      })).then((connectedDevices) => {
        // Update each connected devices in map.
        var collectedConnectedDevicesByProfile = {};
        Object.keys(this.Profiles).forEach((profile, index) => {
          collectedConnectedDevicesByProfile[profile] = connectedDevices[index];
        });
        Debug('_getConnectedDevicesFromPlatform(): ' + 
              'collectedConnectedDevicesByProfile = ' + 
              JSON.stringify(collectedConnectedDevicesByProfile));
        return Promise.resolve(collectedConnectedDevicesByProfile);
      });
    },

    /**
     * The method will get connected device by inputed profile.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @param {String} profileID
     * @returns {Promise} resolve: the connected devices in array
     * @returns {Promise} reject: reason
     */
    _getConnectedDevicesByProfile: 
    function btcm__getConnectedDevicesByProfile(profileID) {
      if (!this._defaultAdapter) {
        Debug('_getConnectedDevicesByProfile(): reject with no adapter');
        return Promise.reject('default adapter is not existed!!');
      }

      if (this._defaultAdapter.state === 'disabled') {
        Debug('_getConnectedDevicesByProfile(): resolve with empty array ' + 
              'since it is impossible to connect with any device');
        return Promise.reject('getConnectedDevices in disabled state');
      }

      return this._defaultAdapter.getConnectedDevices(profileID).then(
      (connectedDevice) => {
        Debug('_getConnectedDevicesByProfile(): resolved with ' + 
              'connectedDevice = ' + JSON.stringify(connectedDevice));
        return Promise.resolve(connectedDevice || []);
      }, (reason) => {
        Debug('_getConnectedDevicesByProfile(): rejected with ' + 
              'reason = ' + reason);
        return Promise.reject(reason);
      });
    },

    /**
     * Get device from paired devices by address.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @param {String} address
     */
    _getPairedDeviceByAddress: 
    function btcm__getPairedDeviceByAddress(address) {
      if (!this._defaultAdapter) {
        return null;
      }

      var pairedDevices = this._defaultAdapter.getPairedDevices();
      if (pairedDevices.length === 0) {
        return null;
      } else {
        for (var i in pairedDevices) {
          if (pairedDevices[i].address === address) {
            return pairedDevices[i];
          }
        }
        return null;
      }
    },

    /**
     * Find out there is any profile still connected.
     *
     * @access private
     * @memberOf BluetoothConnectionManager
     * @param {Object} options
     * @param {String} options.connectedDevices - connected devices info
     * @param {String} options.address - the address of device
     */
    _hasConnectedProfileByAddress: 
    function btcm__hasConnectedProfileByAddress(options) {
      if (!options.connectedDevices[options.address]) {
        return false;
      }

      var hasConnectedProfile = false;
      for (var profileID in this.Profiles) {
        var connectedProfiles = 
          options.connectedDevices[options.address].connectedProfiles;
        if (connectedProfiles && (connectedProfiles[profileID] === true)) {
          hasConnectedProfile = true;
        }
      }
      return hasConnectedProfile;
    },

    /**
     * A function to emit event to each registered listener by the input type.
     *
     * @memberOf BluetoothConnectionManager
     * @param {Object} options
     * @param {String} options.type - type of event name
     * @param {Object} options.detail - the object pass to the listener
     */
    _emitEvent: function btcm__emitEvent(options) {
      this._listeners[options.type].forEach(function(listener) {
        listener(options);
      });
    },

    /**
     * The method will provide event listener for outer modules to regist.
     *
     * @access public
     * @memberOf BluetoothConnectionManager
     * @param {String} eventName
     * @param {Function} callback
     */
    addEventListener: function btcm_addEventListener(eventName, callback) {
      if (callback && (this._listeners.hasOwnProperty(eventName))) {
        this._listeners[eventName].push(callback);
      }
    },

    /**
     * The method will provide event listener for outer modules to un-regist.
     *
     * @access public
     * @memberOf BluetoothConnectionManager
     * @param {String} eventName
     * @param {Function} callback
     */
    removeEventListener: 
    function btcm_removeEventListener(eventName, callback) {
      if (callback && (this._listeners.hasOwnProperty(eventName))) {
        var index = this._listeners[eventName].indexOf(callback);
        if (index >= 0) {
          this._listeners[eventName].splice(index, 1);
        }
      }
    }
  };

  BluetoothConnectionManager._init();
  return BluetoothConnectionManager;
});