1 /*
2  * Copyright 2023 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.google.snippet.bluetooth;
18 
19 import static android.bluetooth.BluetoothDevice.BOND_BONDED;
20 import static android.bluetooth.BluetoothDevice.BOND_NONE;
21 import static android.bluetooth.BluetoothDevice.TRANSPORT_LE;
22 
23 import static java.util.concurrent.TimeUnit.SECONDS;
24 
25 import android.bluetooth.BluetoothAdapter;
26 import android.bluetooth.BluetoothDevice;
27 import android.bluetooth.BluetoothGatt;
28 import android.bluetooth.BluetoothGattCallback;
29 import android.bluetooth.BluetoothManager;
30 import android.bluetooth.BluetoothProfile;
31 import android.bluetooth.OobData;
32 import android.bluetooth.le.ScanCallback;
33 import android.bluetooth.le.ScanResult;
34 import android.bluetooth.le.ScanSettings;
35 import android.content.BroadcastReceiver;
36 import android.content.Context;
37 import android.content.Intent;
38 import android.content.IntentFilter;
39 import android.os.ParcelUuid;
40 import android.util.Log;
41 
42 import java.util.UUID;
43 import java.util.concurrent.CountDownLatch;
44 
45 public final class BluetoothGattMultiDevicesClient {
46     private static final String TAG = "BluetoothGattMultiDevicesClient";
47 
48     private Context mContext;
49     private BluetoothAdapter mBluetoothAdapter;
50     private BluetoothGatt mBluetoothGatt;
51 
52     private CountDownLatch mConnectionBlocker = null;
53     private CountDownLatch mServicesDiscovered = null;
54     private Integer mWaitForConnectionState = null;
55 
56     private static final int CALLBACK_TIMEOUT_SEC = 5;
57 
58     private BluetoothDevice mServer;
59 
60     private final BluetoothGattCallback mGattCallback =
61             new BluetoothGattCallback() {
62                 @Override
63                 public void onConnectionStateChange(
64                         BluetoothGatt device, int status, int newState) {
65                     Log.i(TAG, "onConnectionStateChange: newState=" + newState);
66                     if (newState == mWaitForConnectionState && mConnectionBlocker != null) {
67                         Log.v(TAG, "Connected");
68                         mConnectionBlocker.countDown();
69                     }
70                 }
71 
72                 @Override
73                 public void onServicesDiscovered(BluetoothGatt gatt, int status) {
74                     mServicesDiscovered.countDown();
75                 }
76             };
77 
BluetoothGattMultiDevicesClient(Context context, BluetoothManager manager)78     public BluetoothGattMultiDevicesClient(Context context, BluetoothManager manager) {
79         mContext = context;
80         mBluetoothAdapter = manager.getAdapter();
81     }
82 
connect(String uuid)83     public BluetoothDevice connect(String uuid) {
84         // Scan for the peer
85         var serverFoundBlocker = new CountDownLatch(1);
86         var scanner = mBluetoothAdapter.getBluetoothLeScanner();
87         var callback =
88                 new ScanCallback() {
89                     @Override
90                     public void onScanResult(int callbackType, ScanResult result) {
91                         var uuids = result.getScanRecord().getServiceUuids();
92                         Log.v(TAG, "Found uuids " + uuids);
93                         if (uuids != null
94                                 && uuids.contains(new ParcelUuid(UUID.fromString(uuid)))) {
95                             mServer = result.getDevice();
96                             serverFoundBlocker.countDown();
97                         }
98                     }
99                 };
100         scanner.startScan(null, new ScanSettings.Builder().setLegacy(false).build(), callback);
101         boolean timeout = false;
102         try {
103             timeout = !serverFoundBlocker.await(CALLBACK_TIMEOUT_SEC, SECONDS);
104         } catch (InterruptedException e) {
105             Log.e(TAG, "", e);
106             timeout = true;
107         }
108         scanner.stopScan(callback);
109         if (timeout) {
110             Log.e(TAG, "Did not discover server");
111             return null;
112         }
113 
114         // Connect to the peer
115         mConnectionBlocker = new CountDownLatch(1);
116         mWaitForConnectionState = BluetoothProfile.STATE_CONNECTED;
117         mBluetoothGatt = mServer.connectGatt(mContext, false, mGattCallback, TRANSPORT_LE);
118         timeout = false;
119         try {
120             timeout = !mConnectionBlocker.await(CALLBACK_TIMEOUT_SEC, SECONDS);
121         } catch (InterruptedException e) {
122             Log.e(TAG, "", e);
123             timeout = true;
124         }
125         if (timeout) {
126             Log.e(TAG, "Did not connect to server");
127             return null;
128         }
129         return mServer;
130     }
131 
containsService(String uuid)132     public boolean containsService(String uuid) {
133         mServicesDiscovered = new CountDownLatch(1);
134         mBluetoothGatt.discoverServices();
135         try {
136             mServicesDiscovered.await(CALLBACK_TIMEOUT_SEC, SECONDS);
137         } catch (InterruptedException e) {
138             Log.e(TAG, "", e);
139             return false;
140         }
141 
142         return mBluetoothGatt.getService(UUID.fromString(uuid)) != null;
143     }
144 
disconnect(String uuid)145     public boolean disconnect(String uuid) {
146         if (!containsService(uuid)) {
147             Log.e(TAG, "Connected server does not contain the service with UUID: " + uuid);
148             return false;
149         }
150         // Connect to the peer
151         mConnectionBlocker = new CountDownLatch(1);
152         mWaitForConnectionState = BluetoothProfile.STATE_DISCONNECTED;
153         mBluetoothGatt.disconnect();
154         boolean timeout = false;
155         try {
156             timeout = !mConnectionBlocker.await(CALLBACK_TIMEOUT_SEC, SECONDS);
157         } catch (InterruptedException e) {
158             Log.e(TAG, "", e);
159             timeout = true;
160         }
161         if (timeout) {
162             Log.e(TAG, "Did not disconnect from server");
163             return false;
164         }
165         return true;
166     }
167 
168     private class BroadcastReceiverImpl extends BroadcastReceiver {
169         private final int mWaitForBondState;
170         private final CountDownLatch mBondingBlocker;
171 
BroadcastReceiverImpl(int waitForBondState, CountDownLatch bondingBlocker)172         BroadcastReceiverImpl(int waitForBondState, CountDownLatch bondingBlocker) {
173             mWaitForBondState = waitForBondState;
174             mBondingBlocker = bondingBlocker;
175         }
176 
177         @Override
onReceive(Context context, Intent intent)178         public void onReceive(Context context, Intent intent) {
179             Log.i(TAG, "onReceive: " + intent.getAction());
180             if (intent.getAction().equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) {
181                 int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 0);
182                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
183                 Log.i(TAG, "onReceive: bondState=" + bondState);
184                 if (device.equals(mServer) && bondState == mWaitForBondState
185                         && mBondingBlocker != null) {
186                     mBondingBlocker.countDown();
187                 }
188             }
189         }
190     };
191 
createBondOob(String uuid, OobData oobData)192     public BluetoothDevice createBondOob(String uuid, OobData oobData) {
193         if (connect(uuid) == null) {
194             Log.e(TAG, "Failed to connect with server");
195             return null;
196         }
197         if (!containsService(uuid)) {
198             Log.e(TAG, "Connected server does not contain the service with UUID: " + uuid);
199             return null;
200         }
201         if (oobData == null) {
202             Log.e(TAG, "createBondOob: No oob data received");
203             return null;
204         }
205         if (mServer == null) {
206             Log.e(TAG, "createBondOob: Device not already connected");
207             return null;
208         }
209         // Bond with the peer (this will block until the bond is complete)
210         CountDownLatch bondingBlocker = new CountDownLatch(1);
211         IntentFilter bondIntentFilter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
212         BroadcastReceiverImpl bondBroadcastReceiver =
213                 new BroadcastReceiverImpl(BOND_BONDED, bondingBlocker);
214         mContext.registerReceiver(bondBroadcastReceiver, bondIntentFilter);
215         if (!mServer.createBondOutOfBand(TRANSPORT_LE, oobData, null)) {
216             Log.e(TAG, "createBondOob: Failed to trigger bonding");
217             return null;
218         }
219         boolean timeout = false;
220         try {
221             timeout = !bondingBlocker.await(CALLBACK_TIMEOUT_SEC, SECONDS);
222         } catch (InterruptedException e) {
223             Log.e(TAG, "Failed to wait for bonding", e);
224             timeout = true;
225         }
226         mContext.unregisterReceiver(bondBroadcastReceiver);
227         if (timeout) {
228             Log.e(TAG, "Did not bond with server");
229             return null;
230         }
231         return mServer;
232     }
233 
removeBond(String uuid)234     public boolean removeBond(String uuid) {
235         if (!containsService(uuid)) {
236             Log.e(TAG, "Connected server does not contain the service with UUID: " + uuid);
237             return false;
238         }
239         CountDownLatch bondingBlocker = new CountDownLatch(1);
240         IntentFilter bondIntentFilter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
241         BroadcastReceiverImpl bondBroadcastReceiver =
242                 new BroadcastReceiverImpl(BOND_NONE, bondingBlocker);
243         mContext.registerReceiver(bondBroadcastReceiver, bondIntentFilter);
244         if (!mServer.removeBond()) {
245             Log.e(TAG, "Failed to remove bond");
246             return false;
247         }
248         boolean timeout = false;
249         try {
250             timeout = !bondingBlocker.await(CALLBACK_TIMEOUT_SEC, SECONDS);
251         } catch (InterruptedException e) {
252             Log.e(TAG, "Failed to wait for bond removal", e);
253             timeout = true;
254         }
255         mContext.unregisterReceiver(bondBroadcastReceiver);
256         if (timeout) {
257             Log.e(TAG, "Did not remove bond with server");
258             return false;
259         }
260         return true;
261     }
262 }
263