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.BluetoothDevice;
20 import android.bluetooth.BluetoothGatt;
21 import android.bluetooth.BluetoothGattCharacteristic;
22 import android.bluetooth.BluetoothGattServer;
23 import android.bluetooth.BluetoothGattServerCallback;
24 import android.bluetooth.BluetoothGattService;
25 import android.bluetooth.BluetoothManager;
26 import android.content.Context;
27 import android.os.Build.VERSION_CODES;
28 import android.os.DeadObjectException;
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.DataHolder;
34 import com.google.android.mobly.snippet.bundled.utils.JsonDeserializer;
35 import com.google.android.mobly.snippet.bundled.utils.JsonSerializer;
36 import com.google.android.mobly.snippet.bundled.utils.MbsEnums;
37 import com.google.android.mobly.snippet.event.EventCache;
38 import com.google.android.mobly.snippet.event.SnippetEvent;
39 import com.google.android.mobly.snippet.rpc.AsyncRpc;
40 import com.google.android.mobly.snippet.rpc.Rpc;
41 import com.google.android.mobly.snippet.rpc.RpcMinSdk;
42 import com.google.android.mobly.snippet.util.Log;
43 import org.json.JSONArray;
44 import org.json.JSONException;
45 import org.json.JSONObject;
46 
47 /** Snippet class exposing Android APIs in BluetoothGattServer. */
48 public class BluetoothGattServerSnippet implements Snippet {
49     private static class BluetoothGattServerSnippetException extends Exception {
50         private static final long serialVersionUID = 1;
51 
BluetoothGattServerSnippetException(String msg)52         public BluetoothGattServerSnippetException(String msg) {
53             super(msg);
54         }
55     }
56 
57     private final Context context;
58     private final BluetoothManager bluetoothManager;
59     private final DataHolder dataHolder;
60     private final EventCache eventCache;
61 
62     private BluetoothGattServer bluetoothGattServer;
63 
BluetoothGattServerSnippet()64     public BluetoothGattServerSnippet() {
65         context = InstrumentationRegistry.getInstrumentation().getContext();
66         bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
67         dataHolder = new DataHolder();
68         eventCache = EventCache.getInstance();
69     }
70 
71     @RpcMinSdk(VERSION_CODES.LOLLIPOP)
72     @AsyncRpc(description = "Start BLE server.")
bleStartServer(String callbackId, JSONArray services)73     public void bleStartServer(String callbackId, JSONArray services)
74             throws JSONException, DeadObjectException {
75         BluetoothGattServerCallback gattServerCallback =
76                 new DefaultBluetoothGattServerCallback(callbackId);
77         bluetoothGattServer = bluetoothManager.openGattServer(context, gattServerCallback);
78         addServiceToGattServer(services);
79     }
80 
81     @RpcMinSdk(VERSION_CODES.LOLLIPOP)
82     @AsyncRpc(description = "Start BLE server with workaround.")
bleStartServerWithWorkaround(String callbackId, JSONArray services)83     public void bleStartServerWithWorkaround(String callbackId, JSONArray services)
84             throws JSONException, DeadObjectException {
85         BluetoothGattServerCallback gattServerCallback =
86                 new DefaultBluetoothGattServerCallback(callbackId);
87         boolean isGattServerStarted = false;
88         int count = 0;
89         while (!isGattServerStarted && count < 5) {
90             bluetoothGattServer = bluetoothManager.openGattServer(context, gattServerCallback);
91             if (bluetoothGattServer != null) {
92                 addServiceToGattServer(services);
93                 isGattServerStarted = true;
94             } else {
95                 SystemClock.sleep(1000);
96                 count++;
97             }
98         }
99     }
100 
addServiceToGattServer(JSONArray services)101     private void addServiceToGattServer(JSONArray services) throws JSONException {
102         for (int i = 0; i < services.length(); i++) {
103             JSONObject service = services.getJSONObject(i);
104             BluetoothGattService bluetoothGattService =
105                     JsonDeserializer.jsonToBluetoothGattService(dataHolder, service);
106             bluetoothGattServer.addService(bluetoothGattService);
107         }
108     }
109 
110     @RpcMinSdk(VERSION_CODES.LOLLIPOP)
111     @Rpc(description = "Stop BLE server.")
bleStopServer()112     public void bleStopServer() throws BluetoothGattServerSnippetException {
113         if (bluetoothGattServer == null) {
114             throw new BluetoothGattServerSnippetException("BLE server is not initialized.");
115         }
116         bluetoothGattServer.close();
117     }
118 
119     private class DefaultBluetoothGattServerCallback extends BluetoothGattServerCallback {
120         private final String callbackId;
121 
DefaultBluetoothGattServerCallback(String callbackId)122         DefaultBluetoothGattServerCallback(String callbackId) {
123             this.callbackId = callbackId;
124         }
125 
126         @Override
onConnectionStateChange(BluetoothDevice device, int status, int newState)127         public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
128             SnippetEvent event = new SnippetEvent(callbackId, "onConnectionStateChange");
129             event.getData().putBundle("device", JsonSerializer.serializeBluetoothDevice(device));
130             event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status));
131             event.getData().putString("newState", MbsEnums.BLE_CONNECT_STATUS.getString(newState));
132             eventCache.postEvent(event);
133         }
134 
135         @Override
onServiceAdded(int status, BluetoothGattService service)136         public void onServiceAdded(int status, BluetoothGattService service) {
137             Log.d("Bluetooth Gatt Server service added with status " + status);
138             SnippetEvent event = new SnippetEvent(callbackId, "onServiceAdded");
139             event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status));
140             event.getData()
141                     .putParcelable("Service",
142                                   JsonSerializer.serializeBluetoothGattService(service));
143             eventCache.postEvent(event);
144         }
145 
146         @Override
onCharacteristicReadRequest( BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic)147         public void onCharacteristicReadRequest(
148                 BluetoothDevice device,
149                 int requestId,
150                 int offset,
151                 BluetoothGattCharacteristic characteristic) {
152             Log.d("Bluetooth Gatt Server received a read request");
153             if (dataHolder.get(characteristic) != null) {
154                 bluetoothGattServer.sendResponse(
155                         device,
156                         requestId,
157                         BluetoothGatt.GATT_SUCCESS,
158                         offset,
159                         Base64.decode(dataHolder.get(characteristic), Base64.NO_WRAP));
160             } else {
161                 bluetoothGattServer.sendResponse(
162                         device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
163             }
164         }
165 
166         @Override
onCharacteristicWriteRequest( BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value)167         public void onCharacteristicWriteRequest(
168                 BluetoothDevice device,
169                 int requestId,
170                 BluetoothGattCharacteristic characteristic,
171                 boolean preparedWrite,
172                 boolean responseNeeded,
173                 int offset,
174                 byte[] value) {
175             Log.d("Bluetooth Gatt Server received a write request");
176             bluetoothGattServer.sendResponse(
177                     device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
178             SnippetEvent event = new SnippetEvent(callbackId, "onCharacteristicWriteRequest");
179             event.getData().putString("Data", Base64.encodeToString(value, Base64.NO_WRAP));
180             eventCache.postEvent(event);
181         }
182 
183         @Override
onExecuteWrite(BluetoothDevice device, int requestId, boolean execute)184         public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
185             Log.d("Bluetooth Gatt Server received an execute write request");
186             bluetoothGattServer.sendResponse(
187                     device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null);
188         }
189     }
190 
191     @Override
shutdown()192     public void shutdown() {}
193 }
194