1 /*
2  * Copyright (C) 2016 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.googlecode.android_scripting.facade.bluetooth;
18 
19 import java.util.ArrayList;
20 import java.util.Collections;
21 import java.util.HashMap;
22 import java.util.List;
23 import java.util.Map;
24 
25 import android.app.Service;
26 import android.bluetooth.BluetoothA2dp;
27 import android.bluetooth.BluetoothA2dpSink;
28 import android.bluetooth.BluetoothAdapter;
29 import android.bluetooth.BluetoothDevice;
30 import android.bluetooth.BluetoothHeadset;
31 import android.bluetooth.BluetoothHeadsetClient;
32 import android.bluetooth.BluetoothInputDevice;
33 import android.bluetooth.BluetoothUuid;
34 import android.content.BroadcastReceiver;
35 import android.content.Context;
36 import android.content.Intent;
37 import android.content.IntentFilter;
38 import android.os.Bundle;
39 import android.os.ParcelUuid;
40 
41 import com.googlecode.android_scripting.Log;
42 import com.googlecode.android_scripting.facade.EventFacade;
43 import com.googlecode.android_scripting.facade.FacadeManager;
44 import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
45 import com.googlecode.android_scripting.rpc.Rpc;
46 import com.googlecode.android_scripting.rpc.RpcParameter;
47 
48 public class BluetoothConnectionFacade extends RpcReceiver {
49 
50     private final Service mService;
51     private final BluetoothAdapter mBluetoothAdapter;
52     private final BluetoothPairingHelper mPairingHelper;
53     private final Map<String, BroadcastReceiver> listeningDevices;
54     private final EventFacade mEventFacade;
55 
56     private final IntentFilter mDiscoverConnectFilter;
57     private final IntentFilter mPairingFilter;
58     private final IntentFilter mBondFilter;
59     private final IntentFilter mA2dpStateChangeFilter;
60     private final IntentFilter mA2dpSinkStateChangeFilter;
61     private final IntentFilter mHidStateChangeFilter;
62     private final IntentFilter mHspStateChangeFilter;
63     private final IntentFilter mHfpClientStateChangeFilter;
64 
65     private final Bundle mGoodNews;
66     private final Bundle mBadNews;
67 
68     private BluetoothA2dpFacade mA2dpProfile;
69     private BluetoothA2dpSinkFacade mA2dpSinkProfile;
70     private BluetoothHidFacade mHidProfile;
71     private BluetoothHspFacade mHspProfile;
72     private BluetoothHfpClientFacade mHfpClientProfile;
73 
BluetoothConnectionFacade(FacadeManager manager)74     public BluetoothConnectionFacade(FacadeManager manager) {
75         super(manager);
76         mService = manager.getService();
77         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
78         mPairingHelper = new BluetoothPairingHelper();
79         // Use a synchronized map to avoid racing problems
80         listeningDevices = Collections.synchronizedMap(new HashMap<String, BroadcastReceiver>());
81 
82         mEventFacade = manager.getReceiver(EventFacade.class);
83         mA2dpProfile = manager.getReceiver(BluetoothA2dpFacade.class);
84         mA2dpSinkProfile = manager.getReceiver(BluetoothA2dpSinkFacade.class);
85         mHidProfile = manager.getReceiver(BluetoothHidFacade.class);
86         mHspProfile = manager.getReceiver(BluetoothHspFacade.class);
87         mHfpClientProfile = manager.getReceiver(BluetoothHfpClientFacade.class);
88 
89         mDiscoverConnectFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
90         mDiscoverConnectFilter.addAction(BluetoothDevice.ACTION_UUID);
91         mDiscoverConnectFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
92 
93         mPairingFilter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST);
94         mPairingFilter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST);
95         mPairingFilter.setPriority(999);
96 
97         mBondFilter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
98         mBondFilter.addAction(BluetoothDevice.ACTION_FOUND);
99         mBondFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
100 
101         mA2dpStateChangeFilter = new IntentFilter(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
102         mA2dpSinkStateChangeFilter =
103             new IntentFilter(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED);
104         mHidStateChangeFilter =
105             new IntentFilter(BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED);
106         mHspStateChangeFilter = new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
107         mHfpClientStateChangeFilter =
108             new IntentFilter(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
109 
110         mGoodNews = new Bundle();
111         mGoodNews.putBoolean("Status", true);
112         mBadNews = new Bundle();
113         mBadNews.putBoolean("Status", false);
114     }
115 
unregisterCachedListener(String listenerId)116     private void unregisterCachedListener(String listenerId) {
117         BroadcastReceiver listener = listeningDevices.remove(listenerId);
118         if (listener != null) {
119             mService.unregisterReceiver(listener);
120         }
121     }
122 
123     /**
124      * Connect to a specific device upon its discovery
125      */
126     public class DiscoverConnectReceiver extends BroadcastReceiver {
127         private final String mDeviceID;
128         private BluetoothDevice mDevice;
129 
130         /**
131          * Constructor
132          *
133          * @param deviceID Either the device alias name or mac address.
134          * @param bond If true, bond the device only.
135          */
DiscoverConnectReceiver(String deviceID)136         public DiscoverConnectReceiver(String deviceID) {
137             super();
138             mDeviceID = deviceID;
139         }
140 
141         @Override
onReceive(Context context, Intent intent)142         public void onReceive(Context context, Intent intent) {
143             String action = intent.getAction();
144             // The specified device is found.
145             if (action.equals(BluetoothDevice.ACTION_FOUND)) {
146                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
147                 if (BluetoothFacade.deviceMatch(device, mDeviceID)) {
148                     Log.d("Found device " + device.getAliasName() + " for connection.");
149                     mBluetoothAdapter.cancelDiscovery();
150                     mDevice = device;
151                 }
152             // After discovery stops.
153             } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {
154                 if (mDevice == null) {
155                     Log.d("Device " + mDeviceID + " not discovered.");
156                     mEventFacade.postEvent("Bond" + mDeviceID, mBadNews);
157                     return;
158                 }
159                 boolean status = mDevice.fetchUuidsWithSdp();
160                 Log.d("Initiated ACL connection: " + status);
161             } else if (action.equals(BluetoothDevice.ACTION_UUID)) {
162                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
163                 if (BluetoothFacade.deviceMatch(device, mDeviceID)) {
164                     Log.d("Initiating connections.");
165                     connectProfile(device, mDeviceID);
166                     mService.unregisterReceiver(listeningDevices.remove("Connect" + mDeviceID));
167                 }
168             }
169         }
170     }
171 
172     /**
173      * Connect to a specific device upon its discovery
174      */
175     public class DiscoverBondReceiver extends BroadcastReceiver {
176         private final String mDeviceID;
177         private BluetoothDevice mDevice = null;
178         private boolean started = false;
179 
180         /**
181          * Constructor
182          *
183          * @param deviceID Either the device alias name or Mac address.
184          */
DiscoverBondReceiver(String deviceID)185         public DiscoverBondReceiver(String deviceID) {
186             super();
187             mDeviceID = deviceID;
188         }
189 
190         @Override
onReceive(Context context, Intent intent)191         public void onReceive(Context context, Intent intent) {
192             String action = intent.getAction();
193             // The specified device is found.
194             if (action.equals(BluetoothDevice.ACTION_FOUND)) {
195                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
196                 if (BluetoothFacade.deviceMatch(device, mDeviceID)) {
197                     Log.d("Found device " + device.getAliasName() + " for connection.");
198                     mBluetoothAdapter.cancelDiscovery();
199                     mDevice = device;
200                 }
201             // After discovery stops.
202             } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {
203                 if (mDevice == null) {
204                     Log.d("Device " + mDeviceID + " was not discovered.");
205                     mEventFacade.postEvent("Bond", mBadNews);
206                     return;
207                 }
208                 // Attempt to initiate bonding.
209                 if (!started) {
210                     Log.d("Bond with " + mDevice.getAliasName());
211                     if (mDevice.createBond()) {
212                         started = true;
213                         Log.d("Bonding started.");
214                     } else {
215                         Log.e("Failed to bond with " + mDevice.getAliasName());
216                         mEventFacade.postEvent("Bond", mBadNews);
217                         mService.unregisterReceiver(listeningDevices.remove("Bond" + mDeviceID));
218                     }
219                 }
220             } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) {
221                 Log.d("Bond state changing.");
222                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
223                 if (BluetoothFacade.deviceMatch(device, mDeviceID)) {
224                     int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
225                     Log.d("New state is " + state);
226                     if (state == BluetoothDevice.BOND_BONDED) {
227                         Log.d("Bonding with " + mDeviceID + " successful.");
228                         mEventFacade.postEvent("Bond" + mDeviceID, mGoodNews);
229                         mService.unregisterReceiver(listeningDevices.remove("Bond" + mDeviceID));
230                     }
231                 }
232             }
233         }
234     }
235 
236     public class ConnectStateChangeReceiver extends BroadcastReceiver {
237         private final String mDeviceID;
238 
ConnectStateChangeReceiver(String deviceID)239         public ConnectStateChangeReceiver(String deviceID) {
240             mDeviceID = deviceID;
241         }
242 
243         @Override
onReceive(Context context, Intent intent)244         public void onReceive(Context context, Intent intent) {
245             String action = intent.getAction();
246             BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
247             // Check if received the specified device
248             if (!BluetoothFacade.deviceMatch(device, mDeviceID)) {
249                 return;
250             }
251             if (action.equals(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) {
252                 int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1);
253                 if (state == BluetoothA2dp.STATE_CONNECTED) {
254                     Bundle a2dpGoodNews = (Bundle) mGoodNews.clone();
255                     a2dpGoodNews.putString("Type", "a2dp");
256                     mEventFacade.postEvent("A2dpConnect" + mDeviceID, a2dpGoodNews);
257                     unregisterCachedListener("A2dpConnecting" + mDeviceID);
258                 }
259             } else if (action.equals(BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED)) {
260                 int state = intent.getIntExtra(BluetoothInputDevice.EXTRA_STATE, -1);
261                 if (state == BluetoothInputDevice.STATE_CONNECTED) {
262                     mEventFacade.postEvent("HidConnect" + mDeviceID, mGoodNews);
263                     unregisterCachedListener("HidConnecting" + mDeviceID);
264                 }
265             } else if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
266                 int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, -1);
267                 if (state == BluetoothHeadset.STATE_CONNECTED) {
268                     mEventFacade.postEvent("HspConnect" + mDeviceID, mGoodNews);
269                     unregisterCachedListener("HspConnecting" + mDeviceID);
270                 }
271             }
272         }
273     }
274 
connectProfile(BluetoothDevice device, String deviceID)275     private void connectProfile(BluetoothDevice device, String deviceID) {
276         mService.registerReceiver(mPairingHelper, mPairingFilter);
277         ParcelUuid[] deviceUuids = device.getUuids();
278         Log.d("Device uuid is " + deviceUuids);
279         if (deviceUuids == null) {
280             mEventFacade.postEvent("BluetoothProfileConnectionEvent", mBadNews);
281         }
282         Log.d("Connecting to " + device.getAliasName());
283         if (BluetoothUuid.containsAnyUuid(BluetoothA2dpFacade.SINK_UUIDS, deviceUuids)) {
284             boolean status = mA2dpProfile.a2dpConnect(device);
285             if (status) {
286                 Log.d("Connecting A2dp...");
287                 ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
288                 mService.registerReceiver(receiver, mA2dpStateChangeFilter);
289                 listeningDevices.put("A2dpConnecting" + deviceID, receiver);
290             } else {
291                 Log.d("Failed starting A2dp connection.");
292                 Bundle a2dpBadNews = (Bundle) mBadNews.clone();
293                 a2dpBadNews.putString("Type", "a2dp");
294                 mEventFacade.postEvent("Connect", a2dpBadNews);
295             }
296         }
297         if (BluetoothUuid.containsAnyUuid(BluetoothA2dpSinkFacade.SOURCE_UUIDS, deviceUuids)) {
298             boolean status = mA2dpSinkProfile.a2dpSinkConnect(device);
299             if (status) {
300                 Log.d("Connecting A2dp Sink...");
301                 ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
302                 mService.registerReceiver(receiver, mA2dpSinkStateChangeFilter);
303                 listeningDevices.put("A2dpSinkConnecting" + deviceID, receiver);
304             } else {
305                 Log.d("Failed starting A2dp Sink connection.");
306                 Bundle a2dpSinkBadNews = (Bundle) mBadNews.clone();
307                 a2dpSinkBadNews.putString("Type", "a2dpsink");
308                 mEventFacade.postEvent("Connect", a2dpSinkBadNews);
309             }
310         }
311         if (BluetoothUuid.containsAnyUuid(BluetoothHidFacade.UUIDS, deviceUuids)) {
312             boolean status = mHidProfile.hidConnect(device);
313             if (status) {
314                 Log.d("Connecting Hid...");
315                 ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
316                 mService.registerReceiver(receiver, mHidStateChangeFilter);
317                 listeningDevices.put("HidConnecting" + deviceID, receiver);
318             } else {
319                 Log.d("Failed starting Hid connection.");
320                 mEventFacade.postEvent("HidConnect" + deviceID, mBadNews);
321             }
322         }
323         if (BluetoothUuid.containsAnyUuid(BluetoothHspFacade.UUIDS, deviceUuids)) {
324             boolean status = mHspProfile.hspConnect(device);
325             if (status) {
326                 Log.d("Connecting Hsp...");
327                 ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
328                 mService.registerReceiver(receiver, mHspStateChangeFilter);
329                 listeningDevices.put("HspConnecting" + deviceID, receiver);
330             } else {
331                 Log.d("Failed starting Hsp connection.");
332                 mEventFacade.postEvent("HspConnect" + deviceID, mBadNews);
333             }
334         }
335         if (BluetoothUuid.containsAnyUuid(BluetoothHfpClientFacade.UUIDS, deviceUuids)) {
336             boolean status = mHfpClientProfile.hfpClientConnect(device);
337             if (status) {
338                 Log.d("Connecting HFP Client ...");
339                 ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID);
340                 mService.registerReceiver(receiver, mHfpClientStateChangeFilter);
341                 listeningDevices.put("HfpClientConnecting" + deviceID, receiver);
342             } else {
343                 Log.d("Failed starting Hfp Client connection.");
344                 mEventFacade.postEvent("HfpClientConnect" + deviceID, mBadNews);
345             }
346         }
347         mService.unregisterReceiver(mPairingHelper);
348     }
349 
disconnectProfiles(BluetoothDevice device, String deviceID)350     private void disconnectProfiles(BluetoothDevice device, String deviceID) {
351         Log.d("Disconnecting device " + device);
352         // Blindly disconnect all profiles. We may not have some of them connected so that will be a
353         // null op.
354         mA2dpProfile.a2dpDisconnect(device);
355         mA2dpSinkProfile.a2dpSinkDisconnect(device);
356         mHidProfile.hidDisconnect(device);
357         mHspProfile.hspDisconnect(device);
358         mHfpClientProfile.hfpClientDisconnect(device);
359     }
360 
361     @Rpc(description = "Start intercepting all bluetooth connection pop-ups.")
bluetoothStartPairingHelper()362     public void bluetoothStartPairingHelper() {
363         mService.registerReceiver(mPairingHelper, mPairingFilter);
364     }
365 
366     @Rpc(description = "Return a list of devices connected through bluetooth")
bluetoothGetConnectedDevices()367     public List<BluetoothDevice> bluetoothGetConnectedDevices() {
368         ArrayList<BluetoothDevice> results = new ArrayList<BluetoothDevice>();
369         for (BluetoothDevice bd : mBluetoothAdapter.getBondedDevices()) {
370             if (bd.isConnected()) {
371                 results.add(bd);
372             }
373         }
374         return results;
375     }
376 
377     @Rpc(description = "Return true if a bluetooth device is connected.")
bluetoothIsDeviceConnected(String deviceID)378     public Boolean bluetoothIsDeviceConnected(String deviceID) {
379         for (BluetoothDevice bd : mBluetoothAdapter.getBondedDevices()) {
380             if (BluetoothFacade.deviceMatch(bd, deviceID)) {
381                 return bd.isConnected();
382             }
383         }
384         return false;
385     }
386 
387     @Rpc(description = "Connect to a specified device once it's discovered.",
388          returns = "Whether discovery started successfully.")
bluetoothDiscoverAndConnect( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)389     public Boolean bluetoothDiscoverAndConnect(
390             @RpcParameter(name = "deviceID",
391                           description = "Name or MAC address of a bluetooth device.")
392             String deviceID) {
393         mBluetoothAdapter.cancelDiscovery();
394         if (listeningDevices.containsKey(deviceID)) {
395             Log.d("This device is already in the process of discovery and connecting.");
396             return true;
397         }
398         DiscoverConnectReceiver receiver = new DiscoverConnectReceiver(deviceID);
399         listeningDevices.put("Connect" + deviceID, receiver);
400         mService.registerReceiver(receiver, mDiscoverConnectFilter);
401         return mBluetoothAdapter.startDiscovery();
402     }
403 
404     @Rpc(description = "Bond to a specified device once it's discovered.",
405          returns = "Whether discovery started successfully. ")
bluetoothDiscoverAndBond( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)406     public Boolean bluetoothDiscoverAndBond(
407             @RpcParameter(name = "deviceID",
408                           description = "Name or MAC address of a bluetooth device.")
409             String deviceID) {
410         mBluetoothAdapter.cancelDiscovery();
411         if (listeningDevices.containsKey(deviceID)) {
412             Log.d("This device is already in the process of discovery and bonding.");
413             return true;
414         }
415         if (BluetoothFacade.deviceExists(mBluetoothAdapter.getBondedDevices(), deviceID)) {
416             Log.d("Device " + deviceID + " is already bonded.");
417             mEventFacade.postEvent("Bond" + deviceID, mGoodNews);
418             return true;
419         }
420         DiscoverBondReceiver receiver = new DiscoverBondReceiver(deviceID);
421         if (listeningDevices.containsKey("Bond" + deviceID)) {
422             mService.unregisterReceiver(listeningDevices.remove("Bond" + deviceID));
423         }
424         listeningDevices.put("Bond" + deviceID, receiver);
425         mService.registerReceiver(receiver, mBondFilter);
426         Log.d("Start discovery for bonding.");
427         return mBluetoothAdapter.startDiscovery();
428     }
429 
430     @Rpc(description = "Unbond a device.",
431          returns = "Whether the device was successfully unbonded.")
bluetoothUnbond( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)432     public Boolean bluetoothUnbond(
433             @RpcParameter(name = "deviceID",
434                           description = "Name or MAC address of a bluetooth device.")
435             String deviceID) throws Exception {
436         BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
437                 deviceID);
438         return mDevice.removeBond();
439     }
440 
441     @Rpc(description = "Connect to a device that is already bonded.")
bluetoothConnectBonded( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)442     public void bluetoothConnectBonded(
443             @RpcParameter(name = "deviceID",
444                           description = "Name or MAC address of a bluetooth device.")
445             String deviceID) throws Exception {
446         BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
447                 deviceID);
448         connectProfile(mDevice, deviceID);
449     }
450 
451     // TODO: Split the disconnect RPC by profiles as well for granular control over the ACL
452     @Rpc(description = "Disconnect from a device that is already connected.")
bluetoothDisconnectConnected( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)453     public void bluetoothDisconnectConnected(
454             @RpcParameter(name = "deviceID",
455                           description = "Name or MAC address of a bluetooth device.")
456             String deviceID) throws Exception {
457         BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(),
458                 deviceID);
459         disconnectProfiles(mDevice, deviceID);
460     }
461 
462     @Override
shutdown()463     public void shutdown() {
464         for(BroadcastReceiver receiver : listeningDevices.values()) {
465             mService.unregisterReceiver(receiver);
466         }
467         listeningDevices.clear();
468         mService.unregisterReceiver(mPairingHelper);
469     }
470 }
471