aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-08-01 23:08:30 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-08-01 23:08:30 +0000
commit6f86ee0c341bd7f37c57d40f064a47a41928f1bd (patch)
tree96e4fe0ad770a3fdc1f1850c05a0c77b88f23de5
parent9db2ea350918eaecdf676188364a26d0974c7315 (diff)
parented17dbb8710329d4d841986f903620dea4071174 (diff)
downloadmobly-bundled-snippets-android14-qpr1-s2-release.tar.gz
Change-Id: I575c244d26ea4d9dfb5e2c4b55da8f9f54933d48
-rw-r--r--Android.bp5
-rw-r--r--METADATA4
-rw-r--r--src/main/AndroidManifest.xml8
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java37
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java50
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java36
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java37
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java51
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothGattClientSnippet.java218
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothGattServerSnippet.java193
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/PairingBroadcastReceiver.java4
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/utils/DataHolder.java38
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java53
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java69
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java164
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java16
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java24
17 files changed, 942 insertions, 65 deletions
diff --git a/Android.bp b/Android.bp
index a3d1737..be5cdf7 100644
--- a/Android.bp
+++ b/Android.bp
@@ -32,10 +32,11 @@ android_library {
name: "mobly-bundled-snippets-lib",
static_libs: [
"androidx.test.runner",
- "gson",
+ "androidx.test.uiautomator_uiautomator",
+ "error_prone_annotations",
+ "gson",
"guava",
"mobly-snippet-lib",
- "androidx.test.uiautomator_uiautomator",
],
srcs: [
"src/main/**/*.java",
diff --git a/METADATA b/METADATA
index 51a155a..d606bbe 100644
--- a/METADATA
+++ b/METADATA
@@ -13,7 +13,7 @@ third_party {
type: GIT
value: "https://github.com/google/mobly-bundled-snippets"
}
- version: "fbd882fa01fb2134303c697195ab93b739e4ee87"
- last_upgrade_date { year: 2023 month: 5 day: 31 }
+ version: "363a22ae26a277dfbf6c7a0c6596d1a7c08a39f1"
+ last_upgrade_date { year: 2023 month: 7 day: 26 }
license_type: NOTICE
}
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 08341c3..8c13914 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -5,6 +5,8 @@
<uses-feature android:name="android.hardware.telephony" android:required="false" />
+ <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
@@ -12,8 +14,10 @@
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
@@ -21,6 +25,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+ <uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
@@ -34,9 +39,12 @@
<application>
<meta-data
android:name="mobly-snippets"
+ android:testOnly="true"
android:value="com.google.android.mobly.snippet.bundled.AccountSnippet,
com.google.android.mobly.snippet.bundled.AudioSnippet,
com.google.android.mobly.snippet.bundled.bluetooth.BluetoothAdapterSnippet,
+ com.google.android.mobly.snippet.bundled.bluetooth.BluetoothGattClientSnippet,
+ com.google.android.mobly.snippet.bundled.bluetooth.BluetoothGattServerSnippet,
com.google.android.mobly.snippet.bundled.bluetooth.profiles.BluetoothA2dpSnippet,
com.google.android.mobly.snippet.bundled.bluetooth.profiles.BluetoothHearingAidSnippet,
com.google.android.mobly.snippet.bundled.BluetoothLeAdvertiserSnippet,
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java
index e161a5b..4b2c5b5 100644
--- a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeAdvertiserSnippet.java
@@ -34,6 +34,7 @@ import com.google.android.mobly.snippet.event.SnippetEvent;
import com.google.android.mobly.snippet.rpc.AsyncRpc;
import com.google.android.mobly.snippet.rpc.Rpc;
import com.google.android.mobly.snippet.rpc.RpcMinSdk;
+import com.google.android.mobly.snippet.rpc.RpcOptional;
import com.google.android.mobly.snippet.util.Log;
import java.util.HashMap;
import org.json.JSONException;
@@ -76,7 +77,27 @@ public class BluetoothLeAdvertiserSnippet implements Snippet {
* }
* </pre>
*
- * @param advertiseData A JSONObject representing a {@link AdvertiseData} object. E.g.
+ * @param advertiseData A JSONObject representing a {@link AdvertiseData} object will be
+ * broadcast if the operation succeeds. E.g.
+ * <pre>
+ * {
+ * "IncludeDeviceName": (bool),
+ * # JSON list, each element representing a set of service data, which is composed of
+ * # a UUID, and an optional string.
+ * "ServiceData": [
+ * {
+ * "UUID": (A string representation of {@link ParcelUuid}),
+ * "Data": (Optional, The string representation of what you want to
+ * advertise, base64 encoded)
+ * # If you want to add a UUID without data, simply omit the "Data"
+ * # field.
+ * }
+ * ]
+ * }
+ * </pre>
+ *
+ * @param scanResponse A JSONObject representing a {@link AdvertiseData} object which will
+ * response the data to the scanning device. E.g.
* <pre>
* {
* "IncludeDeviceName": (bool),
@@ -100,7 +121,10 @@ public class BluetoothLeAdvertiserSnippet implements Snippet {
@RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1)
@AsyncRpc(description = "Start BLE advertising.")
public void bleStartAdvertising(
- String callbackId, JSONObject advertiseSettings, JSONObject advertiseData)
+ String callbackId,
+ JSONObject advertiseSettings,
+ JSONObject advertiseData,
+ @RpcOptional JSONObject scanResponse)
throws BluetoothLeAdvertiserSnippetException, JSONException {
if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) {
throw new BluetoothLeAdvertiserSnippetException(
@@ -109,7 +133,12 @@ public class BluetoothLeAdvertiserSnippet implements Snippet {
AdvertiseSettings settings = JsonDeserializer.jsonToBleAdvertiseSettings(advertiseSettings);
AdvertiseData data = JsonDeserializer.jsonToBleAdvertiseData(advertiseData);
AdvertiseCallback advertiseCallback = new DefaultAdvertiseCallback(callbackId);
- mAdvertiser.startAdvertising(settings, data, advertiseCallback);
+ if (scanResponse == null) {
+ mAdvertiser.startAdvertising(settings, data, advertiseCallback);
+ } else {
+ AdvertiseData response = JsonDeserializer.jsonToBleAdvertiseData(scanResponse);
+ mAdvertiser.startAdvertising(settings, data, response, advertiseCallback);
+ }
mAdvertiseCallbacks.put(callbackId, advertiseCallback);
}
@@ -150,6 +179,7 @@ public class BluetoothLeAdvertiserSnippet implements Snippet {
mCallbackId = callbackId;
}
+ @Override
public void onStartSuccess(AdvertiseSettings settingsInEffect) {
Log.e("Bluetooth LE advertising started with settings: " + settingsInEffect.toString());
SnippetEvent event = new SnippetEvent(mCallbackId, "onStartSuccess");
@@ -159,6 +189,7 @@ public class BluetoothLeAdvertiserSnippet implements Snippet {
sEventCache.postEvent(event);
}
+ @Override
public void onStartFailure(int errorCode) {
Log.e("Bluetooth LE advertising failed to start with error code: " + errorCode);
SnippetEvent event = new SnippetEvent(mCallbackId, "onStartFailure");
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java
index 7e133d1..622556f 100644
--- a/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/BluetoothLeScannerSnippet.java
@@ -20,10 +20,13 @@ import android.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
import android.os.Build;
import android.os.Bundle;
import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.bundled.utils.JsonDeserializer;
import com.google.android.mobly.snippet.bundled.utils.JsonSerializer;
import com.google.android.mobly.snippet.bundled.utils.MbsEnums;
import com.google.android.mobly.snippet.event.EventCache;
@@ -31,10 +34,14 @@ import com.google.android.mobly.snippet.event.SnippetEvent;
import com.google.android.mobly.snippet.rpc.AsyncRpc;
import com.google.android.mobly.snippet.rpc.Rpc;
import com.google.android.mobly.snippet.rpc.RpcMinSdk;
+import com.google.android.mobly.snippet.rpc.RpcOptional;
import com.google.android.mobly.snippet.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
/** Snippet class exposing Android APIs in WifiManager. */
@TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
@@ -51,6 +58,7 @@ public class BluetoothLeScannerSnippet implements Snippet {
private final EventCache mEventCache = EventCache.getInstance();
private final HashMap<String, ScanCallback> mScanCallbacks = new HashMap<>();
private final JsonSerializer mJsonSerializer = new JsonSerializer();
+ private long bleScanStartTime = 0;
public BluetoothLeScannerSnippet() {
mScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner();
@@ -60,17 +68,49 @@ public class BluetoothLeScannerSnippet implements Snippet {
* Start a BLE scan.
*
* @param callbackId
+ * @param scanFilters A JSONArray representing a list of {@link ScanFilter} object for finding
+ * exact BLE devices. E.g.
+ * <pre>
+ * [
+ * {
+ * "ServiceUuid": (A string representation of {@link ParcelUuid}),
+ * },
+ * ]
+ * </pre>
+ *
+ * @param scanSettings A JSONObject representing a {@link ScanSettings} object which is the
+ * Settings for the scan. E.g.
+ * <pre>
+ * {
+ * 'ScanMode': 'SCAN_MODE_LOW_LATENCY',
+ * }
+ * </pre>
+ *
* @throws BluetoothLeScanSnippetException
*/
@RpcMinSdk(Build.VERSION_CODES.LOLLIPOP_MR1)
@AsyncRpc(description = "Start BLE scan.")
- public void bleStartScan(String callbackId) throws BluetoothLeScanSnippetException {
+ public void bleStartScan(
+ String callbackId,
+ @RpcOptional JSONArray scanFilters,
+ @RpcOptional JSONObject scanSettings)
+ throws BluetoothLeScanSnippetException, JSONException {
if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) {
throw new BluetoothLeScanSnippetException(
"Bluetooth is disabled, cannot start BLE scan.");
}
DefaultScanCallback callback = new DefaultScanCallback(callbackId);
- mScanner.startScan(callback);
+ if (scanFilters == null && scanSettings == null) {
+ mScanner.startScan(callback);
+ } else {
+ ArrayList<ScanFilter> filters = new ArrayList<>();
+ for (int i = 0; i < scanFilters.length(); i++) {
+ filters.add(JsonDeserializer.jsonToScanFilter(scanFilters.getJSONObject(i)));
+ }
+ ScanSettings settings = JsonDeserializer.jsonToScanSettings(scanSettings);
+ mScanner.startScan(filters, settings, callback);
+ }
+ bleScanStartTime = System.currentTimeMillis();
mScanCallbacks.put(callbackId, callback);
}
@@ -106,16 +146,21 @@ public class BluetoothLeScannerSnippet implements Snippet {
mCallbackId = callbackId;
}
+ @Override
public void onScanResult(int callbackType, ScanResult result) {
Log.i("Got Bluetooth LE scan result.");
+ long bleScanOnResultTime = System.currentTimeMillis();
SnippetEvent event = new SnippetEvent(mCallbackId, "onScanResult");
String callbackTypeString =
MbsEnums.BLE_SCAN_RESULT_CALLBACK_TYPE.getString(callbackType);
event.getData().putString("CallbackType", callbackTypeString);
event.getData().putBundle("result", mJsonSerializer.serializeBleScanResult(result));
+ event.getData()
+ .putLong("StartToResultTimeDeltaMs", bleScanOnResultTime - bleScanStartTime);
mEventCache.postEvent(event);
}
+ @Override
public void onBatchScanResults(List<ScanResult> results) {
Log.i("Got Bluetooth LE batch scan results.");
SnippetEvent event = new SnippetEvent(mCallbackId, "onBatchScanResult");
@@ -127,6 +172,7 @@ public class BluetoothLeScannerSnippet implements Snippet {
mEventCache.postEvent(event);
}
+ @Override
public void onScanFailed(int errorCode) {
Log.e("Bluetooth LE scan failed with error code: " + errorCode);
SnippetEvent event = new SnippetEvent(mCallbackId, "onScanFailed");
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java
index be41e9e..e8a84c9 100644
--- a/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java
@@ -16,6 +16,8 @@
package com.google.android.mobly.snippet.bundled;
+import static java.util.stream.Collectors.toCollection;
+
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.PendingIntent;
@@ -36,6 +38,7 @@ import com.google.android.mobly.snippet.event.SnippetEvent;
import com.google.android.mobly.snippet.rpc.AsyncRpc;
import com.google.android.mobly.snippet.rpc.Rpc;
import java.util.ArrayList;
+import java.util.stream.IntStream;
import org.json.JSONObject;
/** Snippet class for SMS RPCs. */
@@ -80,20 +83,37 @@ public class SmsSnippet implements Snippet {
if (message.length() > MAX_CHAR_COUNT_PER_SMS) {
ArrayList<String> parts = mSmsManager.divideMessage(message);
- ArrayList<PendingIntent> sIntents = new ArrayList<>();
- for (int i = 0; i < parts.size(); i++) {
- sIntents.add(
- PendingIntent.getBroadcast(mContext, 0, new Intent(SMS_SENT_ACTION), 0));
- }
receiver.setExpectedMessageCount(parts.size());
mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION));
- mSmsManager.sendMultipartTextMessage(phoneNumber, null, parts, sIntents, null);
+ mSmsManager.sendMultipartTextMessage(
+ /* destinationAddress= */ phoneNumber,
+ /* scAddress= */ null,
+ /* parts= */ parts,
+ /* sentIntents= */ IntStream.range(0, parts.size())
+ .mapToObj(
+ i ->
+ PendingIntent.getBroadcast(
+ /* context= */ mContext,
+ /* requestCode= */ 0,
+ /* intent= */ new Intent(SMS_SENT_ACTION),
+ /* flags= */ PendingIntent.FLAG_IMMUTABLE))
+ .collect(toCollection(ArrayList::new)),
+ /* deliveryIntents= */ null);
} else {
PendingIntent sentIntent =
- PendingIntent.getBroadcast(mContext, 0, new Intent(SMS_SENT_ACTION), 0);
+ PendingIntent.getBroadcast(
+ /* context= */ mContext,
+ /* requestCode= */ 0,
+ /* intent= */ new Intent(SMS_SENT_ACTION),
+ /* flags= */ PendingIntent.FLAG_IMMUTABLE);
receiver.setExpectedMessageCount(1);
mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION));
- mSmsManager.sendTextMessage(phoneNumber, null, message, sentIntent, null);
+ mSmsManager.sendTextMessage(
+ /* destinationAddress= */ phoneNumber,
+ /* scAddress= */ null,
+ /* text= */ message,
+ /* sentIntent= */ sentIntent,
+ /* deliveryIntent= */ null);
}
SnippetEvent result =
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java
index 7e1a416..89a65d2 100644
--- a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java
@@ -16,7 +16,6 @@
package com.google.android.mobly.snippet.bundled;
-import android.app.UiAutomation;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -44,6 +43,9 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.net.wifi.SupplicantState;
+
+import com.google.android.mobly.snippet.bundled.utils.Utils;
+
/** Snippet class exposing Android APIs in WifiManager. */
public class WifiManagerSnippet implements Snippet {
private static class WifiManagerSnippetException extends Exception {
@@ -52,10 +54,6 @@ public class WifiManagerSnippet implements Snippet {
public WifiManagerSnippetException(String msg) {
super(msg);
}
-
- public WifiManagerSnippetException(String msg, Throwable err) {
- super(msg, err);
- }
}
private static final int TIMEOUT_TOGGLE_STATE = 30;
@@ -69,7 +67,7 @@ public class WifiManagerSnippet implements Snippet {
mWifiManager =
(WifiManager)
mContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
- adaptShellPermissionIfRequired();
+ Utils.adaptShellPermissionIfRequired(mContext);
}
@Rpc(
@@ -403,33 +401,6 @@ public class WifiManagerSnippet implements Snippet {
@Override
public void shutdown() {}
- /**
- * Elevates permission as require for proper wifi controls.
- *
- * Starting in Android Q (29), additional restrictions are added for wifi operation. See
- * below Android Q privacy changes for additional details.
- * https://developer.android.com/preview/privacy/camera-connectivity
- *
- * @throws Throwable if failed to cleanup connection with UiAutomation
- */
- private void adaptShellPermissionIfRequired() throws Throwable {
- if (mContext.getApplicationContext().getApplicationInfo().targetSdkVersion >= 29
- && Build.VERSION.SDK_INT >= 29) {
- Log.d("Elevating permission require to enable support for wifi operation in Android Q+");
- UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation();
- uia.adoptShellPermissionIdentity();
- try {
- Class<?> cls = Class.forName("android.app.UiAutomation");
- Method destroyMethod = cls.getDeclaredMethod("destroy");
- destroyMethod.invoke(uia);
- } catch (NoSuchMethodException
- | IllegalAccessException
- | ClassNotFoundException
- | InvocationTargetException e) {
- throw new WifiManagerSnippetException("Failed to cleaup Ui Automation", e);
- }
- }
- }
private class WifiScanReceiver extends BroadcastReceiver {
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java
index c16a2b0..71061fe 100644
--- a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothAdapterSnippet.java
@@ -22,6 +22,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -35,7 +36,10 @@ import com.google.android.mobly.snippet.bundled.utils.Utils;
import com.google.android.mobly.snippet.rpc.Rpc;
import com.google.android.mobly.snippet.util.Log;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
@@ -62,14 +66,20 @@ public class BluetoothAdapterSnippet implements Snippet {
// Default timeout in seconds.
private static final int TIMEOUT_TOGGLE_STATE_SEC = 30;
private final Context mContext;
+ private final PackageManager mPackageManager;
private static final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
private final JsonSerializer mJsonSerializer = new JsonSerializer();
private static final ConcurrentHashMap<String, BluetoothDevice> mDiscoveryResults =
new ConcurrentHashMap<>();
private volatile boolean mIsDiscoveryFinished = false;
+ private final Map<String, BroadcastReceiver> mReceivers;
- public BluetoothAdapterSnippet() {
+ public BluetoothAdapterSnippet() throws Throwable {
mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ // Use a synchronized map to avoid racing problems
+ mReceivers = Collections.synchronizedMap(new HashMap<String, BroadcastReceiver>());
+ Utils.adaptShellPermissionIfRequired(mContext);
+ mPackageManager = mContext.getPackageManager();
}
/**
@@ -190,6 +200,20 @@ public class BluetoothAdapterSnippet implements Snippet {
return mBluetoothAdapter.getName();
}
+ @Rpc(description = "Automatically confirm the incoming BT pairing request.")
+ public void btStartAutoAcceptIncomingPairRequest() throws Throwable {
+ BroadcastReceiver receiver = new PairingBroadcastReceiver(mContext);
+ mContext.registerReceiver(
+ receiver, PairingBroadcastReceiver.filter);
+ mReceivers.put("AutoAcceptIncomingPairReceiver", receiver);
+ }
+
+ @Rpc(description = "Stop the incoming BT pairing request.")
+ public void btStopAutoAcceptIncomingPairRequest() throws Throwable {
+ BroadcastReceiver receiver = mReceivers.remove("AutoAcceptIncomingPairReceiver");
+ mContext.unregisterReceiver(receiver);
+ }
+
@Rpc(description = "Returns the hardware address of the local Bluetooth adapter.")
public String btGetAddress() {
return mBluetoothAdapter.getAddress();
@@ -240,10 +264,18 @@ public class BluetoothAdapterSnippet implements Snippet {
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, duration);
// Triggers the system UI popup to ask for explicit permission.
mContext.startActivity(discoverableIntent);
- // Clicks the "ALLOW" button.
- BySelector allowButtonSelector = By.text(TEXT_PATTERN_ALLOW).clickable(true);
- uiDevice.wait(Until.findObject(allowButtonSelector), 10);
- uiDevice.findObject(allowButtonSelector).click();
+
+ if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)) {
+ // Clicks the "OK" button.
+ BySelector okButtonSelector = By.desc(TEXT_PATTERN_OK).clickable(true);
+ uiDevice.wait(Until.findObject(okButtonSelector), 10);
+ uiDevice.findObject(okButtonSelector).click();
+ } else {
+ // Clicks the "ALLOW" button.
+ BySelector allowButtonSelector = By.text(TEXT_PATTERN_ALLOW).clickable(true);
+ uiDevice.wait(Until.findObject(allowButtonSelector), 10);
+ uiDevice.findObject(allowButtonSelector).click();
+ }
} else if (Build.VERSION.SDK_INT >= 30) {
if (!(boolean)
Utils.invokeByReflection(
@@ -267,6 +299,8 @@ public class BluetoothAdapterSnippet implements Snippet {
private static final Pattern TEXT_PATTERN_ALLOW =
Pattern.compile("allow", Pattern.CASE_INSENSITIVE);
+ private static final Pattern TEXT_PATTERN_OK =
+ Pattern.compile("ok", Pattern.CASE_INSENSITIVE);
@Rpc(description = "Cancel ongoing bluetooth discovery.")
public void btCancelDiscovery() throws BluetoothAdapterSnippetException {
@@ -356,7 +390,12 @@ public class BluetoothAdapterSnippet implements Snippet {
}
@Override
- public void shutdown() {}
+ public void shutdown() {
+ for (Map.Entry<String, BroadcastReceiver> entry : mReceivers.entrySet()) {
+ mContext.unregisterReceiver(entry.getValue());
+ }
+ mReceivers.clear();
+ }
private class BluetoothScanReceiver extends BroadcastReceiver {
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothGattClientSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothGattClientSnippet.java
new file mode 100644
index 0000000..14ec348
--- /dev/null
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothGattClientSnippet.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2023 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.bundled.bluetooth;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.Base64;
+import androidx.test.platform.app.InstrumentationRegistry;
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.bundled.utils.JsonSerializer;
+import com.google.android.mobly.snippet.bundled.utils.MbsEnums;
+import com.google.android.mobly.snippet.event.EventCache;
+import com.google.android.mobly.snippet.event.SnippetEvent;
+import com.google.android.mobly.snippet.rpc.AsyncRpc;
+import com.google.android.mobly.snippet.rpc.Rpc;
+import com.google.android.mobly.snippet.rpc.RpcMinSdk;
+import com.google.android.mobly.snippet.util.Log;
+import java.util.ArrayList;
+import java.util.HashMap;
+import org.json.JSONException;
+
+/** Snippet class exposing Android APIs in BluetoothGatt. */
+public class BluetoothGattClientSnippet implements Snippet {
+ private static class BluetoothGattClientSnippetException extends Exception {
+ private static final long serialVersionUID = 1;
+
+ public BluetoothGattClientSnippetException(String msg) {
+ super(msg);
+ }
+ }
+
+ private final Context context;
+ private final EventCache eventCache;
+ private final HashMap<String, HashMap<String, BluetoothGattCharacteristic>>
+ characteristicHashMap;
+
+ private BluetoothGatt bluetoothGattClient;
+
+ private long connectionStartTime = 0;
+ private long connectionEndTime = 0;
+
+ public BluetoothGattClientSnippet() {
+ context = InstrumentationRegistry.getInstrumentation().getContext();
+ eventCache = EventCache.getInstance();
+ characteristicHashMap = new HashMap<>();
+ }
+
+ @RpcMinSdk(VERSION_CODES.LOLLIPOP)
+ @AsyncRpc(description = "Start BLE client.")
+ public void bleConnectGatt(String callbackId, String deviceAddress) throws JSONException {
+ BluetoothDevice remoteDevice =
+ BluetoothAdapter.getDefaultAdapter().getRemoteDevice(deviceAddress);
+ BluetoothGattCallback gattCallback = new DefaultBluetoothGattCallback(callbackId);
+ connectionStartTime = System.currentTimeMillis();
+ bluetoothGattClient = remoteDevice.connectGatt(context, false, gattCallback);
+ Log.d("Connection start time is " + connectionStartTime);
+ connectionEndTime = 0;
+ }
+
+ @RpcMinSdk(VERSION_CODES.LOLLIPOP)
+ @Rpc(description = "Start BLE service discovery")
+ public long bleDiscoverServices() throws BluetoothGattClientSnippetException {
+ if (bluetoothGattClient == null) {
+ throw new BluetoothGattClientSnippetException("BLE client is not initialized.");
+ }
+ long discoverServicesStartTime = SystemClock.elapsedRealtimeNanos();
+ Log.d("Discover services start time is " + discoverServicesStartTime);
+ boolean result = bluetoothGattClient.discoverServices();
+ if (!result) {
+ throw new BluetoothGattClientSnippetException("Discover services returned false.");
+ }
+ return discoverServicesStartTime;
+ }
+
+ @RpcMinSdk(VERSION_CODES.LOLLIPOP)
+ @Rpc(description = "Stop BLE client.")
+ public void bleDisconnect() throws BluetoothGattClientSnippetException {
+ if (bluetoothGattClient == null) {
+ throw new BluetoothGattClientSnippetException("BLE client is not initialized.");
+ }
+ bluetoothGattClient.disconnect();
+ }
+
+ @RpcMinSdk(VERSION_CODES.LOLLIPOP)
+ @Rpc(description = "BLE read operation.")
+ public boolean bleReadOperation(String serviceUuid, String characteristicUuid)
+ throws JSONException, BluetoothGattClientSnippetException {
+ if (bluetoothGattClient == null) {
+ throw new BluetoothGattClientSnippetException("BLE client is not initialized.");
+ }
+ boolean result =
+ bluetoothGattClient.readCharacteristic(
+ characteristicHashMap.get(serviceUuid).get(characteristicUuid));
+ Log.d("Read operation returned result " + result);
+ return result;
+ }
+
+ @RpcMinSdk(VERSION_CODES.LOLLIPOP)
+ @Rpc(description = "BLE write operation.")
+ public boolean bleWriteOperation(String serviceUuid, String characteristicUuid, String data)
+ throws JSONException, BluetoothGattClientSnippetException {
+ if (bluetoothGattClient == null) {
+ throw new BluetoothGattClientSnippetException("BLE client is not initialized.");
+ }
+ BluetoothGattCharacteristic characteristic =
+ characteristicHashMap.get(serviceUuid).get(characteristicUuid);
+ characteristic.setValue(Base64.decode(data, Base64.NO_WRAP));
+ boolean result = bluetoothGattClient.writeCharacteristic(characteristic);
+ Log.d("Write operation returned result " + result);
+ return result;
+ }
+
+ private class DefaultBluetoothGattCallback extends BluetoothGattCallback {
+ private final String callbackId;
+
+ DefaultBluetoothGattCallback(String callbackId) {
+ this.callbackId = callbackId;
+ }
+
+ @Override
+ public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+ SnippetEvent event = new SnippetEvent(callbackId, "onConnectionStateChange");
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
+ connectionEndTime = System.currentTimeMillis();
+ event.getData().putLong(
+ "gattConnectionTimeMs", connectionEndTime - connectionStartTime);
+ Log.d("Connection end time is " + connectionEndTime);
+ }
+ event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status));
+ event.getData().putString("newState", MbsEnums.BLE_CONNECT_STATUS.getString(newState));
+ event.getData().putBundle("gatt", JsonSerializer.serializeBluetoothGatt(gatt));
+ eventCache.postEvent(event);
+ }
+
+ @Override
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ long discoverServicesEndTime = SystemClock.elapsedRealtimeNanos();
+ Log.d("Discover services end time is " + discoverServicesEndTime);
+ SnippetEvent event = new SnippetEvent(callbackId, "onServiceDiscovered");
+ event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status));
+ ArrayList<Bundle> services = new ArrayList<>();
+ for (BluetoothGattService service : gatt.getServices()) {
+ HashMap<String, BluetoothGattCharacteristic> characteristics = new HashMap<>();
+ for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
+ characteristics.put(characteristic.getUuid().toString(), characteristic);
+ }
+ characteristicHashMap.put(service.getUuid().toString(), characteristics);
+ services.add(JsonSerializer.serializeBluetoothGattService(service));
+ }
+ // TODO(66740428): Should not return services directly
+ event.getData().putParcelableArrayList("Services", services);
+ event.getData().putBundle("gatt", JsonSerializer.serializeBluetoothGatt(gatt));
+ event.getData().putLong("discoveryServicesEndTime", discoverServicesEndTime);
+ eventCache.postEvent(event);
+ }
+
+ @Override
+ public void onCharacteristicRead(
+ BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ SnippetEvent event = new SnippetEvent(callbackId, "onCharacteristicRead");
+ event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status));
+ // TODO(66740428): Should return the characteristic instead of value
+ event.getData()
+ .putString("Data",
+ Base64.encodeToString(characteristic.getValue(), Base64.NO_WRAP));
+ event.getData().putBundle("gatt", JsonSerializer.serializeBluetoothGatt(gatt));
+ eventCache.postEvent(event);
+ }
+
+ @Override
+ public void onCharacteristicWrite(
+ BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ SnippetEvent event = new SnippetEvent(callbackId, "onCharacteristicWrite");
+ event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status));
+ // TODO(66740428): Should return the characteristic instead of value
+ event.getData().putBundle("gatt", JsonSerializer.serializeBluetoothGatt(gatt));
+ eventCache.postEvent(event);
+ }
+
+ @Override
+ public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
+ SnippetEvent event = new SnippetEvent(callbackId, "onReliableWriteCompleted");
+ event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status));
+ event.getData().putBundle("gatt", JsonSerializer.serializeBluetoothGatt(gatt));
+ eventCache.postEvent(event);
+ }
+ }
+
+ @Override
+ public void shutdown() {
+ if (bluetoothGattClient != null) {
+ bluetoothGattClient.close();
+ }
+ }
+}
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothGattServerSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothGattServerSnippet.java
new file mode 100644
index 0000000..29222eb
--- /dev/null
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/BluetoothGattServerSnippet.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2023 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.bundled.bluetooth;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattServer;
+import android.bluetooth.BluetoothGattServerCallback;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.os.Build.VERSION_CODES;
+import android.os.DeadObjectException;
+import android.os.SystemClock;
+import android.util.Base64;
+import androidx.test.platform.app.InstrumentationRegistry;
+import com.google.android.mobly.snippet.Snippet;
+import com.google.android.mobly.snippet.bundled.utils.DataHolder;
+import com.google.android.mobly.snippet.bundled.utils.JsonDeserializer;
+import com.google.android.mobly.snippet.bundled.utils.JsonSerializer;
+import com.google.android.mobly.snippet.bundled.utils.MbsEnums;
+import com.google.android.mobly.snippet.event.EventCache;
+import com.google.android.mobly.snippet.event.SnippetEvent;
+import com.google.android.mobly.snippet.rpc.AsyncRpc;
+import com.google.android.mobly.snippet.rpc.Rpc;
+import com.google.android.mobly.snippet.rpc.RpcMinSdk;
+import com.google.android.mobly.snippet.util.Log;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** Snippet class exposing Android APIs in BluetoothGattServer. */
+public class BluetoothGattServerSnippet implements Snippet {
+ private static class BluetoothGattServerSnippetException extends Exception {
+ private static final long serialVersionUID = 1;
+
+ public BluetoothGattServerSnippetException(String msg) {
+ super(msg);
+ }
+ }
+
+ private final Context context;
+ private final BluetoothManager bluetoothManager;
+ private final DataHolder dataHolder;
+ private final EventCache eventCache;
+
+ private BluetoothGattServer bluetoothGattServer;
+
+ public BluetoothGattServerSnippet() {
+ context = InstrumentationRegistry.getInstrumentation().getContext();
+ bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
+ dataHolder = new DataHolder();
+ eventCache = EventCache.getInstance();
+ }
+
+ @RpcMinSdk(VERSION_CODES.LOLLIPOP)
+ @AsyncRpc(description = "Start BLE server.")
+ public void bleStartServer(String callbackId, JSONArray services)
+ throws JSONException, DeadObjectException {
+ BluetoothGattServerCallback gattServerCallback =
+ new DefaultBluetoothGattServerCallback(callbackId);
+ bluetoothGattServer = bluetoothManager.openGattServer(context, gattServerCallback);
+ addServiceToGattServer(services);
+ }
+
+ @RpcMinSdk(VERSION_CODES.LOLLIPOP)
+ @AsyncRpc(description = "Start BLE server with workaround.")
+ public void bleStartServerWithWorkaround(String callbackId, JSONArray services)
+ throws JSONException, DeadObjectException {
+ BluetoothGattServerCallback gattServerCallback =
+ new DefaultBluetoothGattServerCallback(callbackId);
+ boolean isGattServerStarted = false;
+ int count = 0;
+ while (!isGattServerStarted && count < 5) {
+ bluetoothGattServer = bluetoothManager.openGattServer(context, gattServerCallback);
+ if (bluetoothGattServer != null) {
+ addServiceToGattServer(services);
+ isGattServerStarted = true;
+ } else {
+ SystemClock.sleep(1000);
+ count++;
+ }
+ }
+ }
+
+ private void addServiceToGattServer(JSONArray services) throws JSONException {
+ for (int i = 0; i < services.length(); i++) {
+ JSONObject service = services.getJSONObject(i);
+ BluetoothGattService bluetoothGattService =
+ JsonDeserializer.jsonToBluetoothGattService(dataHolder, service);
+ bluetoothGattServer.addService(bluetoothGattService);
+ }
+ }
+
+ @RpcMinSdk(VERSION_CODES.LOLLIPOP)
+ @Rpc(description = "Stop BLE server.")
+ public void bleStopServer() throws BluetoothGattServerSnippetException {
+ if (bluetoothGattServer == null) {
+ throw new BluetoothGattServerSnippetException("BLE server is not initialized.");
+ }
+ bluetoothGattServer.close();
+ }
+
+ private class DefaultBluetoothGattServerCallback extends BluetoothGattServerCallback {
+ private final String callbackId;
+
+ DefaultBluetoothGattServerCallback(String callbackId) {
+ this.callbackId = callbackId;
+ }
+
+ @Override
+ public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
+ SnippetEvent event = new SnippetEvent(callbackId, "onConnectionStateChange");
+ event.getData().putBundle("device", JsonSerializer.serializeBluetoothDevice(device));
+ event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status));
+ event.getData().putString("newState", MbsEnums.BLE_CONNECT_STATUS.getString(newState));
+ eventCache.postEvent(event);
+ }
+
+ @Override
+ public void onServiceAdded(int status, BluetoothGattService service) {
+ Log.d("Bluetooth Gatt Server service added with status " + status);
+ SnippetEvent event = new SnippetEvent(callbackId, "onServiceAdded");
+ event.getData().putString("status", MbsEnums.BLE_STATUS_TYPE.getString(status));
+ event.getData()
+ .putParcelable("Service",
+ JsonSerializer.serializeBluetoothGattService(service));
+ eventCache.postEvent(event);
+ }
+
+ @Override
+ public void onCharacteristicReadRequest(
+ BluetoothDevice device,
+ int requestId,
+ int offset,
+ BluetoothGattCharacteristic characteristic) {
+ Log.d("Bluetooth Gatt Server received a read request");
+ if (dataHolder.get(characteristic) != null) {
+ bluetoothGattServer.sendResponse(
+ device,
+ requestId,
+ BluetoothGatt.GATT_SUCCESS,
+ offset,
+ Base64.decode(dataHolder.get(characteristic), Base64.NO_WRAP));
+ } else {
+ bluetoothGattServer.sendResponse(
+ device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
+ }
+ }
+
+ @Override
+ public void onCharacteristicWriteRequest(
+ BluetoothDevice device,
+ int requestId,
+ BluetoothGattCharacteristic characteristic,
+ boolean preparedWrite,
+ boolean responseNeeded,
+ int offset,
+ byte[] value) {
+ Log.d("Bluetooth Gatt Server received a write request");
+ bluetoothGattServer.sendResponse(
+ device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
+ SnippetEvent event = new SnippetEvent(callbackId, "onCharacteristicWriteRequest");
+ event.getData().putString("Data", Base64.encodeToString(value, Base64.NO_WRAP));
+ eventCache.postEvent(event);
+ }
+
+ @Override
+ public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
+ Log.d("Bluetooth Gatt Server received an execute write request");
+ bluetoothGattServer.sendResponse(
+ device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null);
+ }
+ }
+
+ @Override
+ public void shutdown() {}
+}
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/PairingBroadcastReceiver.java b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/PairingBroadcastReceiver.java
index 0cfd362..69ae433 100644
--- a/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/PairingBroadcastReceiver.java
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/bluetooth/PairingBroadcastReceiver.java
@@ -8,14 +8,16 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import com.google.android.mobly.snippet.util.Log;
+import com.google.android.mobly.snippet.bundled.utils.Utils;
@TargetApi(Build.VERSION_CODES.KITKAT)
public class PairingBroadcastReceiver extends BroadcastReceiver {
private final Context mContext;
public static IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST);
- public PairingBroadcastReceiver(Context context) {
+ public PairingBroadcastReceiver(Context context) throws Throwable {
mContext = context;
+ Utils.adaptShellPermissionIfRequired(mContext);
}
public void onReceive(Context context, Intent intent) {
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/DataHolder.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/DataHolder.java
new file mode 100644
index 0000000..021a6ba
--- /dev/null
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/DataHolder.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.mobly.snippet.bundled.utils;
+
+import android.bluetooth.BluetoothGattCharacteristic;
+import java.util.HashMap;
+
+/** A holder to hold android objects for snippets. */
+// TODO(ko1in1u): For future extensions between Snippet classes and Utils.
+public class DataHolder {
+ private final HashMap<BluetoothGattCharacteristic, String> dataToBeRead;
+
+ public DataHolder() {
+ dataToBeRead = new HashMap<>();
+ }
+
+ public String get(BluetoothGattCharacteristic characteristic) {
+ return dataToBeRead.get(characteristic);
+ }
+
+ public void insertData(BluetoothGattCharacteristic characteristic, String string) {
+ dataToBeRead.put(characteristic, string);
+ }
+}
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java
index 2f943e0..a3d5325 100644
--- a/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonDeserializer.java
@@ -17,12 +17,17 @@
package com.google.android.mobly.snippet.bundled.utils;
import android.annotation.TargetApi;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanSettings;
import android.net.wifi.WifiConfiguration;
import android.os.Build;
import android.os.ParcelUuid;
import android.util.Base64;
+import java.util.UUID;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -101,4 +106,52 @@ public class JsonDeserializer {
}
return builder.build();
}
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public static BluetoothGattService jsonToBluetoothGattService(
+ DataHolder dataHolder, JSONObject jsonObject) throws JSONException {
+ BluetoothGattService service =
+ new BluetoothGattService(
+ UUID.fromString(jsonObject.getString("UUID")),
+ MbsEnums.BLE_SERVICE_TYPE.getInt(jsonObject.getString("Type")));
+ JSONArray characteristics = jsonObject.getJSONArray("Characteristics");
+ for (int i = 0; i < characteristics.length(); i++) {
+ BluetoothGattCharacteristic characteristic =
+ jsonToBluetoothGattCharacteristic(dataHolder, characteristics.getJSONObject(i));
+ service.addCharacteristic(characteristic);
+ }
+ return service;
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public static BluetoothGattCharacteristic jsonToBluetoothGattCharacteristic(
+ DataHolder dataHolder, JSONObject jsonObject) throws JSONException {
+ BluetoothGattCharacteristic characteristic =
+ new BluetoothGattCharacteristic(
+ UUID.fromString(jsonObject.getString("UUID")),
+ MbsEnums.BLE_PROPERTY_TYPE.getInt(jsonObject.getString("Property")),
+ MbsEnums.BLE_PERMISSION_TYPE.getInt(jsonObject.getString("Permission")));
+ if (jsonObject.has("Data")) {
+ dataHolder.insertData(characteristic, jsonObject.getString("Data"));
+ }
+ return characteristic;
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public static ScanFilter jsonToScanFilter(JSONObject jsonObject) throws JSONException {
+ ScanFilter.Builder builder = new ScanFilter.Builder();
+ if (jsonObject.has("ServiceUuid")) {
+ builder.setServiceUuid(ParcelUuid.fromString(jsonObject.getString("ServiceUuid")));
+ }
+ return builder.build();
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public static ScanSettings jsonToScanSettings(JSONObject jsonObject) throws JSONException {
+ ScanSettings.Builder builder = new ScanSettings.Builder();
+ if (jsonObject.has("ScanMode")) {
+ builder.setScanMode(MbsEnums.BLE_SCAN_MODE.getInt(jsonObject.getString("ScanMode")));
+ }
+ return builder.build();
+ }
}
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java
index 82e6f7c..6487501 100644
--- a/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/JsonSerializer.java
@@ -16,8 +16,13 @@
package com.google.android.mobly.snippet.bundled.utils;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
import android.annotation.TargetApi;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.ScanRecord;
import android.net.DhcpInfo;
@@ -27,6 +32,7 @@ import android.net.wifi.WifiInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelUuid;
+import android.util.Base64;
import android.util.SparseArray;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -125,7 +131,7 @@ public class JsonSerializer {
return result;
}
- public Bundle serializeBluetoothDevice(BluetoothDevice data) {
+ public static Bundle serializeBluetoothDevice(BluetoothDevice data) {
Bundle result = new Bundle();
result.putString("Address", data.getAddress());
final String bondState =
@@ -184,6 +190,7 @@ public class JsonSerializer {
Bundle result = new Bundle();
result.putString("DeviceName", record.getDeviceName());
result.putInt("TxPowerLevel", record.getTxPowerLevel());
+ result.putParcelableArrayList("Services", serializeBleScanServices(record));
result.putBundle(
"manufacturerSpecificData", serializeBleScanManufacturerSpecificData(record));
return result;
@@ -191,6 +198,28 @@ public class JsonSerializer {
/** Serialize manufacturer specific data from ScanRecord for Bluetooth LE. */
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private ArrayList<Bundle> serializeBleScanServices(ScanRecord record) {
+ ArrayList<Bundle> result = new ArrayList<>();
+ if (record.getServiceUuids() != null) {
+ for (ParcelUuid uuid : record.getServiceUuids()) {
+ Bundle service = new Bundle();
+ service.putString("UUID", uuid.getUuid().toString());
+ if (record.getServiceData(uuid) != null) {
+ service.putString(
+ "Data",
+ new String(Base64.encode(record.getServiceData(uuid), Base64.NO_WRAP),
+ UTF_8));
+ } else {
+ service.putString("Data", "");
+ }
+ result.add(service);
+ }
+ }
+ return result;
+ }
+
+ /** Serialize manufacturer specific data from ScanRecord for Bluetooth LE. */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
private Bundle serializeBleScanManufacturerSpecificData(ScanRecord record) {
Bundle result = new Bundle();
SparseArray<byte[]> sparseArray = record.getManufacturerSpecificData();
@@ -213,4 +242,42 @@ public class JsonSerializer {
result.putBoolean("IsConnectable", advertiseSettings.isConnectable());
return result;
}
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public static Bundle serializeBluetoothGatt(BluetoothGatt gatt) {
+ Bundle result = new Bundle();
+ ArrayList<Bundle> services = new ArrayList<>();
+ for (BluetoothGattService service : gatt.getServices()) {
+ services.add(JsonSerializer.serializeBluetoothGattService(service));
+ }
+ result.putParcelableArrayList("Services", services);
+ result.putBundle("Device", JsonSerializer.serializeBluetoothDevice(gatt.getDevice()));
+ return result;
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public static Bundle serializeBluetoothGattService(BluetoothGattService service) {
+ Bundle result = new Bundle();
+ result.putString("UUID", service.getUuid().toString());
+ result.putString("Type", MbsEnums.BLE_SERVICE_TYPE.getString(service.getType()));
+ ArrayList<Bundle> characteristics = new ArrayList<>();
+ for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
+ characteristics.add(serializeBluetoothGattCharacteristic(characteristic));
+ }
+ result.putParcelableArrayList("Characteristics", characteristics);
+ return result;
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public static Bundle serializeBluetoothGattCharacteristic(
+ BluetoothGattCharacteristic characteristic) {
+ Bundle result = new Bundle();
+ result.putString("UUID", characteristic.getUuid().toString());
+ result.putString(
+ "Property", MbsEnums.BLE_PROPERTY_TYPE.getString(characteristic.getProperties()));
+ result.putString(
+ "Permission",
+ MbsEnums.BLE_PERMISSION_TYPE.getString(characteristic.getPermissions()));
+ return result;
+ }
}
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java
index 08163b4..720fad4 100644
--- a/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/MbsEnums.java
@@ -1,9 +1,31 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
package com.google.android.mobly.snippet.bundled.utils;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanSettings;
+import android.net.wifi.WifiManager.LocalOnlyHotspotCallback;
import android.os.Build;
/** Mobly Bundled Snippets (MBS)'s {@link RpcEnum} objects representing enums in Android APIs. */
@@ -15,6 +37,27 @@ public class MbsEnums {
buildBleScanResultCallbackTypeEnum();
static final RpcEnum BLUETOOTH_DEVICE_BOND_STATE = buildBluetoothDeviceBondState();
static final RpcEnum BLUETOOTH_DEVICE_TYPE = buildBluetoothDeviceTypeEnum();
+ static final RpcEnum BLE_SERVICE_TYPE = buildServiceTypeEnum();
+ public static final RpcEnum BLE_STATUS_TYPE = buildStatusTypeEnum();
+ public static final RpcEnum BLE_CONNECT_STATUS = buildConnectStatusEnum();
+ static final RpcEnum BLE_PROPERTY_TYPE = buildPropertyTypeEnum();
+ static final RpcEnum BLE_PERMISSION_TYPE = buildPermissionTypeEnum();
+ static final RpcEnum BLE_SCAN_MODE = buildBleScanModeEnum();
+ public static final RpcEnum LOCAL_HOTSPOT_FAIL_REASON = buildLocalHotspotFailedReason();
+ public static final RpcEnum ADVERTISE_FAILURE_ERROR_CODE =
+ new RpcEnum.Builder().add("ADVERTISE_FAILED_ALREADY_STARTED",
+ AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED)
+ .add("ADVERTISE_FAILED_DATA_TOO_LARGE",
+ AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE)
+ .add(
+ "ADVERTISE_FAILED_FEATURE_UNSUPPORTED",
+ AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED)
+ .add("ADVERTISE_FAILED_INTERNAL_ERROR",
+ AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR)
+ .add(
+ "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS",
+ AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS)
+ .build();
private static RpcEnum buildBluetoothDeviceBondState() {
RpcEnum.Builder builder = new RpcEnum.Builder();
@@ -89,4 +132,125 @@ public class MbsEnums {
}
return builder.build();
}
+
+ private static RpcEnum buildServiceTypeEnum() {
+ RpcEnum.Builder builder = new RpcEnum.Builder();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return builder.build();
+ }
+ builder.add("SERVICE_TYPE_PRIMARY", BluetoothGattService.SERVICE_TYPE_PRIMARY);
+ builder.add("SERVICE_TYPE_SECONDARY", BluetoothGattService.SERVICE_TYPE_SECONDARY);
+ return builder.build();
+ }
+
+ private static RpcEnum buildStatusTypeEnum() {
+ RpcEnum.Builder builder = new RpcEnum.Builder();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return builder.build();
+ }
+ builder.add("GATT_SUCCESS", BluetoothGatt.GATT_SUCCESS)
+ .add("GATT_CONNECTION_CONGESTED", BluetoothGatt.GATT_CONNECTION_CONGESTED)
+ .add("GATT_FAILURE", BluetoothGatt.GATT_FAILURE)
+ .add("GATT_INSUFFICIENT_AUTHENTICATION",
+ BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION)
+ .add("GATT_INSUFFICIENT_ENCRYPTION", BluetoothGatt.GATT_INSUFFICIENT_ENCRYPTION)
+ .add("GATT_INVALID_ATTRIBUTE_LENGTH", BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH)
+ .add("GATT_INVALID_OFFSET", BluetoothGatt.GATT_INVALID_OFFSET)
+ .add("GATT_READ_NOT_PERMITTED", BluetoothGatt.GATT_READ_NOT_PERMITTED)
+ .add("GATT_REQUEST_NOT_SUPPORTED", BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED)
+ .add("GATT_WRITE_NOT_PERMITTED", BluetoothGatt.GATT_WRITE_NOT_PERMITTED)
+ .add("BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION", 0x13)
+ .add("BLE_HCI_LOCAL_HOST_TERMINATED_CONNECTION", 0x12)
+ .add("BLE_HCI_STATUS_CODE_LMP_RESPONSE_TIMEOUT", 0x22)
+ .add("BLE_HCI_CONN_FAILED_TO_BE_ESTABLISHED", 0x3e)
+ .add("UNEXPECTED_DISCONNECT_NO_ERROR_CODE", 134)
+ .add("DID_NOT_FIND_OFFLINEP2P_SERVICE", 135)
+ .add("MISSING_CHARACTERISTIC", 137)
+ .add("CONNECTION_TIMEOUT", 138)
+ .add("READ_MALFORMED_VERSION", 139)
+ .add("READ_WRITE_VERSION_NONSPECIFIC_ERROR", 140)
+ .add("GATT_0C_err", 0X0C)
+ .add("GATT_16", 0x16)
+ .add("GATT_INTERNAL_ERROR", 129)
+ .add("BLE_HCI_CONNECTION_TIMEOUT", 0x08)
+ .add("GATT_ERROR", 133);
+ return builder.build();
+ }
+
+ private static RpcEnum buildConnectStatusEnum() {
+ RpcEnum.Builder builder = new RpcEnum.Builder();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return builder.build();
+ }
+ builder.add("STATE_CONNECTED", BluetoothProfile.STATE_CONNECTED)
+ .add("STATE_CONNECTING", BluetoothProfile.STATE_CONNECTING)
+ .add("STATE_DISCONNECTED", BluetoothProfile.STATE_DISCONNECTED)
+ .add("STATE_DISCONNECTING", BluetoothProfile.STATE_DISCONNECTING);
+ return builder.build();
+ }
+
+ private static RpcEnum buildPropertyTypeEnum() {
+ RpcEnum.Builder builder = new RpcEnum.Builder();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return builder.build();
+ }
+ builder
+ .add("PROPERTY_NONE", 0)
+ .add("PROPERTY_BROADCAST", BluetoothGattCharacteristic.PROPERTY_BROADCAST)
+ .add("PROPERTY_EXTENDED_PROPS", BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS)
+ .add("PROPERTY_INDICATE", BluetoothGattCharacteristic.PROPERTY_INDICATE)
+ .add("PROPERTY_NOTIFY", BluetoothGattCharacteristic.PROPERTY_NOTIFY)
+ .add("PROPERTY_READ", BluetoothGattCharacteristic.PROPERTY_READ)
+ .add("PROPERTY_SIGNED_WRITE", BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE)
+ .add("PROPERTY_WRITE", BluetoothGattCharacteristic.PROPERTY_WRITE)
+ .add("PROPERTY_WRITE_NO_RESPONSE",
+ BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE);
+ return builder.build();
+ }
+
+ private static RpcEnum buildPermissionTypeEnum() {
+ RpcEnum.Builder builder = new RpcEnum.Builder();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return builder.build();
+ }
+ builder.add("PERMISSION_NONE", 0)
+ .add("PERMISSION_READ", BluetoothGattCharacteristic.PERMISSION_READ)
+ .add("PERMISSION_READ_ENCRYPTED",
+ BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED)
+ .add("PERMISSION_READ_ENCRYPTED_MITM",
+ BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED_MITM)
+ .add("PERMISSION_WRITE", BluetoothGattCharacteristic.PERMISSION_WRITE)
+ .add("PERMISSION_WRITE_ENCRYPTED",
+ BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED)
+ .add("PERMISSION_WRITE_ENCRYPTED_MITM",
+ BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED_MITM)
+ .add("PERMISSION_WRITE_SIGNED", BluetoothGattCharacteristic.PERMISSION_WRITE_SIGNED)
+ .add("PERMISSION_WRITE_SIGNED_MITM",
+ BluetoothGattCharacteristic.PERMISSION_WRITE_SIGNED_MITM);
+ return builder.build();
+ }
+
+ private static RpcEnum buildBleScanModeEnum() {
+ RpcEnum.Builder builder = new RpcEnum.Builder();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ return builder.build();
+ }
+ builder.add("SCAN_MODE_LOW_POWER", ScanSettings.SCAN_MODE_LOW_POWER)
+ .add("SCAN_MODE_BALANCED", ScanSettings.SCAN_MODE_BALANCED)
+ .add("SCAN_MODE_LOW_LATENCY", ScanSettings.SCAN_MODE_LOW_LATENCY);
+ return builder.build();
+ }
+
+ private static RpcEnum buildLocalHotspotFailedReason() {
+ RpcEnum.Builder builder = new RpcEnum.Builder();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return builder.build();
+ }
+ builder.add("ERROR_TETHERING_DISALLOWED",
+ LocalOnlyHotspotCallback.ERROR_TETHERING_DISALLOWED)
+ .add("ERROR_INCOMPATIBLE_MODE", LocalOnlyHotspotCallback.ERROR_INCOMPATIBLE_MODE)
+ .add("ERROR_NO_CHANNEL", LocalOnlyHotspotCallback.ERROR_NO_CHANNEL)
+ .add("ERROR_GENERIC", LocalOnlyHotspotCallback.ERROR_GENERIC);
+ return builder.build();
+ }
}
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java
index d6442a8..85b7058 100644
--- a/src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/RpcEnum.java
@@ -17,6 +17,7 @@
package com.google.android.mobly.snippet.bundled.utils;
import com.google.common.collect.ImmutableBiMap;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
/**
* A container type for handling String-Integer enum conversion in Rpc protocol.
@@ -27,20 +28,20 @@ import com.google.common.collect.ImmutableBiMap;
* <p>Once built, an RpcEnum object is immutable.
*/
public class RpcEnum {
- private final ImmutableBiMap<String, Integer> mEnums;
+ private final ImmutableBiMap<String, Integer> enums;
private RpcEnum(ImmutableBiMap.Builder<String, Integer> builder) {
- mEnums = builder.buildOrThrow();
+ enums = builder.buildOrThrow();
}
/**
* Get the int value of an enum based on its String value.
*
* @param enumString
- * @return
+ * @return int value
*/
public int getInt(String enumString) {
- Integer result = mEnums.get(enumString);
+ Integer result = enums.get(enumString);
if (result == null) {
throw new NoSuchFieldError("No int value found for: " + enumString);
}
@@ -51,12 +52,12 @@ public class RpcEnum {
* Get the String value of an enum based on its int value.
*
* @param enumInt
- * @return
+ * @return string value
*/
public String getString(int enumInt) {
- String result = mEnums.inverse().get(enumInt);
+ String result = enums.inverse().get(enumInt);
if (result == null) {
- throw new NoSuchFieldError("No String value found for: " + enumInt);
+ return String.format("UNKNOWN_VALUE[%s].", enumInt);
}
return result;
}
@@ -76,6 +77,7 @@ public class RpcEnum {
* @param enumInt
* @return
*/
+ @CanIgnoreReturnValue
public Builder add(String enumString, int enumInt) {
builder.put(enumString, enumInt);
return this;
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java b/src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java
index 376bcb5..bd9a76f 100644
--- a/src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/utils/Utils.java
@@ -16,9 +16,14 @@
package com.google.android.mobly.snippet.bundled.utils;
+import android.app.UiAutomation;
+import android.os.Build;
+import android.content.Context;
+import androidx.test.platform.app.InstrumentationRegistry;
import com.google.android.mobly.snippet.bundled.SmsSnippet;
import com.google.android.mobly.snippet.event.EventCache;
import com.google.android.mobly.snippet.event.SnippetEvent;
+import com.google.android.mobly.snippet.util.Log;
import com.google.common.primitives.Primitives;
import com.google.common.reflect.TypeToken;
import java.lang.reflect.InvocationTargetException;
@@ -210,4 +215,23 @@ public final class Utils {
}
return new String(hexChars);
}
+
+ public static void adaptShellPermissionIfRequired(Context context) throws Throwable {
+ if (context.getApplicationContext().getApplicationInfo().targetSdkVersion >= 29
+ && Build.VERSION.SDK_INT >= 29) {
+ Log.d("Elevating permission require to enable support for privileged operation in Android Q+");
+ UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ uia.adoptShellPermissionIdentity();
+ try {
+ Class<?> cls = Class.forName("android.app.UiAutomation");
+ Method destroyMethod = cls.getDeclaredMethod("destroy");
+ destroyMethod.invoke(uia);
+ } catch (NoSuchMethodException
+ | IllegalAccessException
+ | ClassNotFoundException
+ | InvocationTargetException e) {
+ throw new RuntimeException("Failed to cleaup Ui Automation", e);
+ }
+ }
+ }
}