diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-08-01 23:08:30 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-08-01 23:08:30 +0000 |
commit | 6f86ee0c341bd7f37c57d40f064a47a41928f1bd (patch) | |
tree | 96e4fe0ad770a3fdc1f1850c05a0c77b88f23de5 | |
parent | 9db2ea350918eaecdf676188364a26d0974c7315 (diff) | |
parent | ed17dbb8710329d4d841986f903620dea4071174 (diff) | |
download | mobly-bundled-snippets-android14-qpr1-release.tar.gz |
Snap for 10594510 from ed17dbb8710329d4d841986f903620dea4071174 to udc-qpr1-releaseandroid-14.0.0_r27android-14.0.0_r26android-14.0.0_r25android-14.0.0_r24android-14.0.0_r23android-14.0.0_r22android-14.0.0_r21android-14.0.0_r20android-14.0.0_r19android-14.0.0_r18android-14.0.0_r17android-14.0.0_r16android14-qpr1-s2-releaseandroid14-qpr1-release
Change-Id: I575c244d26ea4d9dfb5e2c4b55da8f9f54933d48
17 files changed, 942 insertions, 65 deletions
@@ -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", @@ -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); + } + } + } } |