This can be called multiple times, and each call is associated with a {@link 67 | * AdvertiseCallback} object, which is used to stop the advertising. 68 | * 69 | * @param callbackId 70 | * @param advertiseSettings A JSONObject representing a {@link AdvertiseSettings object}. E.g. 71 | *
72 | * { 73 | * "AdvertiseMode": "ADVERTISE_MODE_BALANCED", 74 | * "Timeout": (int, milliseconds), 75 | * "Connectable": (bool), 76 | * "TxPowerLevel": "ADVERTISE_TX_POWER_LOW" 77 | * } 78 | *79 | * 80 | * @param advertiseData A JSONObject representing a {@link AdvertiseData} object will be 81 | * broadcast if the operation succeeds. E.g. 82 | *
83 | * { 84 | * "IncludeDeviceName": (bool), 85 | * # JSON list, each element representing a set of service data, which is composed of 86 | * # a UUID, and an optional string. 87 | * "ServiceData": [ 88 | * { 89 | * "UUID": (A string representation of {@link ParcelUuid}), 90 | * "Data": (Optional, The string representation of what you want to 91 | * advertise, base64 encoded) 92 | * # If you want to add a UUID without data, simply omit the "Data" 93 | * # field. 94 | * } 95 | * ] 96 | * } 97 | *98 | * 99 | * @param scanResponse A JSONObject representing a {@link AdvertiseData} object which will 100 | * response the data to the scanning device. E.g. 101 | *
102 | * { 103 | * "IncludeDeviceName": (bool), 104 | * # JSON list, each element representing a set of service data, which is composed of 105 | * # a UUID, and an optional string. 106 | * "ServiceData": [ 107 | * { 108 | * "UUID": (A string representation of {@link ParcelUuid}), 109 | * "Data": (Optional, The string representation of what you want to 110 | * advertise, base64 encoded) 111 | * # If you want to add a UUID without data, simply omit the "Data" 112 | * # field. 113 | * } 114 | * ] 115 | * } 116 | *117 | * 118 | * @throws BluetoothLeAdvertiserSnippetException 119 | * @throws JSONException 120 | */ 121 | @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1) 122 | @AsyncRpc(description = "Start BLE advertising.") 123 | public void bleStartAdvertising( 124 | String callbackId, 125 | JSONObject advertiseSettings, 126 | JSONObject advertiseData, 127 | @RpcOptional JSONObject scanResponse) 128 | throws BluetoothLeAdvertiserSnippetException, JSONException { 129 | if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) { 130 | throw new BluetoothLeAdvertiserSnippetException( 131 | "Bluetooth is disabled, cannot start BLE advertising."); 132 | } 133 | AdvertiseSettings settings = JsonDeserializer.jsonToBleAdvertiseSettings(advertiseSettings); 134 | AdvertiseData data = JsonDeserializer.jsonToBleAdvertiseData(advertiseData); 135 | AdvertiseCallback advertiseCallback = new DefaultAdvertiseCallback(callbackId); 136 | if (scanResponse == null) { 137 | mAdvertiser.startAdvertising(settings, data, advertiseCallback); 138 | } else { 139 | AdvertiseData response = JsonDeserializer.jsonToBleAdvertiseData(scanResponse); 140 | mAdvertiser.startAdvertising(settings, data, response, advertiseCallback); 141 | } 142 | mAdvertiseCallbacks.put(callbackId, advertiseCallback); 143 | } 144 | 145 | /** 146 | * Stop a BLE advertising. 147 | * 148 | * @param callbackId The callbackId corresponding to the {@link 149 | * BluetoothLeAdvertiserSnippet#bleStartAdvertising} call that started the advertising. 150 | * @throws BluetoothLeScannerSnippet.BluetoothLeScanSnippetException 151 | */ 152 | @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1) 153 | @Rpc(description = "Stop BLE advertising.") 154 | public void bleStopAdvertising(String callbackId) throws BluetoothLeAdvertiserSnippetException { 155 | AdvertiseCallback callback = mAdvertiseCallbacks.remove(callbackId); 156 | if (callback == null) { 157 | throw new BluetoothLeAdvertiserSnippetException( 158 | "No advertising session found for ID " + callbackId); 159 | } 160 | mAdvertiser.stopAdvertising(callback); 161 | } 162 | 163 | private static class DefaultAdvertiseCallback extends AdvertiseCallback { 164 | private final String mCallbackId; 165 | public static RpcEnum ADVERTISE_FAILURE_ERROR_CODE = 166 | new RpcEnum.Builder() 167 | .add("ADVERTISE_FAILED_ALREADY_STARTED", ADVERTISE_FAILED_ALREADY_STARTED) 168 | .add("ADVERTISE_FAILED_DATA_TOO_LARGE", ADVERTISE_FAILED_DATA_TOO_LARGE) 169 | .add( 170 | "ADVERTISE_FAILED_FEATURE_UNSUPPORTED", 171 | ADVERTISE_FAILED_FEATURE_UNSUPPORTED) 172 | .add("ADVERTISE_FAILED_INTERNAL_ERROR", ADVERTISE_FAILED_INTERNAL_ERROR) 173 | .add( 174 | "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS", 175 | ADVERTISE_FAILED_TOO_MANY_ADVERTISERS) 176 | .build(); 177 | 178 | public DefaultAdvertiseCallback(String callbackId) { 179 | mCallbackId = callbackId; 180 | } 181 | 182 | @Override 183 | public void onStartSuccess(AdvertiseSettings settingsInEffect) { 184 | Log.e("Bluetooth LE advertising started with settings: " + settingsInEffect.toString()); 185 | SnippetEvent event = new SnippetEvent(mCallbackId, "onStartSuccess"); 186 | Bundle advertiseSettings = 187 | JsonSerializer.serializeBleAdvertisingSettings(settingsInEffect); 188 | event.getData().putBundle("SettingsInEffect", advertiseSettings); 189 | sEventCache.postEvent(event); 190 | } 191 | 192 | @Override 193 | public void onStartFailure(int errorCode) { 194 | Log.e("Bluetooth LE advertising failed to start with error code: " + errorCode); 195 | SnippetEvent event = new SnippetEvent(mCallbackId, "onStartFailure"); 196 | final String errorCodeString = ADVERTISE_FAILURE_ERROR_CODE.getString(errorCode); 197 | event.getData().putString("ErrorCode", errorCodeString); 198 | sEventCache.postEvent(event); 199 | } 200 | } 201 | 202 | @Override 203 | public void shutdown() { 204 | for (AdvertiseCallback callback : mAdvertiseCallbacks.values()) { 205 | mAdvertiser.stopAdvertising(callback); 206 | } 207 | mAdvertiseCallbacks.clear(); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 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; 18 | 19 | import android.annotation.TargetApi; 20 | import android.bluetooth.BluetoothAdapter; 21 | import android.bluetooth.le.BluetoothLeScanner; 22 | import android.bluetooth.le.ScanCallback; 23 | import android.bluetooth.le.ScanFilter; 24 | import android.bluetooth.le.ScanResult; 25 | import android.bluetooth.le.ScanSettings; 26 | import android.os.Build; 27 | import android.os.Bundle; 28 | import com.google.android.mobly.snippet.Snippet; 29 | import com.google.android.mobly.snippet.bundled.utils.JsonDeserializer; 30 | import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; 31 | import com.google.android.mobly.snippet.bundled.utils.MbsEnums; 32 | import com.google.android.mobly.snippet.event.EventCache; 33 | import com.google.android.mobly.snippet.event.SnippetEvent; 34 | import com.google.android.mobly.snippet.rpc.AsyncRpc; 35 | import com.google.android.mobly.snippet.rpc.Rpc; 36 | import com.google.android.mobly.snippet.rpc.RpcMinSdk; 37 | import com.google.android.mobly.snippet.rpc.RpcOptional; 38 | import com.google.android.mobly.snippet.util.Log; 39 | import java.util.ArrayList; 40 | import java.util.HashMap; 41 | import java.util.List; 42 | import org.json.JSONArray; 43 | import org.json.JSONException; 44 | import org.json.JSONObject; 45 | 46 | /** Snippet class exposing Android APIs in WifiManager. */ 47 | @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1) 48 | public class BluetoothLeScannerSnippet implements Snippet { 49 | private static class BluetoothLeScanSnippetException extends Exception { 50 | private static final long serialVersionUID = 1; 51 | 52 | public BluetoothLeScanSnippetException(String msg) { 53 | super(msg); 54 | } 55 | } 56 | 57 | private final BluetoothLeScanner mScanner; 58 | private final EventCache mEventCache = EventCache.getInstance(); 59 | private final HashMap
74 | * [ 75 | * { 76 | * "ServiceUuid": (A string representation of {@link ParcelUuid}), 77 | * }, 78 | * ] 79 | *80 | * 81 | * @param scanSettings A JSONObject representing a {@link ScanSettings} object which is the 82 | * Settings for the scan. E.g. 83 | *
84 | * { 85 | * 'ScanMode': 'SCAN_MODE_LOW_LATENCY', 86 | * } 87 | *88 | * 89 | * @throws BluetoothLeScanSnippetException 90 | */ 91 | @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1) 92 | @AsyncRpc(description = "Start BLE scan.") 93 | public void bleStartScan( 94 | String callbackId, 95 | @RpcOptional JSONArray scanFilters, 96 | @RpcOptional JSONObject scanSettings) 97 | throws BluetoothLeScanSnippetException, JSONException { 98 | if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) { 99 | throw new BluetoothLeScanSnippetException( 100 | "Bluetooth is disabled, cannot start BLE scan."); 101 | } 102 | DefaultScanCallback callback = new DefaultScanCallback(callbackId); 103 | if (scanFilters == null && scanSettings == null) { 104 | mScanner.startScan(callback); 105 | } else { 106 | ArrayList
This is useful for strings like the SSID field of Android's Wi-Fi configuration.
69 | *
70 | * @param originalString
71 | */
72 | public static String trimQuotationMarks(String originalString) {
73 | String result = originalString;
74 | if (originalString.length() > 2
75 | && originalString.charAt(0) == '"'
76 | && originalString.charAt(originalString.length() - 1) == '"') {
77 | result = originalString.substring(1, originalString.length() - 1);
78 | }
79 | return result;
80 | }
81 |
82 | public JSONObject toJson(Object object) throws JSONException {
83 | if (object instanceof DhcpInfo) {
84 | return serializeDhcpInfo((DhcpInfo) object);
85 | } else if (object instanceof WifiConfiguration) {
86 | return serializeWifiConfiguration((WifiConfiguration) object);
87 | } else if (object instanceof WifiInfo) {
88 | return serializeWifiInfo((WifiInfo) object);
89 | }
90 | return defaultSerialization(object);
91 | }
92 |
93 | /**
94 | * By default, we rely on Gson to do the right job.
95 | *
96 | * @param data An object to serialize
97 | * @return A JSONObject that has the info of the serialized data object.
98 | * @throws JSONException
99 | */
100 | private JSONObject defaultSerialization(Object data) throws JSONException {
101 | return new JSONObject(gson.toJson(data));
102 | }
103 |
104 | private JSONObject serializeDhcpInfo(DhcpInfo data) throws JSONException {
105 | JSONObject result = new JSONObject(gson.toJson(data));
106 | int ipAddress = data.ipAddress;
107 | byte[] addressBytes = {
108 | (byte) (0xff & ipAddress),
109 | (byte) (0xff & (ipAddress >> 8)),
110 | (byte) (0xff & (ipAddress >> 16)),
111 | (byte) (0xff & (ipAddress >> 24))
112 | };
113 | try {
114 | String addressString = InetAddress.getByAddress(addressBytes).toString();
115 | result.put("IpAddress", addressString);
116 | } catch (UnknownHostException e) {
117 | result.put("IpAddress", ipAddress);
118 | }
119 | return result;
120 | }
121 |
122 | private JSONObject serializeWifiConfiguration(WifiConfiguration data) throws JSONException {
123 | JSONObject result = new JSONObject(gson.toJson(data));
124 | result.put("Status", WifiConfiguration.Status.strings[data.status]);
125 | result.put("SSID", trimQuotationMarks(data.SSID));
126 | return result;
127 | }
128 |
129 | private JSONObject serializeWifiInfo(WifiInfo data) throws JSONException {
130 | JSONObject result = new JSONObject(gson.toJson(data));
131 | result.put("SSID", trimQuotationMarks(data.getSSID()));
132 | for (SupplicantState state : SupplicantState.values()) {
133 | if (data.getSupplicantState().equals(state)) {
134 | result.put("SupplicantState", state.name());
135 | }
136 | }
137 | return result;
138 | }
139 |
140 | public static Bundle serializeBluetoothDevice(BluetoothDevice data) {
141 | Context context = InstrumentationRegistry.getInstrumentation().getContext();
142 | Bundle result = new Bundle();
143 | result.putString("Address", data.getAddress());
144 | if (Build.VERSION.SDK_INT >= 36 &&
145 | context.checkCallingOrSelfPermission(BLUETOOTH_PRIVILEGED_PERMISSION)
146 | == PackageManager.PERMISSION_GRANTED) {
147 | result.putString("IdentityAddress", data.getIdentityAddressWithType().getAddress());
148 | } else {
149 | result.putString("IdentityAddress", null);
150 | }
151 | final String bondState =
152 | MbsEnums.BLUETOOTH_DEVICE_BOND_STATE.getString(data.getBondState());
153 | result.putString("BondState", bondState);
154 | result.putString("Name", data.getName());
155 |
156 | String deviceType = MbsEnums.BLUETOOTH_DEVICE_TYPE.getString(data.getType());
157 | result.putString("DeviceType", deviceType);
158 | ParcelUuid[] parcelUuids = data.getUuids();
159 | if (parcelUuids != null) {
160 | ArrayList Not all fields are serialized here. Will add more as we need.
192 | *
193 | * In Serializing/Deserializing Android API enums, we often need to convert an enum value from
27 | * one form to another. This container class makes it easier to do so.
28 | *
29 | * Once built, an RpcEnum object is immutable.
30 | */
31 | public class RpcEnum {
32 | private final ImmutableBiMap This is often used to wait for asynchronous operations to finish and the system to reach a
46 | * desired state.
47 | *
48 | * If the predicate function throws an exception and interrupts the waiting, the exception
49 | * will be wrapped in an {@link RuntimeException}.
50 | *
51 | * @param predicate A lambda function that specifies the condition to wait for. This function
52 | * should return true when the desired state has been reached.
53 | * @param timeout The number of seconds to wait for before giving up.
54 | * @return true if the operation finished before timeout, false otherwise.
55 | */
56 | public static boolean waitUntil(Utils.Predicate predicate, int timeout) {
57 | timeout *= 10;
58 | try {
59 | while (!predicate.waitCondition() && timeout >= 0) {
60 | Thread.sleep(100);
61 | timeout -= 1;
62 | }
63 | if (predicate.waitCondition()) {
64 | return true;
65 | }
66 | } catch (Throwable e) {
67 | throw new RuntimeException(e);
68 | }
69 | return false;
70 | }
71 |
72 | /**
73 | * Wait on a specific snippet event.
74 | *
75 | * This allows a snippet to wait on another SnippetEvent as long as they know the name and
76 | * callback id. Commonly used to make async calls synchronous, see {@link
77 | * SmsSnippet#waitForSms()} waitForSms} for example usage.
78 | *
79 | * @param callbackId String callbackId that we want to wait on.
80 | * @param eventName String event name that we are waiting on.
81 | * @param timeout int timeout in milliseconds for how long it will wait for the event.
82 | * @return SnippetEvent if one was received.
83 | * @throws Throwable if interrupted while polling for event completion. Throws TimeoutException
84 | * if no snippet event is received.
85 | */
86 | public static SnippetEvent waitForSnippetEvent(
87 | String callbackId, String eventName, Integer timeout) throws Throwable {
88 | String qId = EventCache.getQueueId(callbackId, eventName);
89 | LinkedBlockingDeque Sample usage:
120 | *
121 | * [\x01\x02] -> "0102"
205 | *
206 | * @param bytes The array of byte to convert.
207 | * @return a String with the ASCII hex representation.
208 | */
209 | public static String bytesToHexString(byte[] bytes) {
210 | char[] hexChars = new char[bytes.length * 2];
211 | for (int j = 0; j < bytes.length; j++) {
212 | int v = bytes[j] & 0xFF;
213 | hexChars[j * 2] = hexArray[v >>> 4];
214 | hexChars[j * 2 + 1] = hexArray[v & 0x0F];
215 | }
216 | return new String(hexChars);
217 | }
218 |
219 | public static void adaptShellPermissionIfRequired(Context context) throws Throwable {
220 | if (Build.VERSION.SDK_INT >= 29) {
221 | Log.d("Elevating permission require to enable support for privileged operation in Android Q+");
222 | UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation();
223 | uia.adoptShellPermissionIdentity();
224 | try {
225 | Class> cls = Class.forName("android.app.UiAutomation");
226 | Method destroyMethod = cls.getDeclaredMethod("destroy");
227 | destroyMethod.invoke(uia);
228 | } catch (NoSuchMethodException
229 | | IllegalAccessException
230 | | ClassNotFoundException
231 | | InvocationTargetException e) {
232 | throw new RuntimeException("Failed to cleaup Ui Automation", e);
233 | }
234 | }
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/src/test/java/JsonDeserializerTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2025 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 | import android.bluetooth.BluetoothGattCharacteristic;
18 | import android.bluetooth.BluetoothGattDescriptor;
19 |
20 | import com.google.android.mobly.snippet.bundled.utils.JsonDeserializer;
21 | import com.google.common.truth.Truth;
22 | import java.util.UUID;
23 | import org.json.JSONObject;
24 | import org.junit.Test;
25 | import org.junit.runner.RunWith;
26 | import org.robolectric.annotation.Config;
27 | import org.robolectric.RobolectricTestRunner;
28 |
29 | @RunWith(RobolectricTestRunner.class)
30 | @Config(minSdk = 33)
31 | public class JsonDeserializerTest {
32 | @Test
33 | public void testCharacteristicWithPropertiesPermissions() throws Throwable {
34 | String uuid = "ffffffff-ffff-ffff-ffff-ffffffffffff";
35 |
36 | JSONObject json = new JSONObject();
37 | json.put("UUID", uuid);
38 | json.put("Properties", "PROPERTY_READ");
39 | json.put("Permissions", "PERMISSION_READ");
40 |
41 | BluetoothGattCharacteristic characteristic = JsonDeserializer.jsonToBluetoothGattCharacteristic(null, json);
42 | Truth.assertThat(characteristic.getUuid()).isEqualTo(UUID.fromString(uuid));
43 | Truth.assertThat(characteristic.getProperties()).isEqualTo(BluetoothGattCharacteristic.PROPERTY_READ);
44 | Truth.assertThat(characteristic.getPermissions()).isEqualTo(BluetoothGattCharacteristic.PERMISSION_READ);
45 | }
46 |
47 | @Test
48 | public void testCharacteristicWithMultiplePropertiesPermissions() throws Throwable {
49 | String uuid = "ffffffff-ffff-ffff-ffff-ffffffffffff";
50 |
51 | JSONObject json = new JSONObject();
52 | json.put("UUID", uuid);
53 | json.put("Properties", "PROPERTY_READ|PROPERTY_WRITE");
54 | json.put("Permissions", "PERMISSION_READ|PERMISSION_WRITE");
55 |
56 | BluetoothGattCharacteristic characteristic = JsonDeserializer.jsonToBluetoothGattCharacteristic(null, json);
57 | Truth.assertThat(characteristic.getUuid()).isEqualTo(UUID.fromString(uuid));
58 | Truth.assertThat(characteristic.getProperties()).isEqualTo(BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE);
59 | Truth.assertThat(characteristic.getPermissions()).isEqualTo(BluetoothGattCharacteristic.PERMISSION_READ |BluetoothGattCharacteristic.PERMISSION_WRITE);
60 | }
61 |
62 | @Test
63 | public void testDescriptor() throws Throwable {
64 | String jsonString =
65 | "{" +
66 | " \"UUID\": \"ffffffff-ffff-ffff-ffff-ffffffffffff\"," +
67 | " \"Permissions\": \"PERMISSION_READ|PERMISSION_WRITE\"" +
68 | "}";
69 |
70 | BluetoothGattDescriptor descriptor = JsonDeserializer.jsonToBluetoothGattDescriptor(new JSONObject(jsonString));
71 | Truth.assertThat(descriptor.getUuid()).isEqualTo(UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff"));
72 | Truth.assertThat(descriptor.getPermissions()).isEqualTo(BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE);
73 | }
74 |
75 | @Test
76 | public void testCharacteristicNoDescriptors() throws Throwable {
77 | String jsonString =
78 | "{" +
79 | " \"UUID\": \"ffffffff-ffff-ffff-ffff-ffffffffffff\"," +
80 | " \"Properties\":\"PROPERTY_READ|PROPERTY_WRITE\"," +
81 | " \"Permissions\": \"PERMISSION_READ|PERMISSION_WRITE\"" +
82 | "}";
83 |
84 | BluetoothGattCharacteristic characteristic = JsonDeserializer.jsonToBluetoothGattCharacteristic(null, new JSONObject(jsonString));
85 | Truth.assertThat(characteristic.getUuid()).isEqualTo(UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff"));
86 | Truth.assertThat(characteristic.getProperties()).isEqualTo(BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE);
87 | Truth.assertThat(characteristic.getPermissions()).isEqualTo(BluetoothGattCharacteristic.PERMISSION_READ | BluetoothGattCharacteristic.PERMISSION_WRITE);
88 | Truth.assertThat(characteristic.getDescriptors()).isEmpty();
89 | }
90 |
91 | @Test
92 | public void testCharacteristicEmptyListDescriptors() throws Throwable {
93 | String jsonString =
94 | "{" +
95 | " \"UUID\": \"ffffffff-ffff-ffff-ffff-ffffffffffff\"," +
96 | " \"Properties\":\"PROPERTY_READ|PROPERTY_WRITE\"," +
97 | " \"Permissions\": \"PERMISSION_READ|PERMISSION_WRITE\"," +
98 | " \"Descriptors\": []" +
99 | "}";
100 |
101 | BluetoothGattCharacteristic characteristic = JsonDeserializer.jsonToBluetoothGattCharacteristic(null, new JSONObject(jsonString));
102 | Truth.assertThat(characteristic.getUuid()).isEqualTo(UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff"));
103 | Truth.assertThat(characteristic.getProperties()).isEqualTo(BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE);
104 | Truth.assertThat(characteristic.getPermissions()).isEqualTo(BluetoothGattCharacteristic.PERMISSION_READ | BluetoothGattCharacteristic.PERMISSION_WRITE);
105 | Truth.assertThat(characteristic.getDescriptors()).isEmpty();
106 | }
107 |
108 | @Test
109 | public void testCharacteristic1Descriptor() throws Throwable {
110 | String jsonString =
111 | "{" +
112 | " \"UUID\": \"ffffffff-ffff-ffff-ffff-ffffffffffff\"," +
113 | " \"Properties\":\"PROPERTY_READ|PROPERTY_WRITE\"," +
114 | " \"Permissions\": \"PERMISSION_READ|PERMISSION_WRITE\"," +
115 | " \"Descriptors\":" +
116 | " [" +
117 | " {" +
118 | " \"UUID\": \"dddddddd-dddd-dddd-dddd-dddddddddddd\"," +
119 | " \"Permissions\": \"PERMISSION_READ|PERMISSION_WRITE\"" +
120 | " }" +
121 | " ]" +
122 | "}";
123 |
124 | BluetoothGattCharacteristic characteristic = JsonDeserializer.jsonToBluetoothGattCharacteristic(null, new JSONObject(jsonString));
125 | Truth.assertThat(characteristic.getUuid()).isEqualTo(UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff"));
126 | Truth.assertThat(characteristic.getProperties()).isEqualTo(BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE);
127 | Truth.assertThat(characteristic.getPermissions()).isEqualTo(BluetoothGattCharacteristic.PERMISSION_READ | BluetoothGattCharacteristic.PERMISSION_WRITE);
128 | Truth.assertThat(characteristic.getDescriptors().size()).isEqualTo(1);
129 | Truth.assertThat(characteristic.getDescriptors().get(0).getUuid()).isEqualTo(UUID.fromString("dddddddd-dddd-dddd-dddd-dddddddddddd"));
130 | Truth.assertThat(characteristic.getDescriptors().get(0).getPermissions()).isEqualTo(BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE);
131 | }
132 | @Test
133 | public void testCharacteristic2Descriptors() throws Throwable {
134 | String jsonString =
135 | "{" +
136 | " \"UUID\": \"ffffffff-ffff-ffff-ffff-ffffffffffff\"," +
137 | " \"Properties\":\"PROPERTY_READ|PROPERTY_WRITE\"," +
138 | " \"Permissions\": \"PERMISSION_READ|PERMISSION_WRITE\"," +
139 | " \"Descriptors\":" +
140 | " [" +
141 | " {" +
142 | " \"UUID\": \"dddddddd-dddd-dddd-dddd-dddddddddddd\"," +
143 | " \"Permissions\": \"PERMISSION_READ|PERMISSION_WRITE\"" +
144 | " }," +
145 | " {" +
146 | " \"UUID\": \"eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee\"," +
147 | " \"Permissions\": \"PERMISSION_READ\"" +
148 | " }" +
149 | " ]" +
150 | "}";
151 |
152 | BluetoothGattCharacteristic characteristic = JsonDeserializer.jsonToBluetoothGattCharacteristic(null, new JSONObject(jsonString));
153 | Truth.assertThat(characteristic.getUuid()).isEqualTo(UUID.fromString("ffffffff-ffff-ffff-ffff-ffffffffffff"));
154 | Truth.assertThat(characteristic.getProperties()).isEqualTo(BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE);
155 | Truth.assertThat(characteristic.getPermissions()).isEqualTo(BluetoothGattCharacteristic.PERMISSION_READ | BluetoothGattCharacteristic.PERMISSION_WRITE);
156 | Truth.assertThat(characteristic.getDescriptors().size()).isEqualTo(2);
157 | Truth.assertThat(characteristic.getDescriptors().get(0).getUuid()).isEqualTo(UUID.fromString("dddddddd-dddd-dddd-dddd-dddddddddddd"));
158 | Truth.assertThat(characteristic.getDescriptors().get(0).getPermissions()).isEqualTo(BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE);
159 | Truth.assertThat(characteristic.getDescriptors().get(1).getUuid()).isEqualTo(UUID.fromString("eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"));
160 | Truth.assertThat(characteristic.getDescriptors().get(1).getPermissions()).isEqualTo(BluetoothGattDescriptor.PERMISSION_READ);
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/src/test/java/MbsEnumsTest.java:
--------------------------------------------------------------------------------
1 | import android.bluetooth.BluetoothGattCharacteristic;
2 | import android.os.Build.VERSION_CODES;
3 | import com.google.android.mobly.snippet.bundled.utils.MbsEnums;
4 | import com.google.common.truth.Truth;
5 | import org.junit.Test;
6 | import org.junit.runner.RunWith;
7 | import androidx.test.runner.AndroidJUnitRunner;
8 | import org.junit.runners.JUnit4;
9 | import org.robolectric.annotation.Config;
10 | import org.robolectric.RobolectricTestRunner;
11 |
12 | @RunWith(RobolectricTestRunner.class)
13 | @Config(minSdk = 33)
14 | public class MbsEnumsTest {
15 | @Test
16 | public void testGetIntBitwiseOrValid() throws Throwable {
17 | Truth.assertThat(MbsEnums.BLE_PROPERTY_TYPE.getIntBitwiseOr("PROPERTY_READ|PROPERTY_NOTIFY")).isEqualTo(BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_NOTIFY);
18 | Truth.assertThat(MbsEnums.BLE_PROPERTY_TYPE.getIntBitwiseOr("PROPERTY_READ")).isEqualTo(BluetoothGattCharacteristic.PROPERTY_READ);
19 | }
20 |
21 | @Test
22 | public void testGetIntBitwiseOrInvalid() throws Throwable {
23 | Throwable thrown = null;
24 | try {
25 | MbsEnums.BLE_PROPERTY_TYPE.getIntBitwiseOr("PROPERTY_NOTHING");
26 | } catch (Throwable t) {
27 | thrown = t;
28 | }
29 | Truth.assertThat(thrown).isInstanceOf(NoSuchFieldError.class);
30 | }
31 |
32 | @Test
33 | public void testGetIntBitwiseOrInvalid2() throws Throwable {
34 | Throwable thrown = null;
35 | try {
36 | MbsEnums.BLE_PROPERTY_TYPE.getIntBitwiseOr("PROPERTY_READ|PROPERTY_NOTHING");
37 | } catch (Throwable t) {
38 | thrown = t;
39 | }
40 | Truth.assertThat(thrown).isInstanceOf(NoSuchFieldError.class);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/test/java/UtilsTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 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 | import static com.google.android.mobly.snippet.bundled.utils.Utils.invokeByReflection;
18 |
19 | import com.google.android.mobly.snippet.bundled.utils.Utils;
20 | import com.google.common.truth.Truth;
21 | import java.io.IOException;
22 | import java.util.Collections;
23 | import java.util.List;
24 | import org.junit.Assert;
25 | import org.junit.Test;
26 |
27 | /** Tests for {@link com.google.android.mobly.snippet.bundled.utils.Utils} */
28 | public class UtilsTest {
29 | public static final class ReflectionTest_HostClass {
30 | public Object returnSame(ListThe returned {@link Bundle} has the following info:
194 | * "DeviceName", String
195 | * "TxPowerLevel", String
196 | *
197 | *
198 | * @param record A {@link ScanRecord} object.
199 | * @return A {@link Bundle} object.
200 | */
201 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
202 | private Bundle serializeBleScanRecord(ScanRecord record) {
203 | Bundle result = new Bundle();
204 | result.putString("DeviceName", record.getDeviceName());
205 | result.putInt("TxPowerLevel", record.getTxPowerLevel());
206 | result.putParcelableArrayList("Services", serializeBleScanServices(record));
207 | result.putBundle(
208 | "manufacturerSpecificData", serializeBleScanManufacturerSpecificData(record));
209 | return result;
210 | }
211 |
212 | /** Serialize manufacturer specific data from ScanRecord for Bluetooth LE. */
213 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
214 | private ArrayList
122 | * boolean result = (boolean) Utils.invokeByReflection(
123 | * mWifiManager,
124 | * "setWifiApEnabled", null /* wifiConfiguration * /, true /* enabled * /);
125 | *
126 | *
127 | * @param instance Instance of object defining the method to call.
128 | * @param methodName Name of the method to call. Can be inherited.
129 | * @param args Variadic array of arguments to supply to the method. Their types will be used to
130 | * locate a suitable method to call. Subtypes, primitive types, boxed types, and {@code
131 | * null} arguments are properly handled.
132 | * @return The return value of the method, or {@code null} if no return value.
133 | * @throws NoSuchMethodException If no suitable method could be found.
134 | * @throws Throwable The exception raised by the method, if any.
135 | */
136 | public static Object invokeByReflection(Object instance, String methodName, Object... args)
137 | throws Throwable {
138 | // Java doesn't know if invokeByReflection(instance, name, null) means that the array is
139 | // null or that it's a non-null array containing a single null element. We mean the latter.
140 | // Silly Java.
141 | if (args == null) {
142 | args = new Object[] {null};
143 | }
144 | // Can't use Class#getMethod(Class>...) because it expects that the passed in classes
145 | // exactly match the parameters of the method, and doesn't handle superclasses.
146 | Method method = null;
147 | METHOD_SEARCHER:
148 | for (Method candidateMethod : instance.getClass().getMethods()) {
149 | // getMethods() returns only public methods, so we don't need to worry about checking
150 | // whether the method is accessible.
151 | if (!candidateMethod.getName().equals(methodName)) {
152 | continue;
153 | }
154 | Class>[] declaredParams = candidateMethod.getParameterTypes();
155 | if (declaredParams.length != args.length) {
156 | continue;
157 | }
158 | for (int i = 0; i < declaredParams.length; i++) {
159 | if (args[i] == null) {
160 | // Null is assignable to anything except primitives.
161 | if (declaredParams[i].isPrimitive()) {
162 | continue METHOD_SEARCHER;
163 | }
164 | } else {
165 | // Allow autoboxing during reflection by wrapping primitives.
166 | Class> declaredClass = Primitives.wrap(declaredParams[i]);
167 | Class> actualClass = Primitives.wrap(args[i].getClass());
168 | TypeToken> declaredParamType = TypeToken.of(declaredClass);
169 | TypeToken> actualParamType = TypeToken.of(actualClass);
170 | if (!declaredParamType.isSupertypeOf(actualParamType)) {
171 | continue METHOD_SEARCHER;
172 | }
173 | }
174 | }
175 | method = candidateMethod;
176 | break;
177 | }
178 | if (method == null) {
179 | StringBuilder methodString =
180 | new StringBuilder(instance.getClass().getName())
181 | .append('#')
182 | .append(methodName)
183 | .append('(');
184 | for (int i = 0; i < args.length - 1; i++) {
185 | methodString.append(args[i].getClass().getSimpleName()).append(", ");
186 | }
187 | if (args.length > 0) {
188 | methodString.append(args[args.length - 1].getClass().getSimpleName());
189 | }
190 | methodString.append(')');
191 | throw new NoSuchMethodException(methodString.toString());
192 | }
193 | try {
194 | Object result = method.invoke(instance, args);
195 | return result;
196 | } catch (InvocationTargetException e) {
197 | throw e.getCause();
198 | }
199 | }
200 |
201 | /**
202 | * Convert a byte array (binary data) to a hexadecimal string (ASCII) representation.
203 | *
204 | *