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