• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.googlecode.android_scripting.facade.bluetooth;
18 
19 import android.app.Service;
20 import android.bluetooth.BluetoothA2dp;
21 import android.bluetooth.BluetoothA2dpSink;
22 import android.bluetooth.BluetoothAdapter;
23 import android.bluetooth.BluetoothAdapter.OobDataCallback;
24 import android.bluetooth.BluetoothDevice;
25 import android.bluetooth.BluetoothHeadset;
26 import android.bluetooth.BluetoothHeadsetClient;
27 import android.bluetooth.BluetoothHidDevice;
28 import android.bluetooth.BluetoothHidHost;
29 import android.bluetooth.BluetoothManager;
30 import android.bluetooth.BluetoothMap;
31 import android.bluetooth.BluetoothMapClient;
32 import android.bluetooth.BluetoothPan;
33 import android.bluetooth.BluetoothPbapClient;
34 import android.bluetooth.BluetoothProfile;
35 import android.bluetooth.BluetoothUuid;
36 import android.bluetooth.OobData;
37 import android.content.BroadcastReceiver;
38 import android.content.Context;
39 import android.content.Intent;
40 import android.content.IntentFilter;
41 import android.os.Bundle;
42 import android.os.ParcelUuid;
43 
44 import com.googlecode.android_scripting.Log;
45 import com.googlecode.android_scripting.facade.EventFacade;
46 import com.googlecode.android_scripting.facade.FacadeManager;
47 import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
48 import com.googlecode.android_scripting.rpc.Rpc;
49 import com.googlecode.android_scripting.rpc.RpcDefault;
50 import com.googlecode.android_scripting.rpc.RpcOptional;
51 import com.googlecode.android_scripting.rpc.RpcParameter;
52 
53 import org.json.JSONArray;
54 import org.json.JSONException;
55 
56 import java.util.ArrayList;
57 import java.util.Arrays;
58 import java.util.Collections;
59 import java.util.HashMap;
60 import java.util.List;
61 import java.util.Map;
62 
63 public class BluetoothConnectionFacade extends RpcReceiver {
64 
65     private final Service mService;
66     private final Context mContext;
67     private final BluetoothAdapter mBluetoothAdapter;
68     private final BluetoothManager mBluetoothManager;
69     private final BluetoothPairingHelper mPairingHelper;
70     private final Map<String, BroadcastReceiver> listeningDevices;
71     private final EventFacade mEventFacade;
72     private final OobDataCallback mGenerateOobDataCallback = new OobDataCallback() {
73             @Override
74             public void onError(int error) {
75                 Log.d("onError: " + error);
76                 Bundle results = new Bundle();
77                 results.putInt("Error", error);
78                 mEventFacade.postEvent("ErrorOobData", results.clone());
79             }
80 
81             @Override
82             public void onOobData(int transport, OobData data) {
83                 Log.d("Transport: " + transport);
84                 Log.d("OobData: " + data);
85                 Bundle results = new Bundle();
86                 results.putInt("transport", transport);
87                 // Just what we need create a bond
88                 results.putString("address_with_type",
89                         toHexString(data.getDeviceAddressWithType()));
90                 results.putString("confirmation", toHexString(data.getConfirmationHash()));
91                 results.putString("randomizer", toHexString(data.getRandomizerHash()));
92                 mEventFacade.postEvent("GeneratedOobData", results.clone());
93             }
94         };
95 
96     private final IntentFilter mDiscoverConnectFilter;
97     private final IntentFilter mPairingFilter;
98     private final IntentFilter mBondFilter;
99     private final IntentFilter mA2dpStateChangeFilter;
100     private final IntentFilter mA2dpSinkStateChangeFilter;
101     private final IntentFilter mHidStateChangeFilter;
102     private final IntentFilter mHidDeviceStateChangeFilter;
103     private final IntentFilter mHspStateChangeFilter;
104     private final IntentFilter mHfpClientStateChangeFilter;
105     private final IntentFilter mPbapClientStateChangeFilter;
106     private final IntentFilter mPanStateChangeFilter;
107     private final IntentFilter mMapClientStateChangeFilter;
108     private final IntentFilter mMapStateChangeFilter;
109 
110     private final Bundle mGoodNews;
111     private final Bundle mBadNews;
112 
113     private BluetoothA2dpFacade mA2dpProfile;
114     private BluetoothA2dpSinkFacade mA2dpSinkProfile;
115     private BluetoothHidFacade mHidProfile;
116     private BluetoothHidDeviceFacade mHidDeviceProfile;
117     private BluetoothHspFacade mHspProfile;
118     private BluetoothHfpClientFacade mHfpClientProfile;
119     private BluetoothPbapClientFacade mPbapClientProfile;
120     private BluetoothPanFacade mPanProfile;
121     private BluetoothMapClientFacade mMapClientProfile;
122     private BluetoothMapFacade mMapProfile;
123     private ArrayList<String> mDeviceMonitorList;
124 
BluetoothConnectionFacade(FacadeManager manager)125     public BluetoothConnectionFacade(FacadeManager manager) {
126         super(manager);
127         mService = manager.getService();
128         mContext = mService.getApplicationContext();
129         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
130         mBluetoothManager = (BluetoothManager) mContext.getSystemService(
131                 Service.BLUETOOTH_SERVICE);
132         mDeviceMonitorList = new ArrayList<String>();
133         // Use a synchronized map to avoid racing problems
134         listeningDevices = Collections.synchronizedMap(new HashMap<String, BroadcastReceiver>());
135 
136         mEventFacade = manager.getReceiver(EventFacade.class);
137         mPairingHelper = new BluetoothPairingHelper(mEventFacade);
138         mA2dpProfile = manager.getReceiver(BluetoothA2dpFacade.class);
139         mA2dpSinkProfile = manager.getReceiver(BluetoothA2dpSinkFacade.class);
140         mHidProfile = manager.getReceiver(BluetoothHidFacade.class);
141         mHidDeviceProfile = manager.getReceiver(BluetoothHidDeviceFacade.class);
142         mHspProfile = manager.getReceiver(BluetoothHspFacade.class);
143         mHfpClientProfile = manager.getReceiver(BluetoothHfpClientFacade.class);
144         mPbapClientProfile = manager.getReceiver(BluetoothPbapClientFacade.class);
145         mPanProfile = manager.getReceiver(BluetoothPanFacade.class);
146         mMapClientProfile = manager.getReceiver(BluetoothMapClientFacade.class);
147         mMapProfile = manager.getReceiver(BluetoothMapFacade.class);
148 
149         mDiscoverConnectFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
150         mDiscoverConnectFilter.addAction(BluetoothDevice.ACTION_UUID);
151         mDiscoverConnectFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
152 
153         mPairingFilter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST);
154         mPairingFilter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST);
155         mPairingFilter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY);
156         mPairingFilter.setPriority(999);
157 
158         mBondFilter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
159         mBondFilter.addAction(BluetoothDevice.ACTION_FOUND);
160         mBondFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
161 
162         mA2dpStateChangeFilter = new IntentFilter(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
163         mA2dpSinkStateChangeFilter =
164             new IntentFilter(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED);
165         mHidStateChangeFilter =
166             new IntentFilter(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED);
167         mHidDeviceStateChangeFilter =
168                 new IntentFilter(BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED);
169         mHspStateChangeFilter = new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
170         mHfpClientStateChangeFilter =
171             new IntentFilter(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
172         mPbapClientStateChangeFilter =
173             new IntentFilter(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED);
174         mPanStateChangeFilter =
175             new IntentFilter(BluetoothPan.ACTION_CONNECTION_STATE_CHANGED);
176         mMapClientStateChangeFilter =
177             new IntentFilter(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED);
178         mMapStateChangeFilter =
179             new IntentFilter(BluetoothMap.ACTION_CONNECTION_STATE_CHANGED);
180 
181         mGoodNews = new Bundle();
182         mGoodNews.putBoolean("Status", true);
183         mBadNews = new Bundle();
184         mBadNews.putBoolean("Status", false);
185     }
186 
unregisterCachedListener(String listenerId)187     private void unregisterCachedListener(String listenerId) {
188         BroadcastReceiver listener = listeningDevices.remove(listenerId);
189         if (listener != null) {
190             mService.unregisterReceiver(listener);
191         }
192     }
193 
194     /**
195      * Connect to a specific device upon its discovery
196      */
197     public class DiscoverConnectReceiver extends BroadcastReceiver {
198         private final String mDeviceID;
199         private BluetoothDevice mDevice;
200 
201         /**
202          * Constructor
203          *
204          * @param deviceID Either the device alias name or mac address.
205          * @param bond     If true, bond the device only.
206          */
DiscoverConnectReceiver(String deviceID)207         public DiscoverConnectReceiver(String deviceID) {
208             super();
209             mDeviceID = deviceID;
210         }
211 
212         @Override
onReceive(Context context, Intent intent)213         public void onReceive(Context context, Intent intent) {
214             String action = intent.getAction();
215             // The specified device is found.
216             if (action.equals(BluetoothDevice.ACTION_FOUND)) {
217                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
218                 if (BluetoothFacade.deviceMatch(device, mDeviceID)) {
219                     Log.d("Found device " + device.getAlias() + " for connection.");
220                     mBluetoothAdapter.cancelDiscovery();
221                     mDevice = device;
222                 }
223                 // After discovery stops.
224             } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {
225                 if (mDevice == null) {
226                     Log.d("Device " + mDeviceID + " not discovered.");
227                     mEventFacade.postEvent("Bond" + mDeviceID, mBadNews);
228                     return;
229                 }
230                 boolean status = mDevice.fetchUuidsWithSdp();
231                 Log.d("Initiated ACL connection: " + status);
232             } else if (action.equals(BluetoothDevice.ACTION_UUID)) {
233                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
234                 if (BluetoothFacade.deviceMatch(device, mDeviceID)) {
235                     Log.d("Initiating connections.");
236                     connectProfile(device, mDeviceID);
237                     mService.unregisterReceiver(listeningDevices.remove("Connect" + mDeviceID));
238                 }
239             }
240         }
241     }
242 
243     /**
244      * Connect to a specific device upon its discovery
245      */
246     public class DiscoverBondReceiver extends BroadcastReceiver {
247         private final String mDeviceID;
248         private BluetoothDevice mDevice = null;
249         private boolean started = false;
250 
251         /**
252          * Constructor
253          *
254          * @param deviceID Either the device alias name or Mac address.
255          */
DiscoverBondReceiver(String deviceID)256         public DiscoverBondReceiver(String deviceID) {
257             super();
258             mDeviceID = deviceID;
259         }
260 
261         @Override
onReceive(Context context, Intent intent)262         public void onReceive(Context context, Intent intent) {
263             String action = intent.getAction();
264             // The specified device is found.
265             if (action.equals(BluetoothDevice.ACTION_FOUND)) {
266                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
267                 if (BluetoothFacade.deviceMatch(device, mDeviceID)) {
268                     Log.d("Found device " + device.getAlias() + " for connection.");
269                     mEventFacade.postEvent("Discovery" + mDeviceID, mGoodNews);
270                     mBluetoothAdapter.cancelDiscovery();
271                     mDevice = device;
272                 }
273                 // After discovery stops.
274             } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {
275                 if (mDevice == null) {
276                     Log.d("Device " + mDeviceID + " was not discovered.");
277                     mEventFacade.postEvent("Discovery", mBadNews);
278                     mEventFacade.postEvent("Bond", mBadNews);
279                     return;
280                 }
281                 // Attempt to initiate bonding.
282                 if (!started) {
283                     Log.d("Bond with " + mDevice.getAlias());
284                     if (mDevice.createBond()) {
285                         started = true;
286                         Log.d("Bonding started.");
287                     } else {
288                         Log.e("Failed to bond with " + mDevice.getAlias());
289                         mEventFacade.postEvent("Bond", mBadNews);
290                         mService.unregisterReceiver(listeningDevices.remove("Bond" + mDeviceID));
291                     }
292                 }
293             } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) {
294                 Log.d("Bond state changing.");
295                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
296                 if (BluetoothFacade.deviceMatch(device, mDeviceID)) {
297                     int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
298                     Log.d("New state is " + state);
299                     if (state == BluetoothDevice.BOND_BONDED) {
300                         Log.d("Bonding with " + mDeviceID + " successful.");
301                         mEventFacade.postEvent("Bond" + mDeviceID, mGoodNews);
302                         mService.unregisterReceiver(listeningDevices.remove("Bond" + mDeviceID));
303                     }
304                 }
305             }
306         }
307     }
308 
309     public class ConnectStateChangeReceiver extends BroadcastReceiver {
310         private final String mDeviceID;
311 
ConnectStateChangeReceiver(String deviceID)312         public ConnectStateChangeReceiver(String deviceID) {
313             mDeviceID = deviceID;
314         }
315 
316         @Override
onReceive(Context context, Intent intent)317         public void onReceive(Context context, Intent intent) {
318             // no matter what the action, just push it...
319             String action = intent.getAction();
320             Log.d("Action received: " + action);
321 
322             BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
323             // Check if received the specified device
324             if (!BluetoothFacade.deviceMatch(device, mDeviceID)) {
325                 Log.e("Action devices does match act: " + device + " exp " + mDeviceID);
326                 return;
327             }
328             // Find the state.
329             int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
330             if (state == -1) {
331                 Log.e("Action does not have a state.");
332                 return;
333             }
334 
335             // Switch Only Necessary for Old implementation. Left in for backwards compatability.
336             int profile = -1;
337             switch (action) {
338                 case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED:
339                     profile = BluetoothProfile.A2DP;
340                     break;
341                 case BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED:
342                     profile = BluetoothProfile.HID_HOST;
343                     break;
344                 case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED:
345                     profile = BluetoothProfile.HEADSET;
346                     break;
347                 case BluetoothPan.ACTION_CONNECTION_STATE_CHANGED:
348                     profile = BluetoothProfile.PAN;
349                     break;
350                 case BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED:
351                     profile = BluetoothProfile.HEADSET_CLIENT;
352                     break;
353                 case BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED:
354                     profile = BluetoothProfile.A2DP_SINK;
355                     break;
356                 case BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED:
357                     profile = BluetoothProfile.PBAP_CLIENT;
358                     break;
359                 case BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED:
360                     profile = BluetoothProfile.MAP_CLIENT;
361                     break;
362             }
363 
364             if (profile == -1) {
365                 Log.e("Action does not match any given profiles " + action);
366             }
367 
368             // The newer implementation will just post the Bundle with the literal event
369             // intead of the old implemenatation of posting BluetoothProfileConnectionStateChanged
370             // with the action inside of the Bundle. This makes for cleaner connection handling
371             // from test frameworks. Left the old implemenation in for backwards compatability.
372 
373             // Post an event to Facade.
374             Bundle news = new Bundle();
375             news.putInt("state", state);
376             news.putString("addr", device.getAddress());
377             mEventFacade.postEvent(action, news);
378 
379             news.putInt("profile", profile);
380             news.putString("action", action);
381             mEventFacade.postEvent("BluetoothProfileConnectionStateChanged", news);
382         }
383     }
384 
385     /**
386      * Converts a given JSONArray to an ArrayList of Integers
387      *
388      * @param jsonArray the JSONArray to be converted
389      * @return <code>List<Integer></></code> the converted list of Integers
390      */
jsonArrayToIntegerList(JSONArray jsonArray)391     private List<Integer> jsonArrayToIntegerList(JSONArray jsonArray) throws JSONException {
392         if (jsonArray == null) {
393             return null;
394         }
395         List<Integer> intArray = new ArrayList<Integer>();
396         for (int i = 0; i < jsonArray.length(); i++) {
397             intArray.add(jsonArray.getInt(i));
398         }
399         return intArray;
400 
401     }
402 
403     @Rpc(description = "Start monitoring state changes for input device.")
bluetoothStartConnectionStateChangeMonitor( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)404     public void bluetoothStartConnectionStateChangeMonitor(
405         @RpcParameter(name = "deviceID",
406                     description = "Name or MAC address of a bluetooth device.")
407                     String deviceID) {
408         if (!mDeviceMonitorList.contains(deviceID)) {
409             ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
410             mService.registerReceiver(receiver, mA2dpStateChangeFilter);
411             mService.registerReceiver(receiver, mA2dpSinkStateChangeFilter);
412             mService.registerReceiver(receiver, mHidStateChangeFilter);
413             mService.registerReceiver(receiver, mHspStateChangeFilter);
414             mService.registerReceiver(receiver, mHfpClientStateChangeFilter);
415             mService.registerReceiver(receiver, mPbapClientStateChangeFilter);
416             mService.registerReceiver(receiver, mPanStateChangeFilter);
417             mService.registerReceiver(receiver, mMapClientStateChangeFilter);
418             mService.registerReceiver(receiver, mMapStateChangeFilter);
419             listeningDevices.put("StateChangeListener:" + deviceID, receiver);
420         }
421     }
422 
423     /**
424      * Connect on all the profiles to the given Bluetooth device
425      *
426      * @param device   The <code>BluetoothDevice</code> to connect to
427      * @param deviceID Name (String) of the device to connect to
428      */
connectProfile(BluetoothDevice device, String deviceID)429     private void connectProfile(BluetoothDevice device, String deviceID) {
430         mService.registerReceiver(mPairingHelper, mPairingFilter);
431         ParcelUuid[] deviceUuids = device.getUuids();
432         Log.d("Device uuid is " + Arrays.toString(deviceUuids));
433         if (deviceUuids == null) {
434             mEventFacade.postEvent("BluetoothProfileConnectionEvent", mBadNews);
435         }
436         Log.d("Connecting to " + device.getAlias());
437         if (BluetoothUuid.containsAnyUuid(BluetoothA2dpFacade.SINK_UUIDS, deviceUuids)) {
438             mA2dpProfile.a2dpConnect(device);
439         }
440         if (BluetoothUuid.containsAnyUuid(BluetoothA2dpSinkFacade.SOURCE_UUIDS, deviceUuids)) {
441             mA2dpSinkProfile.a2dpSinkConnect(device);
442         }
443         if (BluetoothUuid.containsAnyUuid(BluetoothHidFacade.UUIDS, deviceUuids)) {
444             mHidProfile.hidConnect(device);
445         }
446         if (BluetoothUuid.containsAnyUuid(BluetoothHspFacade.UUIDS, deviceUuids)) {
447             mHspProfile.hspConnect(device);
448         }
449         if (BluetoothUuid.containsAnyUuid(BluetoothHfpClientFacade.UUIDS, deviceUuids)) {
450             mHfpClientProfile.hfpClientConnect(device);
451         }
452         if (BluetoothUuid.containsAnyUuid(BluetoothMapClientFacade.MAP_UUIDS, deviceUuids)) {
453             mMapClientProfile.mapClientConnect(device);
454         }
455         if (BluetoothUuid.containsAnyUuid(BluetoothPanFacade.UUIDS, deviceUuids)) {
456             mPanProfile.panConnect(device);
457         }
458         if (BluetoothUuid.containsAnyUuid(BluetoothPbapClientFacade.UUIDS, deviceUuids)) {
459             mPbapClientProfile.pbapClientConnect(device);
460         }
461         mService.unregisterReceiver(mPairingHelper);
462     }
463 
464     /**
465      * Disconnect on all available profiles from the given device
466      *
467      * @param device   The <code>BluetoothDevice</code> to disconnect from
468      * @param deviceID Name (String) of the device to disconnect from
469      */
disconnectProfiles(BluetoothDevice device, String deviceID)470     private void disconnectProfiles(BluetoothDevice device, String deviceID) {
471         Log.d("Disconnecting device " + device);
472         // Blindly disconnect all profiles. We may not have some of them connected so that will be a
473         // null op.
474         mA2dpProfile.a2dpDisconnect(device);
475         mA2dpSinkProfile.a2dpSinkDisconnect(device);
476         mHidProfile.hidDisconnect(device);
477         mHidDeviceProfile.hidDeviceDisconnect(device);
478         mHspProfile.hspDisconnect(device);
479         mHfpClientProfile.hfpClientDisconnect(device);
480         mPbapClientProfile.pbapClientDisconnect(device);
481         mPanProfile.panDisconnect(device);
482         mMapClientProfile.mapClientDisconnect(device);
483     }
484 
485     /**
486      * Disconnect from specific profiles provided in the given List of profiles.
487      *
488      * @param device     The {@link BluetoothDevice} to disconnect from
489      * @param deviceID   Name/BDADDR (String) of the device to disconnect from
490      * @param profileIds The list of profiles we want to disconnect on.
491      */
disconnectProfiles(BluetoothDevice device, String deviceID, List<Integer> profileIds)492     private void disconnectProfiles(BluetoothDevice device, String deviceID,
493             List<Integer> profileIds) {
494         boolean result;
495         for (int profileId : profileIds) {
496             switch (profileId) {
497                 case BluetoothProfile.A2DP_SINK:
498                     mA2dpSinkProfile.a2dpSinkDisconnect(device);
499                     break;
500                 case BluetoothProfile.A2DP:
501                     mA2dpProfile.a2dpDisconnect(device);
502                     break;
503                 case BluetoothProfile.HID_HOST:
504                     mHidProfile.hidDisconnect(device);
505                     break;
506                 case BluetoothProfile.HID_DEVICE:
507                     mHidDeviceProfile.hidDeviceDisconnect(device);
508                     break;
509                 case BluetoothProfile.HEADSET:
510                     mHspProfile.hspDisconnect(device);
511                     break;
512                 case BluetoothProfile.HEADSET_CLIENT:
513                     mHfpClientProfile.hfpClientDisconnect(device);
514                     break;
515                 case BluetoothProfile.PAN:
516                     mPanProfile.panDisconnect(device);
517                     break;
518                 case BluetoothProfile.PBAP_CLIENT:
519                     mPbapClientProfile.pbapClientDisconnect(device);
520                     break;
521                 case BluetoothProfile.MAP_CLIENT:
522                     mMapClientProfile.mapDisconnect(device);
523                     break;
524                 default:
525                     Log.d("Unknown Profile Id to disconnect from. Quitting");
526                     return; // returns on the first unknown profile  it encounters.
527             }
528         }
529     }
530 
531     @Rpc(description = "Start intercepting all bluetooth connection pop-ups.")
bluetoothStartPairingHelper( @pcParametername = "autoConfirm", description = "Whether connection should be auto confirmed") @pcDefault"true") @pcOptional Boolean autoConfirm)532     public void bluetoothStartPairingHelper(
533         @RpcParameter(name = "autoConfirm",
534                     description = "Whether connection should be auto confirmed")
535         @RpcDefault("true") @RpcOptional
536         Boolean autoConfirm) {
537         Log.d("Staring pairing helper");
538         mPairingHelper.setAutoConfirm(autoConfirm);
539         mService.registerReceiver(mPairingHelper, mPairingFilter);
540     }
541 
542     @Rpc(description = "Return a list of devices connected through bluetooth")
bluetoothGetConnectedDevices()543     public List<BluetoothDevice> bluetoothGetConnectedDevices() {
544         ArrayList<BluetoothDevice> results = new ArrayList<BluetoothDevice>();
545         for (BluetoothDevice bd : mBluetoothAdapter.getBondedDevices()) {
546             if (bd.isConnected()) {
547                 results.add(bd);
548             }
549         }
550         return results;
551     }
552 
553     /**
554      * Return a list of service UUIDS supported by the bonded device.
555      * @param macAddress the String mac address of the bonded device.
556      *
557      * @return the String list of supported UUIDS.
558      * @throws Exception
559      */
560     @Rpc(description = "Return a list of service UUIDS supported by the bonded device")
bluetoothGetBondedDeviceUuids( @pcParametername = "macAddress") String macAddress)561     public List<String> bluetoothGetBondedDeviceUuids(
562         @RpcParameter(name = "macAddress") String macAddress) throws Exception {
563         BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
564                 macAddress);
565         ArrayList<String> uuidStrings = new ArrayList<>();
566         for (ParcelUuid parcelUuid : mDevice.getUuids()) {
567             uuidStrings.add(parcelUuid.toString());
568         }
569         return uuidStrings;
570     }
571 
572     @Rpc(description = "Return a list of devices connected through bluetooth LE")
bluetoothGetConnectedLeDevices(Integer profile)573     public List<BluetoothDevice> bluetoothGetConnectedLeDevices(Integer profile) {
574         return mBluetoothManager.getConnectedDevices(profile);
575     }
576 
577     @Rpc(description = "Bluetooth init Bond by Mac Address")
bluetoothBond(@pcParametername = "macAddress") String macAddress)578     public boolean bluetoothBond(@RpcParameter(name = "macAddress") String macAddress) {
579         mContext.registerReceiver(new BondBroadcastReceiver(),
580                 new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED));
581         return mBluetoothAdapter.getRemoteDevice(macAddress).createBond();
582     }
583 
584     @Rpc(description = "Bluetooth init LE Bond by Mac Address")
bluetoothLeBond(@pcParametername = "macAddress") String macAddress)585     public boolean bluetoothLeBond(@RpcParameter(name = "macAddress") String macAddress) {
586         mContext.registerReceiver(new BondBroadcastReceiver(),
587                 new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED));
588         return mBluetoothAdapter.getRemoteDevice(macAddress).createBond(BluetoothDevice.TRANSPORT_LE);
589     }
590 
591     @Rpc(description = "Return true if a bluetooth device is connected.")
bluetoothIsDeviceConnected(String deviceID)592     public Boolean bluetoothIsDeviceConnected(String deviceID) {
593         for (BluetoothDevice bd : mBluetoothAdapter.getBondedDevices()) {
594             if (BluetoothFacade.deviceMatch(bd, deviceID)) {
595                 return bd.isConnected();
596             }
597         }
598         return false;
599     }
600 
601     /**
602      * Generates the local Out of Band data for the given transport.
603      */
604     @Rpc(description = "Generate Out of Band data for OOB Pairing.")
bluetoothGenerateLocalOobData(@pcParametername = "transport") String transport)605     public void bluetoothGenerateLocalOobData(@RpcParameter(name = "transport") String transport) {
606         Log.d("bluetoothGenerateLocalOobData(" + transport + ")");
607         mBluetoothAdapter.generateLocalOobData(Integer.parseInt(transport),
608                 mContext.getMainExecutor(), mGenerateOobDataCallback);
609 
610     }
611 
hexStringToByteArray(String s)612     private static byte[] hexStringToByteArray(String s) {
613         if (s == null) {
614             throw new IllegalArgumentException("Hex String must not be null!");
615         }
616         int len = s.length();
617         if ((len % 2) != 0 || len < 1) { // Multiple of 2 or empty
618             throw new IllegalArgumentException("Hex String must be an even number > 0");
619         }
620         byte[] data = new byte[len / 2];
621         for (int i = 0; i < len; i += 2) {
622             data[i / 2] = (byte) ((byte) (Character.digit(s.charAt(i), 16) << 4)
623                     + (byte) Character.digit(s.charAt(i + 1), 16));
624         }
625         return data;
626     }
627 
toHexString(byte[] a)628     private static String toHexString(byte[] a) {
629         if (a == null) return null;
630         StringBuilder builder = new StringBuilder(a.length * 2);
631         for (byte b : a) {
632             builder.append(String.format("%02x", b));
633         }
634         return builder.toString();
635     }
636 
637     /**
638      * Bond to a device using Out of Band Data over LE transport. Note that there is a distinction
639      * between the address with type supplied in the oob data and the address and type of the
640      * BluetoothDevice object.
641      *
642      * @param oobDataAddress is the MAC address to be used in the oob data
643      * @param oobDataAddressType is the BluetoothDevice.AddressType for the oob data MAC address
644      * @param transport String "1", "2", "3" to match TRANSPORT_*
645      * @param c Hex String of the 16 octet confirmation
646      * @param r Hex String of the 16 octet randomizer
647      * @param address String representation of MAC address for the BluetoothDevice object
648      * @param addressType the BluetoothDevice.AddressType for the BluetoothDevice object
649      */
650     @Rpc(description = "Creates and Out of Band LE bond.")
bluetoothCreateLeBondOutOfBand( @pcParametername = "oobDataAddress") String oobDataAddress, @RpcParameter(name = "oobDataAddressType") Integer oobDataAddressType, @RpcParameter(name = "c") String c, @RpcParameter(name = "r") String r, @RpcParameter(name = "address") String address, @RpcParameter(name = "addressType") @RpcDefault("1") Integer addressType)651     public boolean bluetoothCreateLeBondOutOfBand(
652             @RpcParameter(name = "oobDataAddress") String oobDataAddress,
653             @RpcParameter(name = "oobDataAddressType") Integer oobDataAddressType,
654             @RpcParameter(name = "c") String c, @RpcParameter(name = "r") String r,
655             @RpcParameter(name = "address") String address,
656             @RpcParameter(name = "addressType") @RpcDefault("1") Integer addressType) {
657         Log.d("bluetoothCreateLeBondOutOfBand(" + address + ", " + addressType + "," + c + ", "
658                 + r + ")");
659         BluetoothDevice remoteDevice = mBluetoothAdapter.getRemoteLeDevice(address, addressType);
660         byte[] addressBytes = new byte[7];
661         int i = 0;
662         for (String s : oobDataAddress.split(":")) {
663             addressBytes[i] = hexStringToByteArray(s)[0];
664             i++;
665         }
666 
667         // Inserts the oob address type if one is provided
668         if (oobDataAddressType == BluetoothDevice.ADDRESS_TYPE_PUBLIC
669                 || oobDataAddressType == BluetoothDevice.ADDRESS_TYPE_RANDOM) {
670             addressBytes[i] = oobDataAddressType.byteValue();
671         }
672 
673         OobData p192 = null;
674         OobData p256 = new OobData.LeBuilder(hexStringToByteArray(c),
675                 addressBytes, OobData.LE_DEVICE_ROLE_BOTH_PREFER_CENTRAL)
676                 .setRandomizerHash(hexStringToByteArray(r))
677                 .build();
678         mContext.registerReceiver(new BondBroadcastReceiver(),
679                 new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED));
680         return remoteDevice.createBondOutOfBand(BluetoothDevice.TRANSPORT_LE, p192, p256);
681     }
682 
683     private class BondBroadcastReceiver extends BroadcastReceiver {
684         @Override
onReceive(Context context, Intent intent)685         public void onReceive(Context context, Intent intent) {
686             Log.d("BondBroadcastReceiver onReceive(" + context + ", " + intent + ")");
687             int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
688                     BluetoothDevice.BOND_NONE);
689             if (state == BluetoothDevice.BOND_BONDED) {
690                 Bundle event = new Bundle();
691                 event.putBoolean("bonded_state", state == BluetoothDevice.BOND_BONDED);
692                 mEventFacade.postEvent("Bonded", event);
693                 mContext.unregisterReceiver(this);
694             } else if (state == BluetoothDevice.BOND_NONE) {
695                 Bundle event = new Bundle();
696                 event.putBoolean("bonded_state", state == BluetoothDevice.BOND_BONDED);
697                 mEventFacade.postEvent("Unbonded", event);
698                 mContext.unregisterReceiver(this);
699             }
700         }
701     }
702 
703     @Rpc(description = "Return list of connected bluetooth devices over a profile",
704             returns = "List of devices connected over the profile")
bluetoothGetConnectedDevicesOnProfile( @pcParametername = "profileId", description = "profileId same as BluetoothProfile") Integer profileId)705     public List<BluetoothDevice> bluetoothGetConnectedDevicesOnProfile(
706             @RpcParameter(name = "profileId",
707                     description = "profileId same as BluetoothProfile")
708                     Integer profileId) {
709         BluetoothProfile profile = null;
710         switch (profileId) {
711             case BluetoothProfile.A2DP_SINK:
712                 return mA2dpSinkProfile.bluetoothA2dpSinkGetConnectedDevices();
713             case BluetoothProfile.HEADSET_CLIENT:
714                 return mHfpClientProfile.bluetoothHfpClientGetConnectedDevices();
715             case BluetoothProfile.PBAP_CLIENT:
716                 return mPbapClientProfile.bluetoothPbapClientGetConnectedDevices();
717             case BluetoothProfile.MAP_CLIENT:
718                 return mMapClientProfile.bluetoothMapClientGetConnectedDevices();
719             case BluetoothProfile.HID_HOST:
720                 return mHidProfile.bluetoothHidGetConnectedDevices();
721             default:
722                 Log.w("Profile id " + profileId + " is not yet supported.");
723                 return new ArrayList<BluetoothDevice>();
724         }
725     }
726 
727     @Rpc(description = "Connect to a specified device once it's discovered.",
728             returns = "Whether discovery started successfully.")
bluetoothDiscoverAndConnect( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)729     public Boolean bluetoothDiscoverAndConnect(
730             @RpcParameter(name = "deviceID",
731                     description = "Name or MAC address of a bluetooth device.")
732                     String deviceID) {
733         mBluetoothAdapter.cancelDiscovery();
734         if (listeningDevices.containsKey(deviceID)) {
735             Log.d("This device is already in the process of discovery and connecting.");
736             return true;
737         }
738         DiscoverConnectReceiver receiver = new DiscoverConnectReceiver(deviceID);
739         listeningDevices.put("Connect" + deviceID, receiver);
740         mService.registerReceiver(receiver, mDiscoverConnectFilter);
741         return mBluetoothAdapter.startDiscovery();
742     }
743 
744     @Rpc(description = "Bond to a specified device once it's discovered.",
745             returns = "Whether discovery started successfully. ")
bluetoothDiscoverAndBond( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)746     public Boolean bluetoothDiscoverAndBond(
747             @RpcParameter(name = "deviceID",
748                     description = "Name or MAC address of a bluetooth device.")
749                     String deviceID) {
750         mBluetoothAdapter.cancelDiscovery();
751         if (listeningDevices.containsKey(deviceID)) {
752             Log.d("This device is already in the process of discovery and bonding.");
753             return true;
754         }
755         if (BluetoothFacade.deviceExists(mBluetoothAdapter.getBondedDevices(), deviceID)) {
756             Log.d("Device " + deviceID + " is already bonded.");
757             mEventFacade.postEvent("Bond" + deviceID, mGoodNews);
758             return true;
759         }
760         DiscoverBondReceiver receiver = new DiscoverBondReceiver(deviceID);
761         if (listeningDevices.containsKey("Bond" + deviceID)) {
762             mService.unregisterReceiver(listeningDevices.remove("Bond" + deviceID));
763         }
764         listeningDevices.put("Bond" + deviceID, receiver);
765         mService.registerReceiver(receiver, mBondFilter);
766         Log.d("Start discovery for bonding.");
767         return mBluetoothAdapter.startDiscovery();
768     }
769 
770     @Rpc(description = "Unbond a device.",
771             returns = "Whether the device was successfully unbonded.")
bluetoothUnbond( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)772     public Boolean bluetoothUnbond(
773             @RpcParameter(name = "deviceID",
774                     description = "Name or MAC address of a bluetooth device.")
775                     String deviceID) throws Exception {
776         // We don't want to crash the test if the test passes an address that cannot be found.
777         try {
778             BluetoothDevice mDevice = BluetoothFacade.getDevice(
779                     mBluetoothAdapter.getBondedDevices(), deviceID);
780             mContext.registerReceiver(new BondBroadcastReceiver(),
781                     new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED));
782             return mDevice.removeBond();
783         } catch (Exception e) {
784             Log.d("Failed to find the device by deviceId");
785             return false;
786         }
787     }
788 
789     @Rpc(description = "Connect to a device that is already bonded.")
bluetoothConnectBonded( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)790     public void bluetoothConnectBonded(
791             @RpcParameter(name = "deviceID",
792                     description = "Name or MAC address of a bluetooth device.")
793                     String deviceID) throws Exception {
794         BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
795                 deviceID);
796         connectProfile(mDevice, deviceID);
797     }
798 
799     @Rpc(description = "Disconnect from a device that is already connected.")
bluetoothDisconnectConnected( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)800     public void bluetoothDisconnectConnected(
801             @RpcParameter(name = "deviceID",
802                     description = "Name or MAC address of a bluetooth device.")
803                     String deviceID) throws Exception {
804         BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
805                 deviceID);
806         disconnectProfiles(mDevice, deviceID);
807     }
808 
809     @Rpc(description = "Disconnect on a profile from a device that is already connected.")
bluetoothDisconnectConnectedProfile( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID, @RpcParameter(name = "profileSet", description = "List of profiles to disconnect from.") JSONArray profileSet )810     public void bluetoothDisconnectConnectedProfile(
811             @RpcParameter(name = "deviceID",
812                     description = "Name or MAC address of a bluetooth device.")
813                     String deviceID,
814             @RpcParameter(name = "profileSet",
815                     description = "List of profiles to disconnect from.")
816                     JSONArray profileSet
817     ) throws Exception {
818         BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
819                 deviceID);
820         disconnectProfiles(mDevice, deviceID, jsonArrayToIntegerList(profileSet));
821     }
822 
823     @Rpc(description = "Change permissions for a profile.")
bluetoothChangeProfileAccessPermission( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID, @RpcParameter(name = "profileID", description = "Number of Profile to change access permission") Integer profileID, @RpcParameter(name = "access", description = "Access level 0 = Unknown, 1 = Allowed, 2 = Rejected") Integer access )824     public void bluetoothChangeProfileAccessPermission(
825             @RpcParameter(name = "deviceID",
826                     description = "Name or MAC address of a bluetooth device.")
827                     String deviceID,
828             @RpcParameter(name = "profileID",
829                     description = "Number of Profile to change access permission")
830                     Integer profileID,
831             @RpcParameter(name = "access",
832                     description = "Access level 0 = Unknown, 1 = Allowed, 2 = Rejected")
833                     Integer access
834     ) throws Exception {
835         if (access < 0 || access > 2) {
836             Log.w("Unsupported access level.");
837             return;
838         }
839         BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
840                 deviceID);
841         switch (profileID) {
842             case BluetoothProfile.PBAP:
843                 mDevice.setPhonebookAccessPermission(access);
844                 break;
845             default:
846                 Log.w("Unsupported profile access change.");
847         }
848     }
849 
850 
851     @Override
shutdown()852     public void shutdown() {
853         for (BroadcastReceiver receiver : listeningDevices.values()) {
854             try {
855                 mService.unregisterReceiver(receiver);
856             } catch (IllegalArgumentException ex) {
857                 Log.e("Failed to unregister " + ex);
858             }
859         }
860         listeningDevices.clear();
861         mService.unregisterReceiver(mPairingHelper);
862     }
863 }
864 
865