Merge "Import translations. DO NOT MERGE ANYWHERE" into tm-dev
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 7a57426..db1d7e9 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -340,7 +340,7 @@
     method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setSsid(@Nullable String);
     method @NonNull public android.net.NetworkCapabilities.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>);
     method @NonNull public android.net.NetworkCapabilities.Builder setTransportInfo(@Nullable android.net.TransportInfo);
-    method @NonNull public android.net.NetworkCapabilities.Builder setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
+    method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
     method @NonNull public static android.net.NetworkCapabilities.Builder withoutDefaultCapabilities();
   }
 
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index e25a855..a174fe3 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -995,21 +995,25 @@
     // LINT.ThenChange(packages/modules/Connectivity/service/native/include/Common.h)
 
     /**
-     * Specify default rule which may allow or drop packets depending on existing policy.
+     * A firewall rule which allows or drops packets depending on existing policy.
+     * Used by {@link #setUidFirewallRule(int, int, int)} to follow existing policy to handle
+     * specific uid's packets in specific firewall chain.
      * @hide
      */
     @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_RULE_DEFAULT = 0;
 
     /**
-     * Specify allow rule which allows packets.
+     * A firewall rule which allows packets. Used by {@link #setUidFirewallRule(int, int, int)} to
+     * allow specific uid's packets in specific firewall chain.
      * @hide
      */
     @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_RULE_ALLOW = 1;
 
     /**
-     * Specify deny rule which drops packets.
+     * A firewall rule which drops packets. Used by {@link #setUidFirewallRule(int, int, int)} to
+     * drop specific uid's packets in specific firewall chain.
      * @hide
      */
     @SystemApi(client = MODULE_LIBRARIES)
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index f7f2f57..97b1f32 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -863,8 +863,11 @@
     }
 
     /**
-     * Get the underlying networks of this network. If the caller is not system privileged, this is
-     * always redacted to null and it will be never useful to the caller.
+     * Get the underlying networks of this network. If the caller doesn't have one of
+     * {@link android.Manifest.permission.NETWORK_FACTORY},
+     * {@link android.Manifest.permission.NETWORK_SETTINGS} and
+     * {@link NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}, this is always redacted to null and
+     * it will be never useful to the caller.
      *
      * @return <li>If the list is null, this network hasn't declared underlying networks.</li>
      *         <li>If the list is empty, this network has declared that it has no underlying
@@ -2650,7 +2653,7 @@
     /**
      * Builder class for NetworkCapabilities.
      *
-     * This class is mainly for for {@link NetworkAgent} instances to use. Many fields in
+     * This class is mainly for {@link NetworkAgent} instances to use. Many fields in
      * the built class require holding a signature permission to use - mostly
      * {@link android.Manifest.permission.NETWORK_FACTORY}, but refer to the specific
      * description of each setter. As this class lives entirely in app space it does not
@@ -3058,9 +3061,20 @@
         /**
          * Set the underlying networks of this network.
          *
+         * <p>This API is mainly for {@link NetworkAgent}s who hold
+         * {@link android.Manifest.permission.NETWORK_FACTORY} to set its underlying networks.
+         *
+         * <p>The underlying networks are only visible for the receiver who has one of
+         * {@link android.Manifest.permission.NETWORK_FACTORY},
+         * {@link android.Manifest.permission.NETWORK_SETTINGS} and
+         * {@link NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}.
+         * If the receiver doesn't have required permissions, the field will be cleared before
+         * sending to the caller.</p>
+         *
          * @param networks The underlying networks of this network.
          */
         @NonNull
+        @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
         public Builder setUnderlyingNetworks(@Nullable List<Network> networks) {
             mCaps.setUnderlyingNetworks(networks);
             return this;
diff --git a/nearby/tests/multidevices/clients/proguard.flags b/nearby/tests/multidevices/clients/proguard.flags
index fd494a8..11938cd 100644
--- a/nearby/tests/multidevices/clients/proguard.flags
+++ b/nearby/tests/multidevices/clients/proguard.flags
@@ -3,8 +3,8 @@
      *;
 }
 
-# Keep simulator reflection callback.
--keep class android.nearby.fastpair.provider.** {
+# Keep AdvertisingSetCallback#onOwnAddressRead callback.
+-keep class * extends android.bluetooth.le.AdvertisingSetCallback {
      *;
 }
 
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/FastPairProviderSimulatorSnippet.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/FastPairProviderSimulatorSnippet.kt
index 4a8a772..922e950 100644
--- a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/FastPairProviderSimulatorSnippet.kt
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/FastPairProviderSimulatorSnippet.kt
@@ -70,4 +70,10 @@
     fun getBluetoothLeAddress(): String {
         return fastPairProviderSimulatorController.getProviderSimulatorBleAddress()
     }
+
+    /** Gets the latest account key received on the Fast Pair provider simulator */
+    @Rpc(description = "Gets the latest account key received on the Fast Pair provider simulator.")
+    fun getLatestReceivedAccountKey(): String? {
+        return fastPairProviderSimulatorController.getLatestReceivedAccountKey()
+    }
 }
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/controller/FastPairProviderSimulatorController.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/controller/FastPairProviderSimulatorController.kt
index 2ab6dbd..a2d2659 100644
--- a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/controller/FastPairProviderSimulatorController.kt
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/provider/controller/FastPairProviderSimulatorController.kt
@@ -21,7 +21,7 @@
 import android.nearby.fastpair.provider.FastPairSimulator
 import android.nearby.fastpair.provider.bluetooth.BluetoothController
 import com.google.android.mobly.snippet.util.Log
