Merge changes from topic "cherrypicker-L19600000954488498:N18900001264327475" into tm-dev

* changes:
  Address leftover comments
  Test [set|get]AppExclusionList
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index ecb6478..c403548 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -77,6 +77,7 @@
 import com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim;
 import com.android.networkstack.tethering.util.TetheringUtils.ForwardedStats;
 
+import java.io.IOException;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
@@ -1024,7 +1025,7 @@
             map.forEach((k, v) -> {
                 pw.println(String.format("%s: %s", k, v));
             });
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping BPF stats map: " + e);
         }
     }
@@ -1072,7 +1073,7 @@
                 return;
             }
             map.forEach((k, v) -> pw.println(ipv6UpstreamRuletoString(k, v)));
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping IPv6 upstream map: " + e);
         }
     }
@@ -1116,7 +1117,7 @@
         if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_STATS)) {
             try (BpfMap<TetherStatsKey, TetherStatsValue> statsMap = mDeps.getBpfStatsMap()) {
                 dumpRawMap(statsMap, pw);
-            } catch (ErrnoException e) {
+            } catch (ErrnoException | IOException e) {
                 pw.println("Error dumping stats map: " + e);
             }
             return;
@@ -1124,7 +1125,7 @@
         if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_UPSTREAM4)) {
             try (BpfMap<Tether4Key, Tether4Value> upstreamMap = mDeps.getBpfUpstream4Map()) {
                 dumpRawMap(upstreamMap, pw);
-            } catch (ErrnoException e) {
+            } catch (ErrnoException | IOException e) {
                 pw.println("Error dumping IPv4 map: " + e);
             }
             return;
@@ -1195,7 +1196,7 @@
             pw.increaseIndent();
             dumpIpv4ForwardingRuleMap(now, DOWNSTREAM, downstreamMap, pw);
             pw.decreaseIndent();
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping IPv4 map: " + e);
         }
     }
@@ -1220,7 +1221,7 @@
                 }
                 if (v.val > 0) pw.println(String.format("%s: %d", counterName, v.val));
             });
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping counter map: " + e);
         }
     }
@@ -1244,7 +1245,7 @@
                 pw.println(String.format("%d (%s) -> %d (%s)", k.ifIndex, getIfName(k.ifIndex),
                         v.ifIndex, getIfName(v.ifIndex)));
             });
-        } catch (ErrnoException e) {
+        } catch (ErrnoException | IOException e) {
             pw.println("Error dumping dev map: " + e);
         }
         pw.decreaseIndent();
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
index ad2faa0..75c2ad1 100644
--- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
@@ -358,8 +358,8 @@
         try {
             mTestMap.clear();
             fail("clearing already-closed map should throw");
-        } catch (ErrnoException expected) {
-            assertEquals(OsConstants.EBADF, expected.errno);
+        } catch (IllegalStateException expected) {
+            // ParcelFileDescriptor.getFd throws IllegalStateException: Already closed.
         }
     }
 
diff --git a/framework/src/android/net/LinkProperties.java b/framework/src/android/net/LinkProperties.java
index 4ce2593..a8f707e 100644
--- a/framework/src/android/net/LinkProperties.java
+++ b/framework/src/android/net/LinkProperties.java
@@ -64,7 +64,7 @@
      * @hide
      */
     @ChangeId
-    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.S) // Switch to S_V2 when it is available.
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.S_V2)
     @VisibleForTesting
     public static final long EXCLUDED_ROUTES = 186082280;
 
diff --git a/nearby/framework/java/android/nearby/INearbyManager.aidl b/nearby/framework/java/android/nearby/INearbyManager.aidl
index 3fd5ecc..0291fff 100644
--- a/nearby/framework/java/android/nearby/INearbyManager.aidl
+++ b/nearby/framework/java/android/nearby/INearbyManager.aidl
@@ -31,10 +31,10 @@
     int registerScanListener(in ScanRequest scanRequest, in IScanListener listener,
             String packageName, @nullable String attributionTag);
 
-    void unregisterScanListener(in IScanListener listener);
+    void unregisterScanListener(in IScanListener listener, String packageName, @nullable String attributionTag);
 
     void startBroadcast(in BroadcastRequestParcelable broadcastRequest,
             in IBroadcastListener callback, String packageName, @nullable String attributionTag);
 
-    void stopBroadcast(in IBroadcastListener callback);
+    void stopBroadcast(in IBroadcastListener callback, String packageName, @nullable String attributionTag);
 }
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
index a9d7cf7..8f44091 100644
--- a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
+++ b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
@@ -46,6 +46,7 @@
                 @Override
                 public NearbyDeviceParcelable createFromParcel(Parcel in) {
                     Builder builder = new Builder();
+                    builder.setScanType(in.readInt());
                     if (in.readInt() == 1) {
                         builder.setName(in.readString());
                     }
@@ -69,6 +70,12 @@
                         in.readByteArray(data);
                         builder.setData(data);
                     }
+                    if (in.readInt() == 1) {
+                        int saltLength = in.readInt();
+                        byte[] salt = new byte[saltLength];
+                        in.readByteArray(salt);
+                        builder.setData(salt);
+                    }
                     return builder.build();
                 }
 
