Merge remote-tracking branch 'aosp/upstream-master' into mymerge am: 2ebdd873e0

Original change: https://android-review.googlesource.com/c/platform/external/mobly-bundled-snippets/+/2678056

Change-Id: I9dd183bb05fe30cce7b8ac255d07af9a55c2d931
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index a3d1737..be5cdf7 100644
--- a/Android.bp
+++ b/Android.bp
@@ -32,10 +32,11 @@
     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 @@
     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.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 @@
      *          }
      *     </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 @@
     @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 @@
         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 @@
             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 @@
             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.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.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 @@
     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 @@
      * 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 @@
             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 @@
             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.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 @@
 
         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.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 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 @@
         mWifiManager =
                 (WifiManager)
                         mContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
-        adaptShellPermissionIfRequired();
+        Utils.adaptShellPermissionIfRequired(mContext);
     }
 
     @Rpc(
@@ -403,33 +401,6 @@
     @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.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.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 @@
     // 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 @@
         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 @@
             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 @@
 
     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 @@
     }
 
     @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.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 @@
         }
         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.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 @@
         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 @@
         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 @@
 
     /** 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 @@
         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 @@
             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 @@
         }
         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 @@
  * <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 @@
      * 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 @@
          * @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 @@
         }
         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);
+        }
+      }
+    }
 }