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