-import com.google.common.io.BaseEncoding
+import com.google.common.io.BaseEncoding.base64
 
 class FastPairProviderSimulatorController(private val context: Context) :
     FastPairSimulator.AdvertisingChangedCallback, BluetoothController.EventListener {
@@ -50,7 +50,7 @@
     ) {
         eventListener = listener
 
-        val antiSpoofingKey = BaseEncoding.base64().decode(antiSpoofingKeyString)
+        val antiSpoofingKey = base64().decode(antiSpoofingKeyString)
         simulator = FastPairSimulator(
             context, FastPairSimulator.Options.builder(modelId)
                 .setAdvertisingModelId(modelId)
@@ -68,6 +68,9 @@
 
     fun getProviderSimulatorBleAddress() = simulator!!.bleAddress!!
 
+    fun getLatestReceivedAccountKey() =
+        simulator!!.accountKey?.let { base64().encode(it.toByteArray()) }
+
     /**
      * Called when we change our BLE advertisement.
      *
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/FastPairSeekerSnippet.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/FastPairSeekerSnippet.kt
index 65856d8..fd4f4b4 100644
--- a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/FastPairSeekerSnippet.kt
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/FastPairSeekerSnippet.kt
@@ -23,9 +23,11 @@
 import android.nearby.ScanRequest
 import android.nearby.fastpair.seeker.FAKE_TEST_ACCOUNT_NAME
 import android.nearby.multidevices.fastpair.seeker.data.FastPairTestDataManager
+import android.nearby.multidevices.fastpair.seeker.events.PairingCallbackEvents
 import android.nearby.multidevices.fastpair.seeker.events.ScanCallbackEvents
 import android.nearby.multidevices.fastpair.seeker.ui.CheckNearbyHalfSheetUiTest
 import android.nearby.multidevices.fastpair.seeker.ui.DismissNearbyHalfSheetUiTest
+import android.nearby.multidevices.fastpair.seeker.ui.PairByNearbyHalfSheetUiTest
 import androidx.test.core.app.ApplicationProvider
 import com.google.android.mobly.snippet.Snippet
 import com.google.android.mobly.snippet.rpc.AsyncRpc
@@ -114,7 +116,7 @@
     @Rpc(description = "Puts an array of FastPairAccountKeyDeviceMetadata into test data cache.")
     fun putAccountKeyDeviceMetadata(json: String) {
         Log.i("Puts an array of FastPairAccountKeyDeviceMetadata into test data cache.")
-        fastPairTestDataManager.sendAccountKeyDeviceMetadata(json)
+        fastPairTestDataManager.sendAccountKeyDeviceMetadataJsonArray(json)
     }
 
     /** Dumps all FastPairAccountKeyDeviceMetadata from the test data cache. */
@@ -142,11 +144,25 @@
         DismissNearbyHalfSheetUiTest().dismissHalfSheet()
     }
 
+    /** Starts pairing by interacting with half sheet UI.
+     *
+     * @param callbackId the callback ID corresponding to the
+     * {@link FastPairSeekerSnippet#startPairing} call that started the pairing.
+     */
+    @AsyncRpc(description = "Starts pairing by interacting with half sheet UI.")
+    fun startPairing(callbackId: String) {
+        Log.i("Starts pairing by interacting with half sheet UI.")
+
+        PairByNearbyHalfSheetUiTest().clickConnectButton()
+        fastPairTestDataManager.registerDataReceiveListener(PairingCallbackEvents(callbackId))
+    }
+
     /** Invokes when the snippet runner shutting down. */
     override fun shutdown() {
         super.shutdown()
 
         Log.i("Resets the Fast Pair test data cache.")
+        fastPairTestDataManager.unregisterDataReceiveListener()
         fastPairTestDataManager.sendResetCache()
     }
 }
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/data/FastPairTestDataManager.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/data/FastPairTestDataManager.kt
index 291aad8..239ac61 100644
--- a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/data/FastPairTestDataManager.kt
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/data/FastPairTestDataManager.kt
@@ -19,12 +19,20 @@
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
-import android.nearby.fastpair.seeker.*
+import android.content.IntentFilter
+import android.nearby.fastpair.seeker.ACTION_RESET_TEST_DATA_CACHE
+import android.nearby.fastpair.seeker.ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA
+import android.nearby.fastpair.seeker.ACTION_SEND_ANTISPOOF_KEY_DEVICE_METADATA
+import android.nearby.fastpair.seeker.ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA
+import android.nearby.fastpair.seeker.DATA_JSON_STRING_KEY
+import android.nearby.fastpair.seeker.DATA_MODEL_ID_STRING_KEY
+import android.nearby.fastpair.seeker.FastPairTestDataCache
 import android.util.Log
 
 /** Manage local FastPairTestDataCache and send to/sync from the remote cache in data provider. */
 class FastPairTestDataManager(private val context: Context) : BroadcastReceiver() {
     val testDataCache = FastPairTestDataCache()
+    var listener: EventListener? = null
 
     /** Puts a model id to FastPairAntispoofKeyDeviceMetadata pair into local and remote cache.
      *
@@ -41,17 +49,17 @@
         testDataCache.putAntispoofKeyDeviceMetadata(modelId, json)
     }
 
-    /** Puts account key device metadata to local and remote cache.
+    /** Puts account key device metadata array to local and remote cache.
      *
      * @param json a string of FastPairAccountKeyDeviceMetadata JSON array.
      */
-    fun sendAccountKeyDeviceMetadata(json: String) {
+    fun sendAccountKeyDeviceMetadataJsonArray(json: String) {
         Intent().also { intent ->
             intent.action = ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA
             intent.putExtra(DATA_JSON_STRING_KEY, json)
             context.sendBroadcast(intent)
         }
-        testDataCache.putAccountKeyDeviceMetadata(json)
+        testDataCache.putAccountKeyDeviceMetadataJsonArray(json)
     }
 
     /** Clears local and remote cache. */
@@ -73,12 +81,33 @@
             ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA -> {
                 Log.d(TAG, "ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA received!")
                 val json = intent.getStringExtra(DATA_JSON_STRING_KEY)!!
-                testDataCache.putAccountKeyDeviceMetadata(json)
+                testDataCache.putAccountKeyDeviceMetadataJsonObject(json)
+                listener?.onManageFastPairAccountDevice(json)
             }
             else -> Log.d(TAG, "Unknown action received!")
         }
     }
 