@@ -129,6 +136,7 @@
      */
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mScanType);
         dest.writeInt(mName == null ? 0 : 1);
         if (mName != null) {
             dest.writeString(mName);
@@ -162,7 +170,9 @@
     @Override
     public String toString() {
         return "NearbyDeviceParcelable["
-                + "name="
+                + "scanType="
+                + mScanType
+                + ", name="
                 + mName
                 + ", medium="
                 + NearbyDevice.mediumToString(mMedium)
@@ -187,7 +197,8 @@
     public boolean equals(Object other) {
         if (other instanceof NearbyDeviceParcelable) {
             NearbyDeviceParcelable otherNearbyDeviceParcelable = (NearbyDeviceParcelable) other;
-            return Objects.equals(mName, otherNearbyDeviceParcelable.mName)
+            return mScanType == otherNearbyDeviceParcelable.mScanType
+                    && (Objects.equals(mName, otherNearbyDeviceParcelable.mName))
                     && (mMedium == otherNearbyDeviceParcelable.mMedium)
                     && (mTxPower == otherNearbyDeviceParcelable.mTxPower)
                     && (mRssi == otherNearbyDeviceParcelable.mRssi)
@@ -207,6 +218,7 @@
     @Override
     public int hashCode() {
         return Objects.hash(
+                mScanType,
                 mName,
                 mMedium,
                 mRssi,
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index 9073f78..106c290 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -70,6 +70,8 @@
         int ERROR = 2;
     }
 
+    private static final String TAG = "NearbyManager";
+
     /**
      * Whether allows Fast Pair to scan.
      *
@@ -204,7 +206,11 @@
                 ScanListenerTransport transport = reference != null ? reference.get() : null;
                 if (transport != null) {
                     transport.unregister();
-                    mService.unregisterScanListener(transport);
+                    mService.unregisterScanListener(transport, mContext.getPackageName(),
+                            mContext.getAttributionTag());
+                } else {
+                    Log.e(TAG, "Cannot stop scan with this callback "
+                            + "because it is never registered.");
                 }
             }
         } catch (RemoteException e) {
@@ -259,7 +265,11 @@
                 BroadcastListenerTransport transport = reference != null ? reference.get() : null;
                 if (transport != null) {
                     transport.unregister();
-                    mService.stopBroadcast(transport);
+                    mService.stopBroadcast(transport, mContext.getPackageName(),
+                            mContext.getAttributionTag());
+                } else {
+                    Log.e(TAG, "Cannot stop broadcast with this callback "
+                            + "because it is never registered.");
                 }
             }
         } catch (RemoteException e) {
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index 2dee835..5ebf1e5 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -43,7 +43,6 @@
 import com.android.server.nearby.fastpair.FastPairManager;
 import com.android.server.nearby.injector.ContextHubManagerAdapter;
 import com.android.server.nearby.injector.Injector;
-import com.android.server.nearby.presence.PresenceManager;
 import com.android.server.nearby.provider.BroadcastProviderManager;
 import com.android.server.nearby.provider.DiscoveryProviderManager;
 import com.android.server.nearby.provider.FastPairDataProvider;
@@ -58,7 +57,6 @@
     private final Context mContext;
     private Injector mInjector;
     private final FastPairManager mFastPairManager;
-    private final PresenceManager mPresenceManager;
     private final BroadcastReceiver mBluetoothReceiver =
             new BroadcastReceiver() {
                 @Override
@@ -86,7 +84,6 @@
         mBroadcastProviderManager = new BroadcastProviderManager(context, mInjector);
         final LocatorContextWrapper lcw = new LocatorContextWrapper(context, null);
         mFastPairManager = new FastPairManager(lcw);
-        mPresenceManager = new PresenceManager(lcw);
     }
 
     @VisibleForTesting
@@ -110,22 +107,36 @@
     }
 
     @Override
-    public void unregisterScanListener(IScanListener listener) {
+    public void unregisterScanListener(IScanListener listener, String packageName,
+            @Nullable String attributionTag) {
+        // Permissions check
+        enforceBluetoothPrivilegedPermission(mContext);
+        CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
+        DiscoveryPermissions.enforceDiscoveryPermission(mContext, identity);
+
         mProviderManager.unregisterScanListener(listener);
     }
 
     @Override
     public void startBroadcast(BroadcastRequestParcelable broadcastRequestParcelable,
             IBroadcastListener listener, String packageName, @Nullable String attributionTag) {
+        // Permissions check
         enforceBluetoothPrivilegedPermission(mContext);
         BroadcastPermissions.enforceBroadcastPermission(
                 mContext, CallerIdentity.fromBinder(mContext, packageName, attributionTag));
+
         mBroadcastProviderManager.startBroadcast(
                 broadcastRequestParcelable.getBroadcastRequest(), listener);
     }
 
     @Override
-    public void stopBroadcast(IBroadcastListener listener) {
+    public void stopBroadcast(IBroadcastListener listener, String packageName,
+            @Nullable String attributionTag) {
+        // Permissions check
+        enforceBluetoothPrivilegedPermission(mContext);
+        CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
+        BroadcastPermissions.enforceBroadcastPermission(mContext, identity);
+
         mBroadcastProviderManager.stopBroadcast(listener);
     }
 
@@ -156,7 +167,6 @@
                         mBluetoothReceiver,
                         new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
                 mFastPairManager.initiate();
-                mPresenceManager.initiate();
                 break;
         }
     }
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java b/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
deleted file mode 100644
index 382c47a..0000000
--- a/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
+++ /dev/null
@@ -1,136 +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 com.android.server.nearby.presence;
-
-import static com.android.server.nearby.NearbyService.TAG;
-
-import android.annotation.Nullable;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.nearby.NearbyDevice;
-import android.nearby.NearbyManager;
-import android.nearby.PresenceScanFilter;
-import android.nearby.PublicCredential;
-import android.nearby.ScanCallback;
-import android.nearby.ScanRequest;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-
-import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.common.locator.LocatorContextWrapper;
-
-import java.util.Locale;
-import java.util.concurrent.Executors;
-
-/** PresenceManager is the class initiated in nearby service to handle presence related work. */
-public class PresenceManager {
-
-    final LocatorContextWrapper mLocatorContextWrapper;
-    final Locator mLocator;
-    private final IntentFilter mIntentFilter;
-
-    private final ScanCallback mScanCallback =
-            new ScanCallback() {
-                @Override
-                public void onDiscovered(@NonNull NearbyDevice device) {
-                    Log.i(TAG, "[PresenceManager] discovered Device.");
-                }
-
-                @Override
-                public void onUpdated(@NonNull NearbyDevice device) {}
-
-                @Override
-                public void onLost(@NonNull NearbyDevice device) {}
-            };
-
-    private final BroadcastReceiver mScreenBroadcastReceiver =
-            new BroadcastReceiver() {
-                @Override
-                public void onReceive(Context context, Intent intent) {
-                    NearbyManager manager = getNearbyManager();
-                    if (manager == null) {
-                        Log.e(TAG, "Nearby Manager is null");
-                        return;
-                    }
-                    if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {
-                        Log.d(TAG, "Start CHRE scan.");
-                        byte[] secreteId = {1, 0, 0, 0};
-                        byte[] authenticityKey = {2, 0, 0, 0};
-                        byte[] publicKey = {3, 0, 0, 0};
-                        byte[] encryptedMetaData = {4, 0, 0, 0};
-                        byte[] encryptedMetaDataTag = {5, 0, 0, 0};
-                        PublicCredential publicCredential =
-                                new PublicCredential.Builder(
-                                                secreteId,
-                                                authenticityKey,
-                                                publicKey,
-                                                encryptedMetaData,
-                                                encryptedMetaDataTag)
-                                        .build();
-                        PresenceScanFilter presenceScanFilter =
-                                new PresenceScanFilter.Builder()
-                                        .setMaxPathLoss(3)
-                                        .addCredential(publicCredential)
-                                        .addPresenceAction(1)
-                                        .build();
-                        ScanRequest scanRequest =
-                                new ScanRequest.Builder()
-                                        .setScanType(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE)
-                                        .addScanFilter(presenceScanFilter)
-                                        .build();
-                        Log.i(
-                                TAG,
-                                String.format(
-                                        Locale.getDefault(),
-                                        "[PresenceManager] Start Presence scan with request: %s",
-                                        scanRequest.toString()));
-                        manager.startScan(
-                                scanRequest, Executors.newSingleThreadExecutor(), mScanCallback);
-                    } else if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
-                        Log.d(TAG, "Stop CHRE scan.");
-                        manager.stopScan(mScanCallback);
-                    }
-                }
-            };
-
-    public PresenceManager(LocatorContextWrapper contextWrapper) {
-        mLocatorContextWrapper = contextWrapper;
-        mLocator = mLocatorContextWrapper.getLocator();
-        mIntentFilter = new IntentFilter();
-    }
-
-    /** Null when the Nearby Service is not available. */
-    @Nullable
-    private NearbyManager getNearbyManager() {
-        return (NearbyManager)
-                mLocatorContextWrapper
-                        .getApplicationContext()
-                        .getSystemService(Context.NEARBY_SERVICE);
-    }
-
-    /** Function called when nearby service start. */
-    public void initiate() {
-        mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
-        mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
-        mLocatorContextWrapper
-                .getContext()
-                .registerReceiver(mScreenBroadcastReceiver, mIntentFilter);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
index 4cb6d8d..e8aea79 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
@@ -74,13 +74,15 @@
                             builder.setName(record.getDeviceName());
                         }
                         Map<ParcelUuid, byte[]> serviceDataMap = record.getServiceData();
-                        byte[] fastPairData = serviceDataMap.get(FAST_PAIR_UUID);
-                        if (fastPairData != null) {
-                            builder.setData(serviceDataMap.get(FAST_PAIR_UUID));
-                        } else {
-                            byte [] presenceData = serviceDataMap.get(PRESENCE_UUID);
-                            if (presenceData != null) {
-                                builder.setData(serviceDataMap.get(PRESENCE_UUID));
+                        if (serviceDataMap != null) {
+                            byte[] fastPairData = serviceDataMap.get(FAST_PAIR_UUID);
+                            if (fastPairData != null) {
+                                builder.setData(serviceDataMap.get(FAST_PAIR_UUID));
+                            } else {
+                                byte[] presenceData = serviceDataMap.get(PRESENCE_UUID);
+                                if (presenceData != null) {
+                                    builder.setData(serviceDataMap.get(PRESENCE_UUID));
+                                }
                             }
                         }
                     }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
index 6b9bce9..dd9cbb0 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
@@ -16,6 +16,8 @@
 
 package android.nearby.cts;
 
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.nearby.NearbyDevice;
@@ -49,6 +51,7 @@
     public void setUp() {
         mBuilder =
                 new NearbyDeviceParcelable.Builder()
+                        .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
                         .setName("testDevice")
                         .setMedium(NearbyDevice.Medium.BLE)
                         .setRssi(RSSI)
@@ -77,8 +80,8 @@
 
         assertThat(nearbyDeviceParcelable.toString())
                 .isEqualTo(
-                        "NearbyDeviceParcelable[name=testDevice, medium=BLE, txPower=0, rssi=-60,"
-                                + " action=0, bluetoothAddress="
+                        "NearbyDeviceParcelable[scanType=2, name=testDevice, medium=BLE, "
+                                + "txPower=0, rssi=-60, action=0, bluetoothAddress="
                                 + BLUETOOTH_ADDRESS
                                 + ", fastPairModelId=null, data=null, salt=null]");
     }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
index 6824ca6..7696a61 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -128,6 +128,14 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void test_stopScan_noPrivilegedPermission() {
+        mNearbyManager.startScan(mScanRequest, EXECUTOR, mScanCallback);
+        mUiAutomation.dropShellPermissionIdentity();
+        assertThrows(SecurityException.class, () -> mNearbyManager.stopScan(mScanCallback));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testStartStopBroadcast() throws InterruptedException {
         PrivateCredential credential = new PrivateCredential.Builder(SECRETE_ID, AUTHENTICITY_KEY,
                 META_DATA_ENCRYPTION_KEY, DEVICE_NAME)
diff --git a/nearby/tests/integration/untrusted/Android.bp b/nearby/tests/integration/untrusted/Android.bp
index 53dbfb7..57499e4 100644
--- a/nearby/tests/integration/untrusted/Android.bp
+++ b/nearby/tests/integration/untrusted/Android.bp
@@ -21,10 +21,14 @@
     defaults: ["mts-target-sdk-version-current"],
     sdk_version: "test_current",
 
-    srcs: ["src/**/*.kt"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
     static_libs: [
         "androidx.test.ext.junit",
         "androidx.test.rules",
+        "androidx.test.uiautomator_uiautomator",
         "junit",
         "kotlin-test",
         "truth-prebuilt",
diff --git a/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt b/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt
index 3bfac6d..7bf9f63 100644
--- a/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt
+++ b/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt
@@ -28,12 +28,14 @@
 import android.nearby.ScanRequest
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.uiautomator.LogcatWaitMixin
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertThrows
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
+import java.time.Duration
+import java.util.Calendar
 
 @RunWith(AndroidJUnit4::class)
 class NearbyManagerTest {
@@ -75,13 +77,9 @@
         }
     }
 
-    /**
-     * Verify untrusted app can't stop scan because it needs BLUETOOTH_PRIVILEGED
-     * permission which is not for use by third-party applications.
-     */
+    /** Verify untrusted app can't stop scan because it never successfully registers a callback. */
     @Test
-    @Ignore("Permission check for stopXXX not yet implement: b/229338477#comment24")
-    fun testNearbyManagerStopScan_fromUnTrustedApp_throwsException() {
+    fun testNearbyManagerStopScan_fromUnTrustedApp_logsError() {
         val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
         val scanCallback = object : ScanCallback {
             override fun onDiscovered(device: NearbyDevice) {}
@@ -90,10 +88,17 @@
 
             override fun onLost(device: NearbyDevice) {}
         }
+        val startTime = Calendar.getInstance().time
 
-        assertThrows(SecurityException::class.java) {
-            nearbyManager.stopScan(scanCallback)
-        }
+        nearbyManager.stopScan(scanCallback)
+
+        assertThat(
+            LogcatWaitMixin().waitForSpecificLog(
+                "Cannot stop scan with this callback because it is never registered.",
+                startTime,
+                WAIT_INVALID_OPERATIONS_LOGS_TIMEOUT
+            )
+        ).isTrue()
     }
 
     /**
@@ -127,17 +132,26 @@
     }
 
     /**
-     * Verify untrusted app can't stop broadcast because it needs BLUETOOTH_PRIVILEGED
-     * permission which is not for use by third-party applications.
+     * Verify untrusted app can't stop broadcast because it never successfully registers a callback.
      */
     @Test
-    @Ignore("Permission check for stopXXX not yet implement: b/229338477#comment24")
-    fun testNearbyManagerStopBroadcast_fromUnTrustedApp_throwsException() {
+    fun testNearbyManagerStopBroadcast_fromUnTrustedApp_logsError() {
         val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
         val broadcastCallback = BroadcastCallback { }
+        val startTime = Calendar.getInstance().time
 
-        assertThrows(SecurityException::class.java) {
-            nearbyManager.stopBroadcast(broadcastCallback)
-        }
+        nearbyManager.stopBroadcast(broadcastCallback)
+
+        assertThat(
+            LogcatWaitMixin().waitForSpecificLog(
+                "Cannot stop broadcast with this callback because it is never registered.",
+                startTime,
+                WAIT_INVALID_OPERATIONS_LOGS_TIMEOUT
+            )
+        ).isTrue()
+    }
+
+    companion object {
+        private val WAIT_INVALID_OPERATIONS_LOGS_TIMEOUT = Duration.ofSeconds(5)
     }
 }
diff --git a/nearby/tests/integration/untrusted/src/androidx/test/uiautomator/LogcatParser.kt b/nearby/tests/integration/untrusted/src/androidx/test/uiautomator/LogcatParser.kt
new file mode 100644
index 0000000..604e6df
--- /dev/null
+++ b/nearby/tests/integration/untrusted/src/androidx/test/uiautomator/LogcatParser.kt
@@ -0,0 +1,52 @@
+/*
+ * 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 androidx.test.uiautomator
+
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+/** A parser for logcat logs processing. */
+object LogcatParser {
+    private val LOGCAT_LOGS_PATTERN = "^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3} ".toRegex()
+    private const val LOGCAT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS"
+
+    /**
+     * Filters out the logcat logs which contains specific log and appears not before specific time.
+     *
+     * @param logcatLogs the concatenated logcat logs to filter
+     * @param specificLog the log string expected to appear
+     * @param startTime the time point to start finding the specific log
+     * @return a list of logs that match the condition
+     */
+    fun findSpecificLogAfter(
+        logcatLogs: String,
+        specificLog: String,
+        startTime: Date
+    ): List<String> = logcatLogs.split("\n")
+        .filter { it.contains(specificLog) && !parseLogTime(it)!!.before(startTime) }
+
+    /**
+     * Parses the logcat log string to extract the timestamp.
+     *
+     * @param logString the log string to parse
+     * @return the timestamp of the log
+     */
+    private fun parseLogTime(logString: String): Date? =
+        SimpleDateFormat(LOGCAT_DATE_FORMAT, Locale.US)
+            .parse(LOGCAT_LOGS_PATTERN.find(logString)!!.value)
+}
diff --git a/nearby/tests/integration/untrusted/src/androidx/test/uiautomator/LogcatWaitMixin.java b/nearby/tests/integration/untrusted/src/androidx/test/uiautomator/LogcatWaitMixin.java
new file mode 100644
index 0000000..86e39dc
--- /dev/null
+++ b/nearby/tests/integration/untrusted/src/androidx/test/uiautomator/LogcatWaitMixin.java
@@ -0,0 +1,71 @@
+/*
+ * 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 androidx.test.uiautomator;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.Date;
+
+/** A helper class to wait the specific log appear in the logcat logs. */
+public class LogcatWaitMixin extends WaitMixin<UiDevice> {
+
+    private static final String LOG_TAG = LogcatWaitMixin.class.getSimpleName();
+
+    public LogcatWaitMixin() {
+        this(UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()));
+    }
+
+    public LogcatWaitMixin(UiDevice device) {
+        super(device);
+    }
+
+    /**
+     * Waits the {@code specificLog} appear in the logcat logs after the specific {@code startTime}.
+     *
+     * @param waitTime the maximum time for waiting
+     * @return true if the specific log appear within timeout and after the startTime
+     */
+    public boolean waitForSpecificLog(
+            @NonNull String specificLog, @NonNull Date startTime, @NonNull Duration waitTime) {
+        return wait(createWaitCondition(specificLog, startTime), waitTime.toMillis());
+    }
+
+    @NonNull
+    Condition<UiDevice, Boolean> createWaitCondition(
+            @NonNull String specificLog, @NonNull Date startTime) {
+        return new Condition<UiDevice, Boolean>() {
+            @Override
+            Boolean apply(UiDevice device) {
+                String logcatLogs;
+                try {
+                    logcatLogs = device.executeShellCommand("logcat -v time -v year -d");
+                } catch (IOException e) {
+                    Log.e(LOG_TAG, "Fail to dump logcat logs on the device!", e);
+                    return Boolean.FALSE;
+                }
+                return !LogcatParser.INSTANCE
+                        .findSpecificLogAfter(logcatLogs, specificLog, startTime)
+                        .isEmpty();
+            }
+        };
+    }
+}
diff --git a/nearby/tests/multidevices/README.md b/nearby/tests/multidevices/README.md
new file mode 100644
index 0000000..1208451
--- /dev/null
+++ b/nearby/tests/multidevices/README.md
@@ -0,0 +1,145 @@
+# Nearby Mainline Fast Pair end-to-end tests
+
+This document refers to the Mainline Fast Pair project source code in the
+packages/modules/Connectivity/nearby. This is not an officially supported Google
+product.
+
+## About the Fast Pair Project
+
+The Connectivity Nearby mainline module is created in the Android T to host
+Better Together related functionality. Fast Pair is one of the main
+functionalities to provide seamless onboarding and integrated experiences for
+peripheral devices (for example, headsets like Google Pixel Buds) in the Nearby
+component.
+
+## Fully automated test
+
+### Prerequisites
+
+The fully automated end-to-end (e2e) tests are host-driven tests (which means
+test logics are in the host test scripts) using Mobly runner in Python. The two
+phones are installed with the test snippet
+`NearbyMultiDevicesClientsSnippets.apk` in the test time to let the host scripts
+control both sides for testing. Here's the overview of the test environment.
+
+Workstation (runs Python test scripts and controls Android devices through USB
+ADB) \
+├── Phone 1: As Fast Pair seeker role, to scan, pair Fast Pair devices nearby \
+└── Phone 2: As Fast Pair provider role, to simulate a Fast Pair device (for
+example, a Bluetooth headset)
+
+Note: These two phones need to be physically within 0.3 m of each other.
+
+### Prepare Phone 1 (Fast Pair seeker role)
+
+This is the phone to scan/pair Fast Pair devices nearby using the Nearby
+Mainline module. Test it by flashing with the Android T ROM.
+
+### Prepare Phone 2 (Fast Pair provider role)
+
+This is the phone to simulate a Fast Pair device (for example, a Bluetooth
+headset). Flash it with a customized ROM with the following changes:
+
+*   Adjust Bluetooth profile configurations. \
+    The Fast Pair provider simulator is an opposite role to the seeker. It needs
+    to enable/disable the following Bluetooth profile:
+    *   Disable A2DP (profile_supported_a2dp)
+    *   Disable the AVRCP controller (profile_supported_avrcp_controller)
+    *   Enable A2DP sink (profile_supported_a2dp_sink)
+    *   Enable the HFP client connection service (profile_supported_hfpclient,
+        hfp_client_connection_service_enabled)
+    *   Enable the AVRCP target (profile_supported_avrcp_target)
+    *   Enable the automatic audio focus request
+        (a2dp_sink_automatically_request_audio_focus)
+*   Adjust Bluetooth TX power limitation in Bluetooth module and disable the
+    Fast Pair in Google Play service (aka GMS)
+
+```shell
+adb root
+adb shell am broadcast \
+  -a 'com.google.android.gms.phenotype.FLAG_OVERRIDE' \
+  --es package "com.google.android.gms.nearby" \
+  --es user "\*" \
+  --esa flags "enabled" \
+  --esa types "boolean" \
+  --esa values "false" \
+  com.google.android.gms
+```
+
+### Running tests
+
+To run the tests, enter:
+
+```shell
+atest -v CtsNearbyMultiDevicesTestSuite
+```
+
+## Manual testing the seeker side with headsets
+
+Use this testing with headsets such as Google Pixel buds.
+
+The `FastPairTestDataProviderService.apk` is a run-time configurable Fast Pair
+data provider service (`FastPairDataProviderService`):
+
+`packages/modules/Connectivity/nearby/tests/multidevices/clients/test_service/fastpair_seeker_data_provider`
+
+It has a test data manager(`FastPairTestDataManager`) to receive intent
+broadcasts to add or clear the test data cache (`FastPairTestDataCache`). This
+cache provides the data to return to the Fast Pair module for onXXX calls (for
+example, `onLoadFastPairAntispoofKeyDeviceMetadata`) so you can feed the
+metadata for your device.
+
+Here are some sample uses:
+
+*   Send FastPairAntispoofKeyDeviceMetadata for PixelBuds-A to
+    FastPairTestDataCache \
+    `./fast_pair_data_provider_shell.sh -m=718c17
+    -a=../test_data/fastpair/pixelbuds-a_antispoofkey_devicemeta_json.txt`
+*   Send FastPairAccountDevicesMetadata for PixelBuds-A to FastPairTestDataCache
+    \
+    `./fast_pair_data_provider_shell.sh
+    -d=../test_data/fastpair/pixelbuds-a_account_devicemeta_json.txt`
+*   Send FastPairAntispoofKeyDeviceMetadata for Provider Simulator to
+    FastPairTestDataCache \
+    `./fast_pair_data_provider_shell.sh -m=00000c
+    -a=../test_data/fastpair/simulator_antispoofkey_devicemeta_json.txt`
+*   Send FastPairAccountDevicesMetadata for Provider Simulator to
+    FastPairTestDataCache \
+    `./fast_pair_data_provider_shell.sh
+    -d=../test_data/fastpair/simulator_account_devicemeta_json.txt`
+*   Clear FastPairTestDataCache \
+    `./fast_pair_data_provider_shell.sh -c`
+
+See
+[host/tool/fast_pair_data_provider_shell.sh](host/tool/fast_pair_data_provider_shell.sh)
+for more documentation.
+
+To install the data provider as system private app, consider remounting the
+system partition:
+
+```
+adb root && adb remount
+```
+
+Push it in:
+
+```
+adb push ${ANDROID_PRODUCT_OUT}/system/app/NearbyFastPairSeekerDataProvider
+/system/priv-app/
+```
+
+Then reboot:
+
+```
+adb reboot
+```
+
+## Manual testing the seeker side with provider simulator app
+
+The `NearbyFastPairProviderSimulatorApp.apk` is a simple Android app to let you
+control the state of the Fast Pair provider simulator. Install this app on phone
+2 (Fast Pair provider role) to work correctly.
+
+See
+[clients/test_support/fastpair_provider/simulator_ap/Android.bp](clients/test_support/fastpair_provider/simulator_ap/Android.bp)
+for more documentation.
diff --git a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
index e250254..8a18cca 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
@@ -87,8 +87,19 @@
     }
 
     @Test
+    public void test_unregister_noPrivilegedPermission_throwsException() {
+        mUiAutomation.dropShellPermissionIdentity();
+        assertThrows(java.lang.SecurityException.class,
+                () -> mService.unregisterScanListener(mScanListener, PACKAGE_NAME,
+                        /* attributionTag= */ null));
+    }
+
+    @Test
     public void test_unregister() {
-        mService.unregisterScanListener(mScanListener);
+        setMockInjector(/* isMockOpsAllowed= */ true);
+        mService.registerScanListener(mScanRequest, mScanListener, PACKAGE_NAME,
+                /* attributionTag= */ null);
+        mService.unregisterScanListener(mScanListener,  PACKAGE_NAME, /* attributionTag= */ null);
     }
 
     private ScanRequest createScanRequest() {
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
deleted file mode 100644
index 3b34655..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
+++ /dev/null
@@ -1,35 +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 com.android.server.nearby.presence;
-
-import androidx.test.filters.SdkSuppress;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.MockitoAnnotations;
-
-public class PresenceManagerTest {
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testInit() {}
-}
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 217a9a6..b2d8b5e 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -159,8 +159,11 @@
 import java.time.ZoneOffset;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Executor;
@@ -374,9 +377,19 @@
 
     private long mLastStatsSessionPoll;
 
-    /** Map from UID to number of opened sessions */
-    @GuardedBy("mOpenSessionCallsPerUid")
+    private final Object mOpenSessionCallsLock = new Object();
+    /**
+     * Map from UID to number of opened sessions. This is used for rate-limt an app to open
+     * session frequently
+     */
+    @GuardedBy("mOpenSessionCallsLock")
     private final SparseIntArray mOpenSessionCallsPerUid = new SparseIntArray();
+    /**
+     * Map from key {@code OpenSessionKey} to count of opened sessions. This is for recording
+     * the caller of open session and it is only for debugging.
+     */
+    @GuardedBy("mOpenSessionCallsLock")
+    private final HashMap<OpenSessionKey, Integer> mOpenSessionCallsPerCaller = new HashMap<>();
 
     private final static int DUMP_STATS_SESSION_COUNT = 20;
 
@@ -407,6 +420,44 @@
                 Clock.systemUTC());
     }
 
+    /**
+     * This class is a key that used in {@code mOpenSessionCallsPerCaller} to identify the count of
+     * the caller.
+     */
+    private static class OpenSessionKey {
+        public final int uid;
+        public final String packageName;
+
+        OpenSessionKey(int uid, @NonNull String packageName) {
+            this.uid = uid;
+            this.packageName = packageName;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder();
+            sb.append("{");
+            sb.append("uid=").append(uid).append(",");
+            sb.append("package=").append(packageName);
+            sb.append("}");
+            return sb.toString();
+        }
+
+        @Override
+        public boolean equals(@NonNull Object o) {
+            if (this == o) return true;
+            if (o.getClass() != getClass()) return false;
+
+            final OpenSessionKey key = (OpenSessionKey) o;
+            return this.uid == key.uid && TextUtils.equals(this.packageName, key.packageName);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(uid, packageName);
+        }
+    }
+
     private final class NetworkStatsHandler extends Handler {
         NetworkStatsHandler(@NonNull Looper looper) {
             super(looper);
@@ -794,16 +845,27 @@
         return openSessionInternal(flags, callingPackage);
     }
 
-    private boolean isRateLimitedForPoll(int callingUid) {
-        if (callingUid == android.os.Process.SYSTEM_UID) {
-            return false;
-        }
-
+    private boolean isRateLimitedForPoll(@NonNull OpenSessionKey key) {
         final long lastCallTime;
         final long now = SystemClock.elapsedRealtime();
-        synchronized (mOpenSessionCallsPerUid) {
-            int calls = mOpenSessionCallsPerUid.get(callingUid, 0);
-            mOpenSessionCallsPerUid.put(callingUid, calls + 1);
+
+        synchronized (mOpenSessionCallsLock) {
+            Integer callsPerCaller = mOpenSessionCallsPerCaller.get(key);
+            if (callsPerCaller == null) {
+                mOpenSessionCallsPerCaller.put((key), 1);
+            } else {
+                mOpenSessionCallsPerCaller.put(key, Integer.sum(callsPerCaller, 1));
+            }
+
+            int callsPerUid = mOpenSessionCallsPerUid.get(key.uid, 0);
+            mOpenSessionCallsPerUid.put(key.uid, callsPerUid + 1);
+
+            if (key.uid == android.os.Process.SYSTEM_UID) {
+                return false;
+            }
+
+            // To avoid a non-system user to be rate-limited after system users open sessions,
+            // so update mLastStatsSessionPoll after checked if the uid is SYSTEM_UID.
             lastCallTime = mLastStatsSessionPoll;
             mLastStatsSessionPoll = now;
         }
@@ -811,7 +873,7 @@
         return now - lastCallTime < POLL_RATE_LIMIT_MS;
     }
 
-    private int restrictFlagsForCaller(int flags) {
+    private int restrictFlagsForCaller(int flags, @NonNull String callingPackage) {
         // All non-privileged callers are not allowed to turn off POLL_ON_OPEN.
         final boolean isPrivileged = PermissionUtils.checkAnyPermissionOf(mContext,
                 NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
@@ -821,14 +883,15 @@
         }
         // Non-system uids are rate limited for POLL_ON_OPEN.
         final int callingUid = Binder.getCallingUid();
-        flags = isRateLimitedForPoll(callingUid)
+        final OpenSessionKey key = new OpenSessionKey(callingUid, callingPackage);
+        flags = isRateLimitedForPoll(key)
                 ? flags & (~NetworkStatsManager.FLAG_POLL_ON_OPEN)
                 : flags;
         return flags;
     }
 
     private INetworkStatsSession openSessionInternal(final int flags, final String callingPackage) {
-        final int restrictedFlags = restrictFlagsForCaller(flags);
+        final int restrictedFlags = restrictFlagsForCaller(flags, callingPackage);
         if ((restrictedFlags & (NetworkStatsManager.FLAG_POLL_ON_OPEN
                 | NetworkStatsManager.FLAG_POLL_FORCE)) != 0) {
             final long ident = Binder.clearCallingIdentity();
@@ -1938,6 +2001,9 @@
         for (int uid : uids) {
             deleteKernelTagData(uid);
         }
+
+       // TODO: Remove the UID's entries from mOpenSessionCallsPerUid and
+       // mOpenSessionCallsPerCaller
     }
 
     /**
@@ -2061,25 +2127,21 @@
             pw.decreaseIndent();
 
             // Get the top openSession callers
-            final SparseIntArray calls;
-            synchronized (mOpenSessionCallsPerUid) {
-                calls = mOpenSessionCallsPerUid.clone();
+            final HashMap calls;
+            synchronized (mOpenSessionCallsLock) {
+                calls = new HashMap<>(mOpenSessionCallsPerCaller);
             }
-
-            final int N = calls.size();
-            final long[] values = new long[N];
-            for (int j = 0; j < N; j++) {
-                values[j] = ((long) calls.valueAt(j) << 32) | calls.keyAt(j);
-            }
-            Arrays.sort(values);
-
-            pw.println("Top openSession callers (uid=count):");
+            final List<Map.Entry<OpenSessionKey, Integer>> list = new ArrayList<>(calls.entrySet());
+            Collections.sort(list,
+                    (left, right) -> Integer.compare(left.getValue(), right.getValue()));
+            final int num = list.size();
+            final int end = Math.max(0, num - DUMP_STATS_SESSION_COUNT);
+            pw.println("Top openSession callers:");
             pw.increaseIndent();
-            final int end = Math.max(0, N - DUMP_STATS_SESSION_COUNT);
-            for (int j = N - 1; j >= end; j--) {
-                final int uid = (int) (values[j] & 0xffffffff);
-                final int count = (int) (values[j] >> 32);
-                pw.print(uid); pw.print("="); pw.println(count);
+            for (int j = num - 1; j >= end; j--) {
+                final Map.Entry<OpenSessionKey, Integer> entry = list.get(j);
+                pw.print(entry.getKey()); pw.print("="); pw.println(entry.getValue());
+
             }
             pw.decreaseIndent();
             pw.println();
diff --git a/service/ServiceConnectivityResources/Android.bp b/service/ServiceConnectivityResources/Android.bp
index f491cc7..02b2875 100644
--- a/service/ServiceConnectivityResources/Android.bp
+++ b/service/ServiceConnectivityResources/Android.bp
@@ -23,6 +23,7 @@
     name: "ServiceConnectivityResources",
     sdk_version: "module_30",
     min_sdk_version: "30",
+    target_sdk_version: "33",
     resource_dirs: [
         "res",
     ],
diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java
index 345a78d..b66a979 100644
--- a/tests/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/common/java/android/net/LinkPropertiesTest.java
@@ -1262,13 +1262,14 @@
     }
 
     @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
     public void testHasExcludeRoute() {
         LinkProperties lp = new LinkProperties();
-        lp.setInterfaceName("VPN");
-        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV4, 2), RTN_UNICAST));
+        lp.setInterfaceName("tun0");
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV4, 24), RTN_UNICAST));
         lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 0), RTN_UNICAST));
         assertFalse(lp.hasExcludeRoute());
-        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 2), RTN_THROW));
+        lp.addRoute(new RouteInfo(new IpPrefix(ADDRV6, 32), RTN_THROW));
         assertTrue(lp.hasExcludeRoute());
     }
 
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index b684068..c47ccbf 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -26,6 +26,7 @@
         "tradefed",
     ],
     static_libs: [
+        "CompatChangeGatingTestBase",
         "modules-utils-build-testing",
     ],
     // Tag this module as a cts test artifact
diff --git a/tests/cts/hostside/app3/Android.bp b/tests/cts/hostside/app3/Android.bp
new file mode 100644
index 0000000..69667ce
--- /dev/null
+++ b/tests/cts/hostside/app3/Android.bp
@@ -0,0 +1,50 @@
+//
+// 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.
+//
+
+java_defaults {
+    name: "CtsHostsideNetworkTestsApp3Defaults",
+    srcs: ["src/**/*.java"],
+    libs: [
+        "junit",
+    ],
+    static_libs: [
+        "ctstestrunner-axt",
+        "truth-prebuilt",
+    ],
+
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsHostsideNetworkTestsApp3",
+    defaults: [
+        "cts_support_defaults",
+        "CtsHostsideNetworkTestsApp3Defaults",
+    ],
+}
+
+android_test_helper_app {
+    name: "CtsHostsideNetworkTestsApp3PreT",
+    target_sdk_version: "31",
+    defaults: [
+        "cts_support_defaults",
+        "CtsHostsideNetworkTestsApp3Defaults",
+    ],
+}
diff --git a/tests/cts/hostside/app3/AndroidManifest.xml b/tests/cts/hostside/app3/AndroidManifest.xml
new file mode 100644
index 0000000..eabcacb
--- /dev/null
+++ b/tests/cts/hostside/app3/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.cts.net.hostside.app3">
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.cts.net.hostside.app3" />
+
+</manifest>
diff --git a/tests/cts/hostside/app3/src/com/android/cts/net/hostside/app3/ExcludedRoutesGatingTest.java b/tests/cts/hostside/app3/src/com/android/cts/net/hostside/app3/ExcludedRoutesGatingTest.java
new file mode 100644
index 0000000..a1a8209
--- /dev/null
+++ b/tests/cts/hostside/app3/src/com/android/cts/net/hostside/app3/ExcludedRoutesGatingTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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 com.android.cts.net.hostside.app3;
+
+import static org.junit.Assert.assertEquals;
+
+import android.Manifest;
+import android.net.IpPrefix;
+import android.net.LinkProperties;
+import android.net.RouteInfo;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests to verify {@link LinkProperties#getRoutes} behavior, depending on
+ * {@LinkProperties#EXCLUDED_ROUTES} change state.
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExcludedRoutesGatingTest {
+    @Before
+    public void setUp() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+                        Manifest.permission.READ_COMPAT_CHANGE_CONFIG);
+    }
+
+    @After
+    public void tearDown() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .dropShellPermissionIdentity();
+    }
+
+    @Test
+    public void testExcludedRoutesChangeEnabled() {
+        final LinkProperties lp = makeLinkPropertiesWithExcludedRoutes();
+
+        // Excluded routes change is enabled: non-RTN_UNICAST routes are visible.
+        assertEquals(2, lp.getRoutes().size());
+        assertEquals(2, lp.getAllRoutes().size());
+    }
+
+    @Test
+    public void testExcludedRoutesChangeDisabled() {
+        final LinkProperties lp = makeLinkPropertiesWithExcludedRoutes();
+
+        // Excluded routes change is disabled: non-RTN_UNICAST routes are filtered out.
+        assertEquals(0, lp.getRoutes().size());
+        assertEquals(0, lp.getAllRoutes().size());
+    }
+
+    private LinkProperties makeLinkPropertiesWithExcludedRoutes() {
+        final LinkProperties lp = new LinkProperties();
+
+        lp.addRoute(new RouteInfo(new IpPrefix("10.0.0.0/8"), null, null, RouteInfo.RTN_THROW));
+        lp.addRoute(new RouteInfo(new IpPrefix("2001:db8::/64"), null, null,
+                RouteInfo.RTN_UNREACHABLE));
+
+        return lp;
+    }
+}
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java
new file mode 100644
index 0000000..b65fb6b
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java
@@ -0,0 +1,71 @@
+/*
+ * 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 com.android.cts.net;
+
+import android.compat.cts.CompatChangeGatingTestCase;
+
+import java.util.Set;
+
+/**
+ * Tests for the {@link android.net.LinkProperties#EXCLUDED_ROUTES} compatibility change.
+ */
+public class HostsideLinkPropertiesGatingTests extends CompatChangeGatingTestCase {
+    private static final String TEST_APK = "CtsHostsideNetworkTestsApp3.apk";
+    private static final String TEST_APK_PRE_T = "CtsHostsideNetworkTestsApp3PreT.apk";
+    private static final String TEST_PKG = "com.android.cts.net.hostside.app3";
+    private static final String TEST_CLASS = ".ExcludedRoutesGatingTest";
+
+    private static final long EXCLUDED_ROUTES_CHANGE_ID = 186082280;
+
+    protected void tearDown() throws Exception {
+        uninstallPackage(TEST_PKG, true);
+    }
+
+    public void testExcludedRoutesChangeEnabled() throws Exception {
+        installPackage(TEST_APK, true);
+        runDeviceCompatTest("testExcludedRoutesChangeEnabled");
+    }
+
+    public void testExcludedRoutesChangeDisabledPreT() throws Exception {
+        installPackage(TEST_APK_PRE_T, true);
+        runDeviceCompatTest("testExcludedRoutesChangeDisabled");
+    }
+
+    public void testExcludedRoutesChangeDisabledByOverride() throws Exception {
+        installPackage(TEST_APK, true);
+        runDeviceCompatTestWithChangeDisabled("testExcludedRoutesChangeDisabled");
+    }
+
+    public void testExcludedRoutesChangeEnabledByOverridePreT() throws Exception {
+        installPackage(TEST_APK_PRE_T, true);
+        runDeviceCompatTestWithChangeEnabled("testExcludedRoutesChangeEnabled");
+    }
+
+    private void runDeviceCompatTest(String methodName) throws Exception {
+        runDeviceCompatTest(TEST_PKG, TEST_CLASS, methodName, Set.of(), Set.of());
+    }
+
+    private void runDeviceCompatTestWithChangeEnabled(String methodName) throws Exception {
+        runDeviceCompatTest(TEST_PKG, TEST_CLASS, methodName, Set.of(EXCLUDED_ROUTES_CHANGE_ID),
+                Set.of());
+    }
+
+    private void runDeviceCompatTestWithChangeDisabled(String methodName) throws Exception {
+        runDeviceCompatTest(TEST_PKG, TEST_CLASS, methodName, Set.of(),
+                Set.of(EXCLUDED_ROUTES_CHANGE_ID));
+    }
+}
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 48a1c79..33f3af5 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -38,4 +38,20 @@
         <option name="hidden-api-checks" value="false" />
         <option name="isolated-storage" value="false" />
     </test>
+    <!-- When this test is run in a Mainline context (e.g. with `mts-tradefed`), only enable it if
+        one of the Mainline modules below is present on the device used for testing. -->
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <!-- Tethering Module (internal version). -->
+        <option name="mainline-module-package-name" value="com.google.android.tethering" />
+        <!-- Tethering Module (AOSP version). -->
+        <option name="mainline-module-package-name" value="com.android.tethering" />
+        <!-- NetworkStack Module (internal version). Should always be installed with CaptivePortalLogin. -->
+        <option name="mainline-module-package-name" value="com.google.android.networkstack" />
+        <!-- NetworkStack Module (AOSP version). Should always be installed with CaptivePortalLogin. -->
+        <option name="mainline-module-package-name" value="com.android.networkstack" />
+        <!-- Resolver Module (internal version). -->
+        <option name="mainline-module-package-name" value="com.google.android.resolv" />
+        <!-- Resolver Module (AOSP version). -->
+        <option name="mainline-module-package-name" value="com.android.resolv" />
+    </object>
 </configuration>
diff --git a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
index de4f41b..d618915 100644
--- a/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkStatsManagerTest.java
@@ -35,7 +35,14 @@
 
 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
 import android.app.AppOpsManager;
+import android.app.Instrumentation;
 import android.app.usage.NetworkStats;
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
@@ -56,15 +63,24 @@
 import android.os.SystemClock;
 import android.platform.test.annotations.AppModeFull;
 import android.telephony.TelephonyManager;
-import android.test.InstrumentationTestCase;
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
 import com.android.compatibility.common.util.ShellIdentityUtils;
 import com.android.compatibility.common.util.SystemUtil;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.testutils.ConnectivityModuleTest;
 import com.android.testutils.DevSdkIgnoreRule;
 
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -78,7 +94,13 @@
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
-public class NetworkStatsManagerTest extends InstrumentationTestCase {
+@ConnectivityModuleTest
+@AppModeFull(reason = "instant apps cannot be granted USAGE_STATS")
+@RunWith(AndroidJUnit4.class)
+public class NetworkStatsManagerTest {
+    @Rule
+    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(SC_V2 /* ignoreClassUpTo */);
+
     private static final String LOG_TAG = "NetworkStatsManagerTest";
     private static final String APPOPS_SET_SHELL_COMMAND = "appops set {0} {1} {2}";
     private static final String APPOPS_GET_SHELL_COMMAND = "appops get {0} {1}";
@@ -179,9 +201,11 @@
             };
 
     private String mPkg;
+    private Context mContext;
     private NetworkStatsManager mNsm;
     private ConnectivityManager mCm;
     private PackageManager mPm;
+    private Instrumentation mInstrumentation;
     private long mStartTime;
     private long mEndTime;
 
@@ -239,44 +263,40 @@
         }
     }
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        mNsm = (NetworkStatsManager) getInstrumentation().getContext()
-                .getSystemService(Context.NETWORK_STATS_SERVICE);
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getContext();
+        mNsm = mContext.getSystemService(NetworkStatsManager.class);
         mNsm.setPollForce(true);
 
-        mCm = (ConnectivityManager) getInstrumentation().getContext()
-                .getSystemService(Context.CONNECTIVITY_SERVICE);
+        mCm = mContext.getSystemService(ConnectivityManager.class);
+        mPm = mContext.getPackageManager();
+        mPkg = mContext.getPackageName();
 
-        mPm = getInstrumentation().getContext().getPackageManager();
-
-        mPkg = getInstrumentation().getContext().getPackageName();
-
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
         mWriteSettingsMode = getAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS);
         setAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS, "allow");
         mUsageStatsMode = getAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS);
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         if (mWriteSettingsMode != null) {
             setAppOpsMode(AppOpsManager.OPSTR_WRITE_SETTINGS, mWriteSettingsMode);
         }
         if (mUsageStatsMode != null) {
             setAppOpsMode(AppOpsManager.OPSTR_GET_USAGE_STATS, mUsageStatsMode);
         }
-        super.tearDown();
     }
 
     private void setAppOpsMode(String appop, String mode) throws Exception {
         final String command = MessageFormat.format(APPOPS_SET_SHELL_COMMAND, mPkg, appop, mode);
-        SystemUtil.runShellCommand(command);
+        SystemUtil.runShellCommand(mInstrumentation, command);
     }
 
     private String getAppOpsMode(String appop) throws Exception {
         final String command = MessageFormat.format(APPOPS_GET_SHELL_COMMAND, mPkg, appop);
-        String result = SystemUtil.runShellCommand(command);
+        String result = SystemUtil.runShellCommand(mInstrumentation, command);
         if (result == null) {
             Log.w(LOG_TAG, "App op " + appop + " could not be read.");
         }
@@ -284,7 +304,7 @@
     }
 
     private boolean isInForeground() throws IOException {
-        String result = SystemUtil.runShellCommand(getInstrumentation(),
+        String result = SystemUtil.runShellCommand(mInstrumentation,
                 "cmd activity get-uid-state " + Process.myUid());
         return result.contains("FOREGROUND");
     }
@@ -381,15 +401,14 @@
     private String getSubscriberId(int networkIndex) {
         int networkType = mNetworkInterfacesToTest[networkIndex].getNetworkType();
         if (ConnectivityManager.TYPE_MOBILE == networkType) {
-            TelephonyManager tm = (TelephonyManager) getInstrumentation().getContext()
-                    .getSystemService(Context.TELEPHONY_SERVICE);
+            TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
             return ShellIdentityUtils.invokeMethodWithShellPermissions(tm,
                     (telephonyManager) -> telephonyManager.getSubscriberId());
         }
         return "";
     }
 
-    @AppModeFull
+    @Test
     public void testDeviceSummary() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             if (!shouldTestThisNetworkType(i, MINUTE / 2)) {
@@ -425,7 +444,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testUserSummary() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             if (!shouldTestThisNetworkType(i, MINUTE / 2)) {
@@ -461,7 +480,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testAppSummary() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Use tolerance value that large enough to make sure stats of at
@@ -537,7 +556,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testAppDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -580,7 +599,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testUidDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -634,7 +653,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testTagDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -741,7 +760,7 @@
                 bucket.getRxBytes(), bucket.getTxBytes()));
     }
 
-    @AppModeFull
+    @Test
     public void testUidTagStateDetails() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -818,7 +837,7 @@
         }
     }
 
-    @AppModeFull
+    @Test
     public void testCallback() throws Exception {
         for (int i = 0; i < mNetworkInterfacesToTest.length; ++i) {
             // Relatively large tolerance to accommodate for history bucket size.
@@ -851,9 +870,10 @@
         }
     }
 
-    @AppModeFull
-    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+    @Test
     public void testDataMigrationUtils() throws Exception {
+        if (!SdkLevel.isAtLeastT()) return;
+
         final List<String> prefixes = List.of(PREFIX_UID, PREFIX_XT, PREFIX_UID_TAG);
         for (final String prefix : prefixes) {
             final long duration = TextUtils.equals(PREFIX_XT, prefix) ? TimeUnit.HOURS.toMillis(1)