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