+    fun registerDataReceiveListener(listener: EventListener) {
+        this.listener = listener
+        val bondStateFilter = IntentFilter(ACTION_WRITE_ACCOUNT_KEY_DEVICE_METADATA)
+        context.registerReceiver(this, bondStateFilter)
+    }
+
+    fun unregisterDataReceiveListener() {
+        this.listener = null
+        context.unregisterReceiver(this)
+    }
+
+    /** Interface for listening the data receive from the remote cache in data provider. */
+    interface EventListener {
+        /** Reports a FastPairAccountKeyDeviceMetadata write into the cache.
+         *
+         * @param json the FastPairAccountKeyDeviceMetadata as JSON object string.
+         */
+        fun onManageFastPairAccountDevice(json: String)
+    }
+
     companion object {
         private const val TAG = "FastPairTestDataManager"
     }
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/PairingCallbackEvents.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/PairingCallbackEvents.kt
new file mode 100644
index 0000000..19de1d9
--- /dev/null
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/events/PairingCallbackEvents.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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 android.nearby.multidevices.fastpair.seeker.events
+
+import android.nearby.multidevices.fastpair.seeker.data.FastPairTestDataManager
+import com.google.android.mobly.snippet.util.postSnippetEvent
+
+/** The Mobly snippet events to report to the Python side. */
+class PairingCallbackEvents(private val callbackId: String) :
+    FastPairTestDataManager.EventListener {
+
+    /** Reports a FastPairAccountKeyDeviceMetadata write into the cache.
+     *
+     * @param json the FastPairAccountKeyDeviceMetadata as JSON object string.
+     */
+    override fun onManageFastPairAccountDevice(json: String) {
+        postSnippetEvent(callbackId, "onManageAccountDevice") {
+            putString("accountDeviceJsonString", json)
+        }
+    }
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ui/PairByNearbyHalfSheetUiTest.kt b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ui/PairByNearbyHalfSheetUiTest.kt
new file mode 100644
index 0000000..9028668
--- /dev/null
+++ b/nearby/tests/multidevices/clients/src/android/nearby/multidevices/fastpair/seeker/ui/PairByNearbyHalfSheetUiTest.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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 android.nearby.multidevices.fastpair.seeker.ui
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** An instrumented test to start pairing by interacting with Nearby half sheet UI.
+ *
+ * To run this test directly:
+ * am instrument -w -r \
+ * -e class android.nearby.multidevices.fastpair.seeker.ui.PairByNearbyHalfSheetUiTest \
+ * android.nearby.multidevices/androidx.test.runner.AndroidJUnitRunner
+ */
+@RunWith(AndroidJUnit4::class)
+class PairByNearbyHalfSheetUiTest {
+    private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+    @Test
+    fun clickConnectButton() {
+        val connectButton = NearbyHalfSheetUiMap.DevicePairingFragment.connectButton
+        device.findObject(connectButton).click()
+        device.wait(Until.gone(connectButton), CONNECT_BUTTON_TIMEOUT_MILLS)
+    }
+
+    companion object {
+        const val CONNECT_BUTTON_TIMEOUT_MILLS = 3000L
+    }
+}
\ No newline at end of file
diff --git a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/shared/android/nearby/fastpair/seeker/FastPairTestDataCache.kt b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/shared/android/nearby/fastpair/seeker/FastPairTestDataCache.kt
index be94031..e08a122 100644
--- a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/shared/android/nearby/fastpair/seeker/FastPairTestDataCache.kt
+++ b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/shared/android/nearby/fastpair/seeker/FastPairTestDataCache.kt
@@ -31,12 +31,18 @@
     private val antispoofKeyDeviceMetadataDataMap =
         mutableMapOf<String, FastPairAntispoofKeyDeviceMetadataData>()
 
-    fun putAccountKeyDeviceMetadata(json: String) {
+    fun putAccountKeyDeviceMetadataJsonArray(json: String) {
         accountKeyDeviceMetadataList +=
             gson.fromJson(json, Array<FastPairAccountKeyDeviceMetadataData>::class.java)
                 .map { it.toFastPairAccountKeyDeviceMetadata() }
     }
 
+    fun putAccountKeyDeviceMetadataJsonObject(json: String) {
+        accountKeyDeviceMetadataList +=
+            gson.fromJson(json, FastPairAccountKeyDeviceMetadataData::class.java)
+                .toFastPairAccountKeyDeviceMetadata()
+    }
+
     fun putAccountKeyDeviceMetadata(accountKeyDeviceMetadata: FastPairAccountKeyDeviceMetadata) {
         accountKeyDeviceMetadataList += accountKeyDeviceMetadata
     }
diff --git a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/data/FastPairTestDataManager.kt b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/data/FastPairTestDataManager.kt
index f226789..e924da1 100644
--- a/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/data/FastPairTestDataManager.kt
+++ b/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider/src/android/nearby/fastpair/seeker/data/FastPairTestDataManager.kt
@@ -72,7 +72,7 @@
             ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA -> {
                 Log.d(TAG, "ACTION_SEND_ACCOUNT_KEY_DEVICE_METADATA received!")
                 val json = intent.getStringExtra(DATA_JSON_STRING_KEY)!!
-                testDataCache.putAccountKeyDeviceMetadata(json)
+                testDataCache.putAccountKeyDeviceMetadataJsonArray(json)
             }
             ACTION_RESET_TEST_DATA_CACHE -> {
                 Log.d(TAG, "ACTION_RESET_TEST_DATA_CACHE received!")
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp b/nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp
index 920834a..e01c436 100644
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/Android.bp
@@ -22,7 +22,16 @@
         "src/**/*.java",
         "src/**/*.kt",
     ],
-    sdk_version: "test_current",
+    sdk_version: "core_platform",
+    libs: [
+        // order matters: classes in framework-bluetooth are resolved before framework, meaning
+        // @hide APIs in framework-bluetooth are resolved before @SystemApi stubs in framework
+        "framework-bluetooth.impl",
+        "framework",
+
+        // if sdk_version="" this gets automatically included, but here we need to add manually.
+        "framework-res",
+    ],
     static_libs: [
         "NearbyFastPairProviderLiteProtos",
         "androidx.test.core",
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/Android.bp b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/Android.bp
index 79c5007..87d352f 100644
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/Android.bp
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/Android.bp
@@ -16,9 +16,20 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+// Build and install NearbyFastPairProviderSimulatorApp to your phone:
+// m NearbyFastPairProviderSimulatorApp
+// adb root
+// adb remount && adb reboot (make first time remount work)
+//
+// adb root
+// adb remount
+// adb push ${ANDROID_PRODUCT_OUT}/system/app/NearbyFastPairProviderSimulatorApp /system/app/
+// adb reboot
 android_app {
     name: "NearbyFastPairProviderSimulatorApp",
     sdk_version: "test_current",
+    // Sign with "platform" certificate for accessing Bluetooth @SystemAPI
+    certificate: "platform",
     static_libs: ["NearbyFastPairProviderSimulatorLib"],
     optimize: {
         enabled: true,
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proguard.flags b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proguard.flags
index 28680b3..0827c60 100644
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proguard.flags
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/simulator_app/proguard.flags
@@ -1,5 +1,5 @@
-# Keep simulator reflection callback.
--keep class android.nearby.fastpair.provider.** {
+# Keep AdvertisingSetCallback#onOwnAddressRead callback.
+-keep class * extends android.bluetooth.le.AdvertisingSetCallback {
      *;
 }
 
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulator.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulator.java
index 232e84b..0d5563e 100644
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulator.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/FastPairSimulator.java
@@ -107,8 +107,6 @@
 import com.android.server.nearby.common.bluetooth.fastpair.Ltv;
 import com.android.server.nearby.common.bluetooth.fastpair.MessageStreamHmacEncoder;
 import com.android.server.nearby.common.bluetooth.fastpair.NamingEncoder;
-import com.android.server.nearby.common.bluetooth.fastpair.Reflect;
-import com.android.server.nearby.common.bluetooth.fastpair.ReflectionException;
 
 import com.google.common.base.Ascii;
 import com.google.common.primitives.Bytes;
@@ -185,15 +183,6 @@
 
     private static final long ADVERTISING_REFRESH_DELAY_1_MIN = TimeUnit.MINUTES.toMillis(1);
 
-    /** The user will be prompted to accept or deny the incoming pairing request */
-    public static final int PAIRING_VARIANT_CONSENT = 3;
-
-    /**
-     * The user will be prompted to enter the passkey displayed on remote device. This is used for
-     * Bluetooth 2.1 pairing.
-     */
-    public static final int PAIRING_VARIANT_DISPLAY_PASSKEY = 4;
-
     /**
      * The size of account key filter in bytes is (1.2*n + 3), n represents the size of account key,
      * see https://developers.google.com/nearby/fast-pair/spec#advertising_when_not_discoverable.
@@ -299,7 +288,7 @@
                         // the passkey later over GATT.
                         mLocalPasskey = key;
                         checkPasskey();
-                    } else if (variant == PAIRING_VARIANT_DISPLAY_PASSKEY) {
+                    } else if (variant == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) {
                         if (mPasskeyEventCallback != null) {
                             mPasskeyEventCallback.onPasskeyRequested(
                                     FastPairSimulator.this::enterPassKey);
@@ -307,7 +296,7 @@
                             mLogger.log("passkeyEventCallback is not set!");
                             enterPassKey(key);
                         }
-                    } else if (variant == PAIRING_VARIANT_CONSENT) {
+                    } else if (variant == BluetoothDevice.PAIRING_VARIANT_CONSENT) {
                         setPasskeyConfirmation(true);
 
                     } else if (variant == BluetoothDevice.PAIRING_VARIANT_PIN) {
@@ -1969,14 +1958,7 @@
 
     public void enterPassKey(int passkey) {
         mLogger.log("enterPassKey called with passkey %d.", passkey);
-        try {
-            boolean result =
-                    (Boolean) Reflect.on(mPairingDevice).withMethod("setPasskey", int.class).get(
-                            passkey);
-            mLogger.log("enterPassKey called with result %b", result);
-        } catch (ReflectionException e) {
-            mLogger.log("enterPassKey meet Exception %s.", e.getMessage());
-        }
+        mPairingDevice.setPairingConfirmation(true);
     }
 
     private void checkPasskey() {
@@ -2290,19 +2272,11 @@
     }
 
     public void disconnect(BluetoothProfile profile, BluetoothDevice device) {
-        try {
-            Reflect.on(profile).withMethod("disconnect", BluetoothDevice.class).invoke(device);
-        } catch (ReflectionException e) {
-            mLogger.log(e, "Error disconnecting device=%s from profile=%s", device, profile);
-        }
+        device.disconnect();
     }
 
     public void removeBond(BluetoothDevice device) {
-        try {
-            Reflect.on(device).withMethod("removeBond").invoke();
-        } catch (ReflectionException e) {
-            mLogger.log(e, "Error removing bond for device=%s", device);
-        }
+        device.removeBond();
     }
 
     public void resetAccountKeys() {
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/OreoFastPairAdvertiser.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/OreoFastPairAdvertiser.java
index bb77c11..bc0cdfe 100644
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/OreoFastPairAdvertiser.java
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/OreoFastPairAdvertiser.java
@@ -18,7 +18,6 @@
 
 import static com.google.common.io.BaseEncoding.base16;
 
-import android.annotation.TargetApi;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.le.AdvertiseData;
 import android.bluetooth.le.AdvertiseSettings;
@@ -27,21 +26,17 @@
 import android.bluetooth.le.AdvertisingSetParameters;
 import android.bluetooth.le.BluetoothLeAdvertiser;
 import android.nearby.fastpair.provider.utils.Logger;
-import android.os.Build.VERSION_CODES;
 import android.os.ParcelUuid;
 
 import androidx.annotation.Nullable;
 
 import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService;
-import com.android.server.nearby.common.bluetooth.fastpair.Reflect;
-import com.android.server.nearby.common.bluetooth.fastpair.ReflectionException;
 
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
 import java.util.Locale;
 
 /** Fast Pair advertiser taking advantage of new Android Oreo advertising features. */
-@TargetApi(VERSION_CODES.O)
 public final class OreoFastPairAdvertiser implements FastPairAdvertiser {
     private static final String TAG = "OreoFastPairAdvertiser";
     private final Logger mLogger = new Logger(TAG);
@@ -63,13 +58,7 @@
                     mLogger.log("Advertising succeeded, advertising at %s dBm", txPower);
                     simulator.setIsAdvertising(true);
                     mAdvertisingSet = set;
-
-                    try {
-                        // Requires custom Android build, see callback below.
-                        Reflect.on(set).withMethod("getOwnAddress").invoke();
-                    } catch (ReflectionException e) {
-                        mLogger.log(e, "Error calling getOwnAddress for AdvertisingSet");
-                    }
+                    mAdvertisingSet.getOwnAddress();
                 } else {
                     mLogger.log(
                             new IllegalStateException(),
@@ -88,7 +77,8 @@
                 }
             }
 
-            // Called via reflection with AdvertisingSet.getOwnAddress().
+            // Callback for AdvertisingSet.getOwnAddress().
+            @Override
             public void onOwnAddressRead(
                     AdvertisingSet set, int addressType, String address) {
                 if (!address.equals(simulator.getBleAddress())) {
@@ -108,12 +98,7 @@
     public void startAdvertising(@Nullable byte[] serviceData) {
         // To be informed that BLE address is rotated, we need to polling query it asynchronously.
         if (mAdvertisingSet != null) {
-            try {
-                // Requires custom Android build, see callback: onOwnAddressRead.
-                Reflect.on(mAdvertisingSet).withMethod("getOwnAddress").invoke();
-            } catch (ReflectionException ignored) {
-                // Ignore it due to user already knows it when setting advertisingSet.
-            }
+            mAdvertisingSet.getOwnAddress();
         }
 
         if (mSimulator.isDestroyed()) {
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothController.kt b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothController.kt
index 6a3e59e..0cc0c92 100644
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothController.kt
+++ b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/bluetooth/BluetoothController.kt
@@ -26,15 +26,13 @@
 import android.content.IntentFilter
 import android.nearby.fastpair.provider.FastPairSimulator
 import android.nearby.fastpair.provider.utils.Logger
-import android.nearby.fastpair.provider.utils.Reflect
-import android.nearby.fastpair.provider.utils.ReflectionException
 import android.os.SystemClock
 import android.provider.Settings
 
 /** Controls the local Bluetooth adapter for Fast Pair testing. */
 class BluetoothController(
     private val context: Context,
-    private val listener: EventListener,
+    private val listener: EventListener
 ) : BroadcastReceiver() {
     private val mLogger = Logger(TAG)
     private val bluetoothAdapter: BluetoothAdapter =
@@ -67,23 +65,10 @@
      * ```
      */
     fun setIoCapability(ioCapabilityClassic: Int, ioCapabilityBLE: Int) {
-        try {
-            Reflect.on(bluetoothAdapter)
-                .withMethod("setIoCapability", Int::class.javaPrimitiveType)[
-                    ioCapabilityClassic]
-        } catch (e: ReflectionException) {
-            mLogger.log(e, "Error setIoCapability to %s: %s", ioCapabilityClassic)
-        }
-        try {
-            Reflect.on(bluetoothAdapter)
-                .withMethod("setLeIoCapability", Int::class.javaPrimitiveType)[
-                    ioCapabilityBLE]
-        } catch (e: ReflectionException) {
-            mLogger.log(e, "Error setLeIoCapability to %s: %s", ioCapabilityBLE)
-        }
+        bluetoothAdapter.ioCapability = ioCapabilityClassic
+        bluetoothAdapter.leIoCapability = ioCapabilityBLE
 
         // Toggling airplane mode on/off to restart Bluetooth stack and reset the BLE.
-        // Since it also increases reliability, we will do so even if ReflectionException is caught.
         try {
             Settings.Global.putInt(
                 context.contentResolver,
@@ -165,7 +150,7 @@
 
                 override fun onServiceDisconnected(profile: Int) {}
             },
-            BLUETOOTH_PROFILE_A2DP_SINK
+            BluetoothProfile.A2DP_SINK
         )
     }
 
@@ -204,7 +189,8 @@
                         else -> remoteDevice
                     }
                 mLogger.log(
-                    "ACTION_BOND_STATE_CHANGED, the bound state of the remote device (%s) change to %s.",
+                    "ACTION_BOND_STATE_CHANGED, the bound state of " +
+                            "the remote device (%s) change to %s.",
                     remoteDevice?.remoteDeviceToString(),
                     bondState.bondStateToString()
                 )
@@ -284,9 +270,6 @@
     companion object {
         private const val TAG = "BluetoothController"
 
-        /** Hidden SystemApi field in [BluetoothProfile] interface. */
-        private const val BLUETOOTH_PROFILE_A2DP_SINK = 11
-
         private const val TURN_AIRPLANE_MODE_OFF = 0
         private const val TURN_AIRPLANE_MODE_ON = 1
     }
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/utils/Reflect.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/utils/Reflect.java
deleted file mode 100644
index 5ae5310..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/utils/Reflect.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * 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 android.nearby.fastpair.provider.utils;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
-/**
- * Utilities for calling methods using reflection. The main benefit of using this helper is to avoid
- * complications around exception handling when calling methods reflectively. It's not safe to use
- * Java 8's multicatch on such exceptions, because the java compiler converts multicatch into
- * ReflectiveOperationException in some instances, which doesn't work on older sdk versions.
- * Instead, use these utilities and catch ReflectionException.
- *
- * <p>Example usage:
- *
- * <pre>{@code
- * try {
- *   Reflect.on(btAdapter)
- *       .withMethod("setScanMode", int.class)
- *       .invoke(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE)
- * } catch (ReflectionException e) { }
- * }</pre>
- */
-public final class Reflect {
-    private final Object mTargetObject;
-
-    private Reflect(Object targetObject) {
-        this.mTargetObject = targetObject;
-    }
-
-    /** Creates an instance of this helper to invoke methods on the given target object. */
-    public static Reflect on(Object targetObject) {
-        return new Reflect(targetObject);
-    }
-
-    /** Finds a method with the given name and parameter types. */
-    public ReflectionMethod withMethod(String methodName, Class<?>... paramTypes)
-            throws ReflectionException {
-        try {
-            return new ReflectionMethod(mTargetObject.getClass().getMethod(methodName, paramTypes));
-        } catch (NoSuchMethodException e) {
-            throw new ReflectionException(e);
-        }
-    }
-
-    /** Represents an invokable method found reflectively. */
-    public final class ReflectionMethod {
-        private final Method mMethod;
-
-        private ReflectionMethod(Method method) {
-            this.mMethod = method;
-        }
-
-        /**
-         * Invokes this instance method with the given parameters. The called method does not return
-         * a value.
-         */
-        public void invoke(Object... parameters) throws ReflectionException {
-            try {
-                mMethod.invoke(mTargetObject, parameters);
-            } catch (IllegalAccessException e) {
-                throw new ReflectionException(e);
-            } catch (InvocationTargetException e) {
-                throw new ReflectionException(e);
-            }
-        }
-
-        /**
-         * Invokes this instance method with the given parameters. The called method returns a non
-         * null
-         * value.
-         */
-        public Object get(Object... parameters) throws ReflectionException {
-            Object value;
-            try {
-                value = mMethod.invoke(mTargetObject, parameters);
-            } catch (IllegalAccessException e) {
-                throw new ReflectionException(e);
-            } catch (InvocationTargetException e) {
-                throw new ReflectionException(e);
-            }
-            if (value == null) {
-                throw new ReflectionException(new NullPointerException());
-            }
-            return value;
-        }
-    }
-}
diff --git a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/utils/ReflectionException.java b/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/utils/ReflectionException.java
deleted file mode 100644
index 959fd11..0000000
--- a/nearby/tests/multidevices/clients/test_support/fastpair_provider/src/android/nearby/fastpair/provider/utils/ReflectionException.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * 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 android.nearby.fastpair.provider.utils;
-
-/**
- * An exception thrown during a reflection operation. Like ReflectiveOperationException, except
- * compatible on older API versions.
- */
-public final class ReflectionException extends Exception {
-    public ReflectionException(Throwable cause) {
-        super(cause.getMessage(), cause);
-    }
-}
diff --git a/nearby/tests/multidevices/host/initial_pairing_test.py b/nearby/tests/multidevices/host/initial_pairing_test.py
new file mode 100644
index 0000000..1a49045
--- /dev/null
+++ b/nearby/tests/multidevices/host/initial_pairing_test.py
@@ -0,0 +1,62 @@
+#  Copyright (C) 2022 The Android Open Source Project
+#
+#  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.
+
+"""CTS-V Nearby Mainline Fast Pair end-to-end test case: initial pairing test."""
+
+from test_helper import constants
+from test_helper import fast_pair_base_test
+
+# The model ID to simulate on provider side.
+PROVIDER_SIMULATOR_MODEL_ID = constants.DEFAULT_MODEL_ID
+# The public key to simulate as registered headsets.
+PROVIDER_SIMULATOR_ANTI_SPOOFING_KEY = constants.DEFAULT_ANTI_SPOOFING_KEY
+# The anti-spoof key device metadata JSON file for data provider at seeker side.
+PROVIDER_SIMULATOR_KDM_JSON_FILE = constants.DEFAULT_KDM_JSON_FILE
+
+# Time in seconds for events waiting.
+SETUP_TIMEOUT_SEC = constants.SETUP_TIMEOUT_SEC
+BECOME_DISCOVERABLE_TIMEOUT_SEC = constants.BECOME_DISCOVERABLE_TIMEOUT_SEC
+START_ADVERTISING_TIMEOUT_SEC = constants.START_ADVERTISING_TIMEOUT_SEC
+HALF_SHEET_POPUP_TIMEOUT_SEC = constants.HALF_SHEET_POPUP_TIMEOUT_SEC
+MANAGE_ACCOUNT_DEVICE_TIMEOUT_SEC = constants.AVERAGE_PAIRING_TIMEOUT_SEC * 2
+
+
+class InitialPairingTest(fast_pair_base_test.FastPairBaseTest):
+    """Fast Pair initial pairing test."""
+
+    def setup_test(self) -> None:
+        super().setup_test()
+        self._provider.start_model_id_advertising(PROVIDER_SIMULATOR_MODEL_ID,
+                                                  PROVIDER_SIMULATOR_ANTI_SPOOFING_KEY)
+        self._provider.wait_for_discoverable_mode(BECOME_DISCOVERABLE_TIMEOUT_SEC)
+        self._provider.wait_for_advertising_start(START_ADVERTISING_TIMEOUT_SEC)
+        self._seeker.put_anti_spoof_key_device_metadata(PROVIDER_SIMULATOR_MODEL_ID,
+                                                        PROVIDER_SIMULATOR_KDM_JSON_FILE)
+        self._seeker.set_fast_pair_scan_enabled(True)
+
+    # TODO(b/214015364): Remove Bluetooth bound on both sides ("Forget device").
+    def teardown_test(self) -> None:
+        self._seeker.set_fast_pair_scan_enabled(False)
+        self._provider.teardown_provider_simulator()
+        self._seeker.dismiss_halfsheet()
+        super().teardown_test()
+
+    def test_seeker_initial_pair_provider(self) -> None:
+        self._seeker.wait_and_assert_halfsheet_showed(
+            timeout_seconds=HALF_SHEET_POPUP_TIMEOUT_SEC,
+            expected_model_id=PROVIDER_SIMULATOR_MODEL_ID)
+        self._seeker.start_pairing()
+        self._seeker.wait_and_assert_account_device(
+            get_account_key_from_provider=self._provider.get_latest_received_account_key,
+            timeout_seconds=MANAGE_ACCOUNT_DEVICE_TIMEOUT_SEC)
diff --git a/nearby/tests/multidevices/host/suite_main.py b/nearby/tests/multidevices/host/suite_main.py
index 406a4f0..4f5d48c 100644
--- a/nearby/tests/multidevices/host/suite_main.py
+++ b/nearby/tests/multidevices/host/suite_main.py
@@ -19,6 +19,7 @@
 
 from mobly import suite_runner
 
+import initial_pairing_test
 import seeker_discover_provider_test
 import seeker_show_halfsheet_test
 
@@ -26,6 +27,7 @@
 _TEST_CLASSES_LIST = [
     seeker_discover_provider_test.SeekerDiscoverProviderTest,
     seeker_show_halfsheet_test.SeekerShowHalfSheetTest,
+    initial_pairing_test.InitialPairingTest,
 ]
 
 
diff --git a/nearby/tests/multidevices/host/test_helper/constants.py b/nearby/tests/multidevices/host/test_helper/constants.py
index 646b428..342be8f 100644
--- a/nearby/tests/multidevices/host/test_helper/constants.py
+++ b/nearby/tests/multidevices/host/test_helper/constants.py
@@ -21,12 +21,14 @@
 # Default anti-spoof Key Device Metadata JSON file for data provider at seeker side.
 DEFAULT_KDM_JSON_FILE = 'simulator_antispoofkey_devicemeta_json.txt'
 
-# Time in seconds for events waiting.
+# Time in seconds for events waiting according to Fast Pair certification guidelines:
+# https://developers.google.com/nearby/fast-pair/certification-guideline
 SETUP_TIMEOUT_SEC = 5
 BECOME_DISCOVERABLE_TIMEOUT_SEC = 10
 START_ADVERTISING_TIMEOUT_SEC = 5
-SCAN_TIMEOUT_SEC = 30
-HALF_SHEET_POPUP_TIMEOUT_SEC = 30
+SCAN_TIMEOUT_SEC = 5
+HALF_SHEET_POPUP_TIMEOUT_SEC = 5
+AVERAGE_PAIRING_TIMEOUT_SEC = 12
 
 # The phone to simulate Fast Pair provider (like headphone) needs changes in Android system:
 # 1. System permission check removal
diff --git a/nearby/tests/multidevices/host/test_helper/fast_pair_provider_simulator.py b/nearby/tests/multidevices/host/test_helper/fast_pair_provider_simulator.py
index 8a98112..d6484fb 100644
--- a/nearby/tests/multidevices/host/test_helper/fast_pair_provider_simulator.py
+++ b/nearby/tests/multidevices/host/test_helper/fast_pair_provider_simulator.py
@@ -18,6 +18,7 @@
 from mobly.controllers import android_device
 from mobly.controllers.android_device_lib import snippet_event
 import retry
+from typing import Optional
 
 from test_helper import event_helper
 
@@ -179,3 +180,11 @@
             on_received=_on_advertising_mode_change_event_received,
             on_waiting=_on_advertising_mode_change_event_waiting,
             on_missed=_on_advertising_mode_change_event_missed)
+
+    def get_latest_received_account_key(self) -> Optional[str]:
+        """Gets the latest account key received on the provider side.
+
+        Returns:
+          The account key received at provider side.
+        """
+        return self._ad.fp.getLatestReceivedAccountKey()
diff --git a/nearby/tests/multidevices/host/test_helper/fast_pair_seeker.py b/nearby/tests/multidevices/host/test_helper/fast_pair_seeker.py
index cfdb966..64fc2f2 100644
--- a/nearby/tests/multidevices/host/test_helper/fast_pair_seeker.py
+++ b/nearby/tests/multidevices/host/test_helper/fast_pair_seeker.py
@@ -14,6 +14,9 @@
 
 """Fast Pair seeker role."""
 
+import json
+from typing import Callable, Optional
+
 from mobly import asserts
 from mobly.controllers import android_device
 from mobly.controllers.android_device_lib import snippet_event
@@ -26,10 +29,12 @@
 
 # Events reported from the seeker snippet.
 ON_PROVIDER_FOUND_EVENT = 'onDiscovered'
+ON_MANAGE_ACCOUNT_DEVICE_EVENT = 'onManageAccountDevice'
 
 # Abbreviations for common use type.
 AndroidDevice = android_device.AndroidDevice
 JsonObject = utils.JsonObject
+ProviderAccountKeyCallable = Callable[[], Optional[str]]
 SnippetEvent = snippet_event.SnippetEvent
 wait_for_event = event_helper.wait_callback_event
 
@@ -41,6 +46,7 @@
         self._ad = ad
         self._ad.debug_tag = 'MainlineFastPairSeeker'
         self._scan_result_callback = None
+        self._pairing_result_callback = None
 
     def load_snippet(self) -> None:
         """Starts the seeker snippet and connects.
@@ -58,18 +64,6 @@
         """Stops the Fast Pair seeker scanning."""
         self._ad.fp.stopScan()
 
-    def start_pair(self, model_id: str, address: str) -> None:
-        """Starts the Fast Pair seeker pairing.
-
-        Args:
-          model_id: A 3-byte hex string for seeker side to recognize the provider
-            device (ex: 0x00000C).
-          address: The BLE mac address of the Fast Pair provider.
-        """
-        self._ad.log.info('Before calling startPairing')
-        self._ad.fp.startPairing(model_id, address)
-        self._ad.log.info('After calling startPairing')
-
     def wait_and_assert_provider_found(self, timeout_seconds: int,
                                        expected_model_id: str,
                                        expected_ble_mac_address: str) -> None:
@@ -137,8 +131,8 @@
 
         Args:
           timeout_seconds: The number of seconds to wait before giving up.
-          expected_model_id: The expected model ID of the remote Fast Pair provider
-            device.
+          expected_model_id: A 3-byte hex string for seeker side to recognize
+            the remote provider device (ex: 0x00000c).
         """
         self._ad.log.info('Waits and asserts the half sheet showed for model id "%s".',
                           expected_model_id)
@@ -147,3 +141,46 @@
     def dismiss_halfsheet(self) -> None:
         """Dismisses the half sheet UI if showed."""
         self._ad.fp.dismissHalfSheet()
+
+    def start_pairing(self) -> None:
+        """Starts pairing the provider via "Connect" button on half sheet UI."""
+        self._pairing_result_callback = self._ad.fp.startPairing()
+
+    def wait_and_assert_account_device(
+            self, timeout_seconds: int,
+            get_account_key_from_provider: ProviderAccountKeyCallable) -> None:
+        """Waits and asserts the onHalfSheetShowed event from the seeker.
+
+        Args:
+          timeout_seconds: The number of seconds to wait before giving up.
+          get_account_key_from_provider: The callable to get expected account key from the provider
+            side.
+        """
+
+        def _on_manage_account_device_event_received(manage_account_device_event: SnippetEvent,
+                                                     elapsed_time: int) -> bool:
+            account_key_json_str = manage_account_device_event.data['accountDeviceJsonString']
+            account_key_from_seeker = json.loads(account_key_json_str)['account_key']
+            account_key_from_provider = get_account_key_from_provider()
+            self._ad.log.info('Seeker add an account device with account key "%s" in %d seconds.',
+                              account_key_from_seeker, elapsed_time)
+            self._ad.log.info('The latest provider side account key is "%s".',
+                              account_key_from_provider)
+            return account_key_from_seeker == account_key_from_provider
+
+        def _on_manage_account_device_event_waiting(elapsed_time: int) -> None:
+            self._ad.log.info(
+                'Still waiting "%s" event callback from seeker side '
+                'after %d seconds...', ON_MANAGE_ACCOUNT_DEVICE_EVENT, elapsed_time)
+
+        def _on_manage_account_device_event_missed() -> None:
+            asserts.fail(f'Timed out after {timeout_seconds} seconds waiting for '
+                         f'the specific "{ON_MANAGE_ACCOUNT_DEVICE_EVENT}" event.')
+
+        wait_for_event(
+            callback_event_handler=self._pairing_result_callback,
+            event_name=ON_MANAGE_ACCOUNT_DEVICE_EVENT,
+            timeout_seconds=timeout_seconds,
+            on_received=_on_manage_account_device_event_received,
+            on_waiting=_on_manage_account_device_event_waiting,
+            on_missed=_on_manage_account_device_event_missed)
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index b4cc41a..0528f29 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -2238,6 +2238,13 @@
                 callingAttributionTag);
     }
 
+    private void redactUnderlyingNetworksForCapabilities(NetworkCapabilities nc, int pid, int uid) {
+        if (nc.getUnderlyingNetworks() != null
+                && !checkNetworkFactoryOrSettingsPermission(pid, uid)) {
+            nc.setUnderlyingNetworks(null);
+        }
+    }
+
     @VisibleForTesting
     NetworkCapabilities networkCapabilitiesRestrictedForCallerPermissions(
             NetworkCapabilities nc, int callerPid, int callerUid) {
@@ -2250,8 +2257,6 @@
         if (!checkSettingsPermission(callerPid, callerUid)) {
             newNc.setUids(null);
             newNc.setSSID(null);
-            // TODO: Processes holding NETWORK_FACTORY should be able to see the underlying networks
-            newNc.setUnderlyingNetworks(null);
         }
         if (newNc.getNetworkSpecifier() != null) {
             newNc.setNetworkSpecifier(newNc.getNetworkSpecifier().redact());
@@ -2265,6 +2270,7 @@
             newNc.setAllowedUids(new ArraySet<>());
             newNc.setSubscriptionIds(Collections.emptySet());
         }
+        redactUnderlyingNetworksForCapabilities(newNc, callerPid, callerUid);
 
         return newNc;
     }
@@ -2877,6 +2883,15 @@
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
     }
 
+    private boolean checkNetworkFactoryOrSettingsPermission(int pid, int uid) {
+        return PERMISSION_GRANTED == mContext.checkPermission(
+                android.Manifest.permission.NETWORK_FACTORY, pid, uid)
+                || PERMISSION_GRANTED == mContext.checkPermission(
+                android.Manifest.permission.NETWORK_SETTINGS, pid, uid)
+                || PERMISSION_GRANTED == mContext.checkPermission(
+                NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, pid, uid);
+    }
+
     private boolean checkSettingsPermission() {
         return checkAnyPermissionOf(
                 android.Manifest.permission.NETWORK_SETTINGS,
diff --git a/service/src/com/android/server/connectivity/FullScore.java b/service/src/com/android/server/connectivity/FullScore.java
index 799f46b..b13ba93 100644
--- a/service/src/com/android/server/connectivity/FullScore.java
+++ b/service/src/com/android/server/connectivity/FullScore.java
@@ -29,6 +29,7 @@
 import android.net.NetworkCapabilities;
 import android.net.NetworkScore;
 import android.net.NetworkScore.KeepConnectedReason;
+import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -46,6 +47,8 @@
  * they are handling a score that had the CS-managed bits set.
  */
 public class FullScore {
+    private static final String TAG = FullScore.class.getSimpleName();
+
     // This will be removed soon. Do *NOT* depend on it for any new code that is not part of
     // a migration.
     private final int mLegacyInt;
@@ -126,7 +129,15 @@
     @VisibleForTesting
     static @NonNull String policyNameOf(final int policy) {
         final String name = sMessageNames.get(policy);
-        if (name == null) throw new IllegalArgumentException("Unknown policy: " + policy);
+        if (name == null) {
+            // Don't throw here because name might be null due to proguard stripping out the
+            // POLICY_* constants, potentially causing a crash only on user builds because proguard
+            // does not run on userdebug builds.
+            // TODO: make MessageUtils safer by not returning the array and instead storing it
+            // internally and providing a getter (that does not throw) for individual values.
+            Log.wtf(TAG, "Unknown policy: " + policy);
+            return Integer.toString(policy);
+        }
         return name.substring("POLICY_".length());
     }
 
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index f007b83..0504973 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -72,6 +72,7 @@
 import android.os.HandlerThread
 import android.os.Message
 import android.os.SystemClock
+import android.platform.test.annotations.AppModeFull
 import android.telephony.TelephonyManager
 import android.telephony.data.EpsBearerQosSessionAttributes
 import android.util.DebugUtils.valueToString
@@ -106,7 +107,6 @@
 import com.android.testutils.TestableNetworkCallback
 import com.android.testutils.assertThrows
 import org.junit.After
-import org.junit.Assume.assumeFalse
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -946,11 +946,9 @@
         return Pair(agent, qosTestSocket!!)
     }
 
+    @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
     @Test
     fun testQosCallbackRegisterWithUnregister() {
-        // Instant apps can't bind sockets to localhost
-        // TODO: use @AppModeFull when supported by DevSdkIgnoreRunner
-        assumeFalse(realContext.packageManager.isInstantApp())
         val (agent, socket) = setupForQosCallbackTesting()
 
         val qosCallback = TestableQosCallback()
@@ -975,11 +973,9 @@
         }
     }
 
+    @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
     @Test
     fun testQosCallbackOnQosSession() {
-        // Instant apps can't bind sockets to localhost
-        // TODO: use @AppModeFull when supported by DevSdkIgnoreRunner
-        assumeFalse(realContext.packageManager.isInstantApp())
         val (agent, socket) = setupForQosCallbackTesting()
         val qosCallback = TestableQosCallback()
         Executors.newSingleThreadExecutor().let { executor ->
@@ -1023,11 +1019,9 @@
         }
     }
 
+    @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
     @Test
     fun testQosCallbackOnError() {
-        // Instant apps can't bind sockets to localhost
-        // TODO: use @AppModeFull when supported by DevSdkIgnoreRunner
-        assumeFalse(realContext.packageManager.isInstantApp())
         val (agent, socket) = setupForQosCallbackTesting()
         val qosCallback = TestableQosCallback()
         Executors.newSingleThreadExecutor().let { executor ->
@@ -1064,11 +1058,9 @@
         }
     }
 
+    @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
     @Test
     fun testQosCallbackIdsAreMappedCorrectly() {
-        // Instant apps can't bind sockets to localhost
-        // TODO: use @AppModeFull when supported by DevSdkIgnoreRunner
-        assumeFalse(realContext.packageManager.isInstantApp())
         val (agent, socket) = setupForQosCallbackTesting()
         val qosCallback1 = TestableQosCallback()
         val qosCallback2 = TestableQosCallback()
@@ -1107,11 +1099,9 @@
         }
     }
 
+    @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
     @Test
     fun testQosCallbackWhenNetworkReleased() {
-        // Instant apps can't bind sockets to localhost
-        // TODO: use @AppModeFull when supported by DevSdkIgnoreRunner
-        assumeFalse(realContext.packageManager.isInstantApp())
         val (agent, socket) = setupForQosCallbackTesting()
         Executors.newSingleThreadExecutor().let { executor ->
             try {
@@ -1151,6 +1141,7 @@
         )
     }
 
+    @AppModeFull(reason = "Instant apps don't have permission to bind sockets.")
     @Test
     fun testUnregisterAfterReplacement() {
         // Keeps an eye on all test networks.
diff --git a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
index e7f6245..c03a9cd 100644
--- a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt
@@ -22,6 +22,7 @@
 import android.os.Build
 import android.text.TextUtils
 import android.util.ArraySet
+import android.util.Log
 import androidx.test.filters.SmallTest
 import com.android.server.connectivity.FullScore.MAX_CS_MANAGED_POLICY
 import com.android.server.connectivity.FullScore.POLICY_ACCEPT_UNVALIDATED
@@ -32,11 +33,12 @@
 import com.android.server.connectivity.FullScore.POLICY_IS_VPN
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.After
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import kotlin.reflect.full.staticProperties
 import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
 
@@ -63,6 +65,23 @@
         return mixInScore(nc, nac, validated, false /* yieldToBadWifi */, destroyed)
     }
 
+    private val TAG = this::class.simpleName
+
+    private var wtfHandler: Log.TerribleFailureHandler? = null
+
+    @Before
+    fun setUp() {
+        // policyNameOf will call Log.wtf if passed an invalid policy.
+        wtfHandler = Log.setWtfHandler() { tagString, what, system ->
+            Log.d(TAG, "WTF captured, ignoring: $tagString $what")
+        }
+    }
+
+    @After
+    fun tearDown() {
+        Log.setWtfHandler(wtfHandler)
+    }
+
     @Test
     fun testGetLegacyInt() {
         val ns = FullScore(50, 0L /* policy */, KEEP_CONNECTED_NONE)
@@ -101,10 +120,9 @@
             assertFalse(foundNames.contains(name))
             foundNames.add(name)
         }
-        assertFailsWith<IllegalArgumentException> {
-            FullScore.policyNameOf(MAX_CS_MANAGED_POLICY + 1)
-        }
         assertEquals("IS_UNMETERED", FullScore.policyNameOf(POLICY_IS_UNMETERED))
+        val invalidPolicy = MAX_CS_MANAGED_POLICY + 1
+        assertEquals(Integer.toString(invalidPolicy), FullScore.policyNameOf(invalidPolicy))
     }
 
     fun getAllPolicies() = Regex("POLICY_.*").let { nameRegex ->