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