1 /* 2 * Copyright (C) 2023 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.google.android.mobly.snippet.bundled.bluetooth; 18 19 import android.bluetooth.BluetoothAdapter; 20 import android.bluetooth.BluetoothDevice; 21 import android.bluetooth.BluetoothGatt; 22 import android.bluetooth.BluetoothGattCallback; 23 import android.bluetooth.BluetoothGattCharacteristic; 24 import android.bluetooth.BluetoothGattService; 25 import android.bluetooth.BluetoothProfile; 26 import android.content.Context; 27 import android.os.Build.VERSION_CODES; 28 import android.os.Bundle; 29 import android.os.SystemClock; 30 import android.util.Base64; 31 import androidx.test.platform.app.InstrumentationRegistry; 32 import com.google.android.mobly.snippet.Snippet; 33 import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; 34 import com.google.android.mobly.snippet.bundled.utils.MbsEnums; 35 import com.google.android.mobly.snippet.event.EventCache; 36 import com.google.android.mobly.snippet.event.SnippetEvent; 37 import com.google.android.mobly.snippet.rpc.AsyncRpc; 38 import com.google.android.mobly.snippet.rpc.Rpc; 39 import com.google.android.mobly.snippet.rpc.RpcMinSdk; 40 import com.google.android.mobly.snippet.util.Log; 41 import java.util.ArrayList; 42 import java.util.HashMap; 43 import org.json.JSONException; 44 45 /** Snippet class exposing Android APIs in BluetoothGatt. */ 46 public class BluetoothGattClientSnippet implements Snippet { 47 private static class BluetoothGattClientSnippetException extends Exception { 48 private static final long serialVersionUID = 1; 49 BluetoothGattClientSnippetException(String msg)50 public BluetoothGattClientSnippetException(String msg) { 51 super(msg); 52 } 53 } 54 55 private final Context context; 56 private final EventCache eventCache; 57 private final HashMap<String, HashMap<String, BluetoothGattCharacteristic>> 58 characteristicHashMap; 59 60 private BluetoothGatt bluetoothGattClient; 61 62 private long connectionStartTime = 0; 63 private long connectionEndTime = 0; 64 BluetoothGattClientSnippet()65 public BluetoothGattClientSnippet() { 66 context = InstrumentationRegistry.getInstrumentation().getContext(); 67 eventCache = EventCache.getInstance(); 68 characteristicHashMap = new HashMap<>(); 69 } 70 71 @RpcMinSdk(VERSION_CODES.LOLLIPOP) 72 @AsyncRpc(description = "Start BLE client.") bleConnectGatt(String callbackId, String deviceAddress)73 public void bleConnectGatt(String callbackId, String deviceAddress) throws JSONException { 74 BluetoothDevice remoteDevice = 75 BluetoothAdapter.getDefaultAdapter().getRemoteDevice(deviceAddress); 76 BluetoothGattCallback gattCallback = new DefaultBluetoothGattCallback(callbackId); 77 connectionStartTime = System.currentTimeMillis(); 78 bluetoothGattClient = remoteDevice.connectGatt(context, false, gattCallback); 79 Log.d("Connection start time is " + connectionStartTime); 80 connectionEndTime = 0; 81 } 82 83 @RpcMinSdk(VERSION_CODES.LOLLIPOP) 84 @Rpc(description = "Start BLE service discovery") bleDiscoverServices()85 public long bleDiscoverServices() throws BluetoothGattClientSnippetException { 86 if (bluetoothGattClient == null) { 87 throw new BluetoothGattClientSnippetException("BLE client is not initialized."); 88 } 89 long discoverServicesStartTime = SystemClock.elapsedRealtimeNanos(); 90 Log.d("Discover services start time is " + discoverServicesStartTime); 91 boolean result = bluetoothGattClient.discoverServices(); 92 if (!result) { 93 throw new BluetoothGattClientSnippetException("Discover services returned false."); 94 } 95 return discoverServicesStartTime; 96 } 97 98 @RpcMinSdk(VERSION_CODES.LOLLIPOP) 99 @Rpc(description = "Stop BLE client.") bleDisconnect()100 public void bleDisconnect() throws BluetoothGattClientSnippetException { 101 if (bluetoothGattClient == null) { 102 throw new BluetoothGattClientSnippetException("BLE client is not initialized."); 103 } 104 bluetoothGattClient.disconnect(); 105 } 106 107 @RpcMinSdk(VERSION_CODES.LOLLIPOP) 108 @Rpc(description = "BLE read operation.") bleReadOperation(String serviceUuid, String characteristicUuid)109 public boolean bleReadOperation(String serviceUuid, String characteristicUuid) 110 throws JSONException, BluetoothGattClientSnippetException { 111 if (bluetoothGattClient == null) { 112 throw new BluetoothGattClientSnippetException("BLE client is not initialized."); 113 } 114 boolean result = 115 bluetoothGattClient.readCharacteristic( 116 characteristicHashMap.get(serviceUuid).get(characteristicUuid)); 117 Log.d("Read operation returned result " + result); 118 return result; 119 } 120 121 @RpcMinSdk(VERSION_CODES.LOLLIPOP) 122 @Rpc(description = "BLE write operation.") bleWriteOperation(String serviceUuid, String characteristicUuid, String data)123 public boolean bleWriteOperation(String serviceUuid, String characteristicUuid, String data) 124 throws JSONException, BluetoothGattClientSnippetException { 125 if (bluetoothGattClient == null) { 126 throw new BluetoothGattClientSnippetException("BLE client is not initialized."); 127 } 128 BluetoothGattCharacteristic characteristic = 129 characteristicHashMap.get(serviceUuid).get(characteristicUuid); 130 characteristic.setValue(Base64.decode(data, Base64.NO_WRAP)); 131 boolean result = bluetoothGattClient.writeCharacteristic(characteristic); 132 Log.d("Write operation returned result " + result); 133 return result; 134 } 135 136 private class DefaultBluetoothGattCallback extends BluetoothGattCallback { 137 private final String callbackId; 138 DefaultBluetoothGattCallback(String callbackId)139 DefaultBluetoothGattCallback(String callbackId) { 140 this.callbackId = callbackId; 141 } 142 143 @Override onConnectionStateChange(BluetoothGatt gatt, int status, int newState)144 public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { 145 SnippetEvent event = new SnippetEvent(callbackId, "onConnectionStateChange"); 146 if (newState == BluetoothProfile.STATE_CONNECTED) { 147 connectionEndTime = System.currentTimeMillis(); 148 event.getData().putLong( 149 "gattConnectionTimeMs", connectionEndTime - connectionStartTime); 150 Log.d("Connection end time is " + connectionEndTime); 151 } 152 event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status)); 153 event.getData().putString("newState", MbsEnums.BLE_CONNECT_STATUS.getString(newState)); 154 event.getData().putBundle("gatt", JsonSerializer.serializeBluetoothGatt(gatt)); 155 eventCache.postEvent(event); 156 } 157 158 @Override onServicesDiscovered(BluetoothGatt gatt, int status)159 public void onServicesDiscovered(BluetoothGatt gatt, int status) { 160 long discoverServicesEndTime = SystemClock.elapsedRealtimeNanos(); 161 Log.d("Discover services end time is " + discoverServicesEndTime); 162 SnippetEvent event = new SnippetEvent(callbackId, "onServiceDiscovered"); 163 event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status)); 164 ArrayList<Bundle> services = new ArrayList<>(); 165 for (BluetoothGattService service : gatt.getServices()) { 166 HashMap<String, BluetoothGattCharacteristic> characteristics = new HashMap<>(); 167 for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) { 168 characteristics.put(characteristic.getUuid().toString(), characteristic); 169 } 170 characteristicHashMap.put(service.getUuid().toString(), characteristics); 171 services.add(JsonSerializer.serializeBluetoothGattService(service)); 172 } 173 // TODO(66740428): Should not return services directly 174 event.getData().putParcelableArrayList("Services", services); 175 event.getData().putBundle("gatt", JsonSerializer.serializeBluetoothGatt(gatt)); 176 event.getData().putLong("discoveryServicesEndTime", discoverServicesEndTime); 177 eventCache.postEvent(event); 178 } 179 180 @Override onCharacteristicRead( BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)181 public void onCharacteristicRead( 182 BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { 183 SnippetEvent event = new SnippetEvent(callbackId, "onCharacteristicRead"); 184 event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status)); 185 // TODO(66740428): Should return the characteristic instead of value 186 event.getData() 187 .putString("Data", 188 Base64.encodeToString(characteristic.getValue(), Base64.NO_WRAP)); 189 event.getData().putBundle("gatt", JsonSerializer.serializeBluetoothGatt(gatt)); 190 eventCache.postEvent(event); 191 } 192 193 @Override onCharacteristicWrite( BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)194 public void onCharacteristicWrite( 195 BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { 196 SnippetEvent event = new SnippetEvent(callbackId, "onCharacteristicWrite"); 197 event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status)); 198 // TODO(66740428): Should return the characteristic instead of value 199 event.getData().putBundle("gatt", JsonSerializer.serializeBluetoothGatt(gatt)); 200 eventCache.postEvent(event); 201 } 202 203 @Override onReliableWriteCompleted(BluetoothGatt gatt, int status)204 public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { 205 SnippetEvent event = new SnippetEvent(callbackId, "onReliableWriteCompleted"); 206 event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status)); 207 event.getData().putBundle("gatt", JsonSerializer.serializeBluetoothGatt(gatt)); 208 eventCache.postEvent(event); 209 } 210 } 211 212 @Override shutdown()213 public void shutdown() { 214 if (bluetoothGattClient != null) { 215 bluetoothGattClient.close(); 216 } 217 } 218 } 219