Merge "Define the IpSecTransformState API flag in Connectivity module" into main
diff --git a/framework-t/Android.bp b/framework-t/Android.bp
index f485a44..9203a3e 100644
--- a/framework-t/Android.bp
+++ b/framework-t/Android.bp
@@ -118,6 +118,7 @@
         "framework-bluetooth",
         "framework-wifi",
         "framework-connectivity-pre-jarjar",
+        "framework-location.stubs.module_lib",
     ],
     visibility: ["//packages/modules/Connectivity:__subpackages__"],
 }
@@ -140,6 +141,7 @@
         "sdk_module-lib_current_framework-connectivity",
     ],
     libs: [
+        "framework-location.stubs.module_lib",
         "sdk_module-lib_current_framework-connectivity",
     ],
     permitted_packages: [
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 8251f85..1f1953c 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -59,11 +59,17 @@
   }
 
   public class NearbyManager {
+    method @FlaggedApi("com.android.nearby.flags.powered_off_finding") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public int getPoweredOffFindingMode();
     method public void queryOffloadCapability(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.nearby.OffloadCapability>);
+    method @FlaggedApi("com.android.nearby.flags.powered_off_finding") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public void setPoweredOffFindingEphemeralIds(@NonNull java.util.List<byte[]>);
+    method @FlaggedApi("com.android.nearby.flags.powered_off_finding") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public void setPoweredOffFindingMode(int);
     method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void startBroadcast(@NonNull android.nearby.BroadcastRequest, @NonNull java.util.concurrent.Executor, @NonNull android.nearby.BroadcastCallback);
     method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int startScan(@NonNull android.nearby.ScanRequest, @NonNull java.util.concurrent.Executor, @NonNull android.nearby.ScanCallback);
     method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void stopBroadcast(@NonNull android.nearby.BroadcastCallback);
     method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void stopScan(@NonNull android.nearby.ScanCallback);
+    field @FlaggedApi("com.android.nearby.flags.powered_off_finding") public static final int POWERED_OFF_FINDING_MODE_DISABLED = 1; // 0x1
+    field @FlaggedApi("com.android.nearby.flags.powered_off_finding") public static final int POWERED_OFF_FINDING_MODE_ENABLED = 2; // 0x2
+    field @FlaggedApi("com.android.nearby.flags.powered_off_finding") public static final int POWERED_OFF_FINDING_MODE_UNSUPPORTED = 0; // 0x0
   }
 
   public final class OffloadCapability implements android.os.Parcelable {
diff --git a/nearby/framework/Android.bp b/nearby/framework/Android.bp
index 0fd9a89..4be102c 100644
--- a/nearby/framework/Android.bp
+++ b/nearby/framework/Android.bp
@@ -50,6 +50,7 @@
         "androidx.annotation_annotation",
         "framework-annotations-lib",
         "framework-bluetooth",
+        "framework-location.stubs.module_lib",
     ],
     static_libs: [
         "modules-utils-preconditions",
diff --git a/nearby/framework/java/android/nearby/INearbyManager.aidl b/nearby/framework/java/android/nearby/INearbyManager.aidl
index 7af271e..21ae0ac 100644
--- a/nearby/framework/java/android/nearby/INearbyManager.aidl
+++ b/nearby/framework/java/android/nearby/INearbyManager.aidl
@@ -20,6 +20,7 @@
 import android.nearby.IScanListener;
 import android.nearby.BroadcastRequestParcelable;
 import android.nearby.ScanRequest;
+import android.nearby.PoweredOffFindingEphemeralId;
 import android.nearby.aidl.IOffloadCallback;
 
 /**
@@ -40,4 +41,10 @@
     void stopBroadcast(in IBroadcastListener callback, String packageName, @nullable String attributionTag);
 
     void queryOffloadCapability(in IOffloadCallback callback) ;
-}
\ No newline at end of file
+
+    void setPoweredOffFindingEphemeralIds(in List<PoweredOffFindingEphemeralId> eids);
+
+    void setPoweredOffModeEnabled(boolean enabled);
+
+    boolean getPoweredOffModeEnabled();
+}
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index 00f1c38..cae653d 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -18,6 +18,7 @@
 
 import android.Manifest;
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -25,9 +26,12 @@
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
+import android.bluetooth.BluetoothManager;
 import android.content.Context;
+import android.location.LocationManager;
 import android.nearby.aidl.IOffloadCallback;
 import android.os.RemoteException;
+import android.os.SystemProperties;
 import android.provider.Settings;
 import android.util.Log;
 
@@ -37,6 +41,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
+import java.util.List;
 import java.util.Objects;
 import java.util.WeakHashMap;
 import java.util.concurrent.Executor;
@@ -75,8 +80,51 @@
         int ERROR = 2;
     }
 
+    /**
+     * Return value of {@link #getPoweredOffFindingMode()} when this powered off finding is not
+     * supported the device.
+     */
+    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    public static final int POWERED_OFF_FINDING_MODE_UNSUPPORTED = 0;
+
+    /**
+     * Return value of {@link #getPoweredOffFindingMode()} and argument of {@link
+     * #setPoweredOffFindingMode(int)} when powered off finding is supported but disabled. The
+     * device will not start to advertise when powered off.
+     */
+    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    public static final int POWERED_OFF_FINDING_MODE_DISABLED = 1;
+
+    /**
+     * Return value of {@link #getPoweredOffFindingMode()} and argument of {@link
+     * #setPoweredOffFindingMode(int)} when powered off finding is enabled. The device will start to
+     * advertise when powered off.
+     */
+    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    public static final int POWERED_OFF_FINDING_MODE_ENABLED = 2;
+
+    /**
+     * Powered off finding modes.
+     *
+     * @hide
+     */
+    @IntDef(
+            prefix = {"POWERED_OFF_FINDING_MODE"},
+            value = {
+                    POWERED_OFF_FINDING_MODE_UNSUPPORTED,
+                    POWERED_OFF_FINDING_MODE_DISABLED,
+                    POWERED_OFF_FINDING_MODE_ENABLED,
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PoweredOffFindingMode {}
+
     private static final String TAG = "NearbyManager";
 
+    private static final int POWERED_OFF_FINDING_EID_LENGTH = 20;
+
+    private static final String POWER_OFF_FINDING_SUPPORTED_PROPERTY =
+            "ro.bluetooth.finder.supported";
+
     /**
      * TODO(b/286137024): Remove this when CTS R5 is rolled out.
      * Whether allows Fast Pair to scan.
@@ -456,4 +504,124 @@
                 "successfully %s Fast Pair scan", enable ? "enables" : "disables"));
     }
 
+    /**
+     * Sets the precomputed EIDs for advertising when the phone is powered off. The Bluetooth
+     * controller will store these EIDs in its memory, and will start advertising them in Find My
+     * Device network EID frames when powered off, only if the powered off finding mode was
+     * previously enabled by calling {@link #setPoweredOffFindingMode(int)}.
+     *
+     * <p>The EIDs are cryptographic ephemeral identifiers that change periodically, based on the
+     * Android clock at the time of the shutdown. They are used as the public part of asymmetric key
+     * pairs. Members of the Find My Device network can use them to encrypt the location of where
+     * they sight the advertising device. Only someone in possession of the private key (the device
+     * owner or someone that the device owner shared the key with) can decrypt this encrypted
+     * location.
+     *
+     * <p>Android will typically call this method during the shutdown process. Even after the
+     * method was called, it is still possible to call {#link setPoweredOffFindingMode() to disable
+     * the advertisement, for example to temporarily disable it for a single shutdown.
+     *
+     * <p>If called more than once, the EIDs of the most recent call overrides the EIDs from any
+     * previous call.
+     *
+     * @throws IllegalArgumentException if the length of one of the EIDs is not 20 bytes
+     */
+    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+    public void setPoweredOffFindingEphemeralIds(@NonNull List<byte[]> eids) {
+        Objects.requireNonNull(eids);
+        if (!isPoweredOffFindingSupported()) {
+            throw new UnsupportedOperationException(
+                    "Powered off finding is not supported on this device");
+        }
+        List<PoweredOffFindingEphemeralId> ephemeralIdList = eids.stream().map(
+                eid -> {
+                    Preconditions.checkArgument(eid.length == POWERED_OFF_FINDING_EID_LENGTH);
+                    PoweredOffFindingEphemeralId ephemeralId = new PoweredOffFindingEphemeralId();
+                    ephemeralId.bytes = eid;
+                    return ephemeralId;
+                }).toList();
+        try {
+            mService.setPoweredOffFindingEphemeralIds(ephemeralIdList);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+    }
+
+    /**
+     * Turns the powered off finding on or off. Power off finding will operate only if this method
+     * was called at least once since boot, and the value of the argument {@code
+     * poweredOffFindinMode} was {@link #POWERED_OFF_FINDING_MODE_ENABLED} the last time the method
+     * was called.
+     *
+     * <p>When an Android device with the powered off finding feature is turned off (either as part
+     * of a normal shutdown or due to dead battery), its Bluetooth chip starts to advertise Find My
+     * Device network EID frames with the EID payload that were provided by the last call to {@link
+     * #setPoweredOffFindingEphemeralIds(List)}. These EIDs can be sighted by other Android devices
+     * in BLE range that are part of the Find My Device network. The Android sighters use the EID to
+     * encrypt the location of the Android device and upload it to the server, in a way that only
+     * the owner of the advertising device, or people that the owner shared their encryption key
+     * with, can decrypt the location.
+     *
+     * @param poweredOffFindingMode {@link #POWERED_OFF_FINDING_MODE_ENABLED} or {@link
+     * #POWERED_OFF_FINDING_MODE_DISABLED}
+     *
+     * @throws IllegalStateException if called with {@link #POWERED_OFF_FINDING_MODE_ENABLED} when
+     * Bluetooth or location services are disabled
+     */
+    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+    public void setPoweredOffFindingMode(@PoweredOffFindingMode int poweredOffFindingMode) {
+        Preconditions.checkArgument(
+                poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED
+                        || poweredOffFindingMode == POWERED_OFF_FINDING_MODE_DISABLED,
+                "invalid poweredOffFindingMode");
+        if (!isPoweredOffFindingSupported()) {
+            throw new UnsupportedOperationException(
+                    "Powered off finding is not supported on this device");
+        }
+        if (poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED) {
+            Preconditions.checkState(areLocationAndBluetoothEnabled(),
+                    "Location services and Bluetooth must be on");
+        }
+        try {
+            mService.setPoweredOffModeEnabled(
+                    poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the state of the powered off finding feature.
+     *
+     * <p>{@link #POWERED_OFF_FINDING_MODE_UNSUPPORTED} if the feature is not supported by the
+     * device, {@link #POWERED_OFF_FINDING_MODE_DISABLED} if this was the last value set by {@link
+     * #setPoweredOffFindingMode(int)} or if no value was set since boot, {@link
+     * #POWERED_OFF_FINDING_MODE_ENABLED} if this was the last value set by {@link
+     * #setPoweredOffFindingMode(int)}
+     */
+    @FlaggedApi("com.android.nearby.flags.powered_off_finding")
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+    public @PoweredOffFindingMode int getPoweredOffFindingMode() {
+        if (!isPoweredOffFindingSupported()) {
+            return POWERED_OFF_FINDING_MODE_UNSUPPORTED;
+        }
+        try {
+            return mService.getPoweredOffModeEnabled()
+                    ? POWERED_OFF_FINDING_MODE_ENABLED : POWERED_OFF_FINDING_MODE_DISABLED;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private boolean isPoweredOffFindingSupported() {
+        return Boolean.parseBoolean(SystemProperties.get(POWER_OFF_FINDING_SUPPORTED_PROPERTY));
+    }
+
+    private boolean areLocationAndBluetoothEnabled() {
+        return mContext.getSystemService(BluetoothManager.class).getAdapter().isEnabled()
+                && mContext.getSystemService(LocationManager.class).isLocationEnabled();
+    }
 }
diff --git a/nearby/framework/java/android/nearby/PoweredOffFindingEphemeralId.aidl b/nearby/framework/java/android/nearby/PoweredOffFindingEphemeralId.aidl
new file mode 100644
index 0000000..9f4bfef
--- /dev/null
+++ b/nearby/framework/java/android/nearby/PoweredOffFindingEphemeralId.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024, 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;
+
+/**
+ * Find My Device network ephemeral ID for powered off finding.
+ *
+ * @hide
+ */
+parcelable PoweredOffFindingEphemeralId {
+    byte[20] bytes;
+}
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index 3c183ec..1575f07 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -35,12 +35,14 @@
 import android.nearby.INearbyManager;
 import android.nearby.IScanListener;
 import android.nearby.NearbyManager;
+import android.nearby.PoweredOffFindingEphemeralId;
 import android.nearby.ScanRequest;
 import android.nearby.aidl.IOffloadCallback;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.managers.BluetoothFinderManager;
 import com.android.server.nearby.managers.BroadcastProviderManager;
 import com.android.server.nearby.managers.DiscoveryManager;
 import com.android.server.nearby.managers.DiscoveryProviderManager;
@@ -50,6 +52,8 @@
 import com.android.server.nearby.util.permissions.BroadcastPermissions;
 import com.android.server.nearby.util.permissions.DiscoveryPermissions;
 
+import java.util.List;
+
 /** Service implementing nearby functionality. */
 public class NearbyService extends INearbyManager.Stub {
     public static final String TAG = "NearbyService";
@@ -79,6 +83,7 @@
             };
     private final DiscoveryManager mDiscoveryProviderManager;
     private final BroadcastProviderManager mBroadcastProviderManager;
+    private final BluetoothFinderManager mBluetoothFinderManager;
 
     public NearbyService(Context context) {
         mContext = context;
@@ -90,6 +95,7 @@
                 mNearbyConfiguration.refactorDiscoveryManager()
                         ? new DiscoveryProviderManager(context, mInjector)
                         : new DiscoveryProviderManagerLegacy(context, mInjector);
+        mBluetoothFinderManager = new BluetoothFinderManager();
     }
 
     @VisibleForTesting
@@ -148,6 +154,30 @@
         mDiscoveryProviderManager.queryOffloadCapability(callback);
     }
 
+    @Override
+    public void setPoweredOffFindingEphemeralIds(List<PoweredOffFindingEphemeralId> eids) {
+        // Permissions check
+        enforceBluetoothPrivilegedPermission(mContext);
+
+        mBluetoothFinderManager.sendEids(eids);
+    }
+
+    @Override
+    public void setPoweredOffModeEnabled(boolean enabled) {
+        // Permissions check
+        enforceBluetoothPrivilegedPermission(mContext);
+
+        mBluetoothFinderManager.setPoweredOffFinderMode(enabled);
+    }
+
+    @Override
+    public boolean getPoweredOffModeEnabled() {
+        // Permissions check
+        enforceBluetoothPrivilegedPermission(mContext);
+
+        return mBluetoothFinderManager.getPoweredOffFinderMode();
+    }
+
     /**
      * Called by the service initializer.
      *
diff --git a/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java b/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java
new file mode 100644
index 0000000..63ff516
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 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.managers;
+
+import android.nearby.PoweredOffFindingEphemeralId;
+
+import java.util.List;
+
+/** Connects to {@link IBluetoothFinder} HAL and invokes its API. */
+// A placeholder implementation until the HAL API can be used.
+public class BluetoothFinderManager {
+
+    private boolean mPoweredOffFindingModeEnabled = false;
+
+    /** An empty implementation of the corresponding HAL API call. */
+    public void sendEids(List<PoweredOffFindingEphemeralId> eids) {}
+
+    /** A placeholder implementation of the corresponding HAL API call. */
+    public void setPoweredOffFinderMode(boolean enable) {
+        mPoweredOffFindingModeEnabled = enable;
+    }
+
+    /** A placeholder implementation of the corresponding HAL API call. */
+    public boolean getPoweredOffFinderMode() {
+        return mPoweredOffFindingModeEnabled;
+    }
+}
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index aa2806d..8009303 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -34,6 +34,7 @@
         "framework-bluetooth.stubs.module_lib",
         "framework-configinfrastructure",
         "framework-connectivity-t.impl",
+        "framework-location.stubs.module_lib",
     ],
     srcs: ["src/**/*.java"],
     test_suites: [
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 bc9691d..832ac03 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -25,12 +25,14 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
 
 import android.app.UiAutomation;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothManager;
 import android.bluetooth.cts.BTAdapterUtils;
 import android.content.Context;
+import android.location.LocationManager;
 import android.nearby.BroadcastCallback;
 import android.nearby.BroadcastRequest;
 import android.nearby.NearbyDevice;
@@ -42,6 +44,8 @@
 import android.nearby.ScanCallback;
 import android.nearby.ScanRequest;
 import android.os.Build;
+import android.os.Process;
+import android.os.UserHandle;
 import android.provider.DeviceConfig;
 
 import androidx.annotation.NonNull;
@@ -50,6 +54,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SdkSuppress;
 
+import com.android.compatibility.common.util.SystemUtil;
 import com.android.modules.utils.build.SdkLevel;
 
 import org.junit.Before;
@@ -57,6 +62,7 @@
 import org.junit.runner.RunWith;
 
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
@@ -189,6 +195,92 @@
         mScanCallback.onError(ERROR_UNSUPPORTED);
     }
 
+    @Test
+    public void testsetPoweredOffFindingEphemeralIds() {
+        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+        assumeTrue(SdkLevel.isAtLeastV());
+        // Only test supporting devices.
+        if (mNearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+        mNearbyManager.setPoweredOffFindingEphemeralIds(List.of(new byte[20], new byte[20]));
+    }
+
+    @Test
+    public void testsetPoweredOffFindingEphemeralIds_noPrivilegedPermission() {
+        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+        assumeTrue(SdkLevel.isAtLeastV());
+        // Only test supporting devices.
+        if (mNearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+        mUiAutomation.dropShellPermissionIdentity();
+
+        assertThrows(SecurityException.class,
+                () -> mNearbyManager.setPoweredOffFindingEphemeralIds(List.of(new byte[20])));
+    }
+
+
+    @Test
+    public void testSetAndGetPoweredOffFindingMode_enabled() {
+        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+        assumeTrue(SdkLevel.isAtLeastV());
+        // Only test supporting devices.
+        if (mNearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+        enableLocation();
+        // enableLocation() has dropped shell permission identity.
+        mUiAutomation.adoptShellPermissionIdentity(BLUETOOTH_PRIVILEGED);
+
+        mNearbyManager.setPoweredOffFindingMode(
+                NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
+        assertThat(mNearbyManager.getPoweredOffFindingMode())
+                .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED);
+    }
+
+    @Test
+    public void testSetAndGetPoweredOffFindingMode_disabled() {
+        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+        assumeTrue(SdkLevel.isAtLeastV());
+        // Only test supporting devices.
+        if (mNearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+        mNearbyManager.setPoweredOffFindingMode(
+                NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
+        assertThat(mNearbyManager.getPoweredOffFindingMode())
+                .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED);
+    }
+
+    @Test
+    public void testSetPoweredOffFindingMode_noPrivilegedPermission() {
+        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+        assumeTrue(SdkLevel.isAtLeastV());
+        // Only test supporting devices.
+        if (mNearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+        enableLocation();
+        mUiAutomation.dropShellPermissionIdentity();
+
+        assertThrows(SecurityException.class, () -> mNearbyManager
+                .setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED));
+    }
+
+    @Test
+    public void testGetPoweredOffFindingMode_noPrivilegedPermission() {
+        // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used.
+        assumeTrue(SdkLevel.isAtLeastV());
+        // Only test supporting devices.
+        if (mNearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return;
+
+        mUiAutomation.dropShellPermissionIdentity();
+
+        assertThrows(SecurityException.class, () -> mNearbyManager.getPoweredOffFindingMode());
+    }
+
     private void enableBluetooth() {
         BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
         BluetoothAdapter bluetoothAdapter = manager.getAdapter();
@@ -197,6 +289,13 @@
         }
     }
 
+    private void enableLocation() {
+        LocationManager locationManager = mContext.getSystemService(LocationManager.class);
+        UserHandle user = Process.myUserHandle();
+        SystemUtil.runWithShellPermissionIdentity(
+                mUiAutomation, () -> locationManager.setLocationEnabledForUser(true, user));
+    }
+
     private static class OffloadCallback implements Consumer<OffloadCapability> {
         @Override
         public void accept(OffloadCapability aBoolean) {
diff --git a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
index 506b4e2..b949720 100644
--- a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
+++ b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt
@@ -29,6 +29,7 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -96,4 +97,49 @@
         )
         nearbyManager.stopBroadcast(broadcastCallback)
     }
+
+    /** Verify privileged app can set powered off finding ephemeral IDs without exception. */
+    @Test
+    fun testNearbyManagerSetPoweredOffFindingEphemeralIds_fromPrivilegedApp_succeed() {
+        val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+        // Only test supporting devices.
+        if (nearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return
+
+        val eid = ByteArray(20)
+
+        nearbyManager.setPoweredOffFindingEphemeralIds(listOf(eid))
+    }
+
+    /**
+     * Verifies that [NearbyManager.setPoweredOffFindingEphemeralIds] checkes the ephemeral ID
+     * length.
+     */
+    @Test
+    fun testNearbyManagerSetPoweredOffFindingEphemeralIds_wrongSize_throwsException() {
+        val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+        // Only test supporting devices.
+        if (nearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return
+
+        assertThrows(IllegalArgumentException::class.java) {
+            nearbyManager.setPoweredOffFindingEphemeralIds(listOf(ByteArray(21)))
+        }
+        assertThrows(IllegalArgumentException::class.java) {
+            nearbyManager.setPoweredOffFindingEphemeralIds(listOf(ByteArray(19)))
+        }
+    }
+
+    /** Verify privileged app can set and get powered off finding mode without exception. */
+    @Test
+    fun testNearbyManagerSetGetPoweredOffMode_fromPrivilegedApp_succeed() {
+        val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+        // Only test supporting devices.
+        if (nearbyManager.getPoweredOffFindingMode()
+                == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return
+
+        nearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED)
+        assertThat(nearbyManager.getPoweredOffFindingMode())
+                .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED)
+    }
 }
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 7bf9f63..015d022 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
@@ -30,12 +30,12 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.uiautomator.LogcatWaitMixin
 import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import java.util.Calendar
 import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.time.Duration
-import java.util.Calendar
 
 @RunWith(AndroidJUnit4::class)
 class NearbyManagerTest {
@@ -151,6 +151,46 @@
         ).isTrue()
     }
 
+    /**
+     * Verify untrusted app can't set powered off finding ephemeral IDs because it needs
+     * BLUETOOTH_PRIVILEGED permission which is not for use by third-party applications.
+     */
+    @Test
+    fun testNearbyManagerSetPoweredOffFindingEphemeralIds_fromUnTrustedApp_throwsException() {
+        val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+        val eid = ByteArray(20)
+
+        assertThrows(SecurityException::class.java) {
+            nearbyManager.setPoweredOffFindingEphemeralIds(listOf(eid))
+        }
+    }
+
+    /**
+     * Verify untrusted app can't set powered off finding mode because it needs BLUETOOTH_PRIVILEGED
+     * permission which is not for use by third-party applications.
+     */
+    @Test
+    fun testNearbyManagerSetPoweredOffFindingMode_fromUnTrustedApp_throwsException() {
+        val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+
+        assertThrows(SecurityException::class.java) {
+            nearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED)
+        }
+    }
+
+    /**
+     * Verify untrusted app can't get powered off finding mode because it needs BLUETOOTH_PRIVILEGED
+     * permission which is not for use by third-party applications.
+     */
+    @Test
+    fun testNearbyManagerGetPoweredOffFindingMode_fromUnTrustedApp_throwsException() {
+        val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager
+
+        assertThrows(SecurityException::class.java) {
+            nearbyManager.getPoweredOffFindingMode()
+        }
+    }
+
     companion object {
         private val WAIT_INVALID_OPERATIONS_LOGS_TIMEOUT = Duration.ofSeconds(5)
     }
diff --git a/netbpfload/Android.bp b/netbpfload/Android.bp
index b5e4722..b71890e 100644
--- a/netbpfload/Android.bp
+++ b/netbpfload/Android.bp
@@ -44,7 +44,7 @@
         "com.android.tethering",
         "//apex_available:platform",
     ],
-    // really should be Android 14/U (34), but we cannot include binaries built
+    // really should be Android 13/T (33), but we cannot include binaries built
     // against newer sdk in the apex, which still targets 30(R):
     // module "netbpfload" variant "android_x86_apex30": should support
     // min_sdk_version(30) for "com.android.tethering": newer SDK(34).
@@ -54,15 +54,14 @@
     required: ["bpfloader"],
 }
 
-// Versioned netbpfload init rc: init system will process it only on api V/35+ devices
-// (TODO: consider reducing to T/33+ - adjust the comment up above in line 43 as well)
-// Note: S[31] Sv2[32] T[33] U[34] V[35])
+// Versioned netbpfload init rc: init system will process it only on api T/33+ devices
+// Note: R[30] S[31] Sv2[32] T[33] U[34] V[35])
 //
 // For details of versioned rc files see:
 // https://android.googlesource.com/platform/system/core/+/HEAD/init/README.md#versioned-rc-files-within-apexs
 prebuilt_etc {
     name: "netbpfload.mainline.rc",
     src: "netbpfload.mainline.rc",
-    filename: "netbpfload.35rc",
+    filename: "netbpfload.33rc",
     installable: false,
 }
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index 3c5e4c3..2d8867e 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -243,9 +243,15 @@
     const bool isAtLeastU = (device_api_level >= __ANDROID_API_U__);
     const bool isAtLeastV = (device_api_level >= __ANDROID_API_V__);
 
-    ALOGI("NetBpfLoad api:%d/%d kver:%07x platform:%d mainline:%d",
+    // last in U QPR2 beta1
+    const bool has_platform_bpfloader_rc = exists("/system/etc/init/bpfloader.rc");
+    // first in U QPR2 beta~2
+    const bool has_platform_netbpfload_rc = exists("/system/etc/init/netbpfload.rc");
+
+    ALOGI("NetBpfLoad api:%d/%d kver:%07x platform:%d mainline:%d rc:%d%d",
           android_get_application_target_sdk_version(), device_api_level,
-          android::bpf::kernelVersion(), is_platform, is_mainline);
+          android::bpf::kernelVersion(), is_platform, is_mainline,
+          has_platform_bpfloader_rc, has_platform_netbpfload_rc);
 
     if (!is_platform && !is_mainline) {
         ALOGE("Unable to determine if we're platform or mainline netbpfload.");
@@ -258,8 +264,28 @@
         ALOGW("exec '%s' fail: %d[%s]", apexNetBpfLoad, errno, strerror(errno));
     }
 
+    if (!has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
+        ALOGE("Unable to find platform's bpfloader & netbpfload init scripts.");
+        return 1;
+    }
+
+    if (has_platform_bpfloader_rc && has_platform_netbpfload_rc) {
+        ALOGE("Platform has *both* bpfloader & netbpfload init scripts.");
+        return 1;
+    }
+
     logTetheringApexVersion();
 
+    if (is_mainline && has_platform_bpfloader_rc && !has_platform_netbpfload_rc) {
+        // Tethering apex shipped initrc file causes us to reach here
+        // but we're not ready to correctly handle anything before U QPR2
+        // in which the 'bpfloader' vs 'netbpfload' split happened
+        const char * args[] = { platformBpfLoader, NULL, };
+        execve(args[0], (char**)args, envp);
+        ALOGE("exec '%s' fail: %d[%s]", platformBpfLoader, errno, strerror(errno));
+        return 1;
+    }
+
     if (isAtLeastT && !android::bpf::isAtLeastKernelVersion(4, 9, 0)) {
         ALOGE("Android T requires kernel 4.9.");
         return 1;
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 57bcebd..9ba49d2 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -2616,7 +2616,15 @@
     /* Information tracked per client */
     private class ClientInfo {
 
-        private static final int MAX_LIMIT = 10;
+        /**
+         * Maximum number of requests (callbacks) for a client.
+         *
+         * 200 listeners should be more than enough for most use-cases: even if a client tries to
+         * file callbacks for every service on a local network, there are generally much less than
+         * 200 devices on a local network (a /24 only allows 255 IPv4 devices), and while some
+         * devices may have multiple services, many devices do not advertise any.
+         */
+        private static final int MAX_LIMIT = 200;
         private final INsdManagerCallback mCb;
         /* Remembers a resolved service until getaddrinfo completes */
         private NsdServiceInfo mResolvedService;
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index eff9bbd..e6287bc 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -1956,8 +1956,8 @@
         mMulticastRoutingCoordinatorService =
                 mDeps.makeMulticastRoutingCoordinatorService(mHandler);
 
-        mDestroyFrozenSockets = mDeps.isAtLeastU()
-                && mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION);
+        mDestroyFrozenSockets = mDeps.isAtLeastV() || (mDeps.isAtLeastU()
+                && mDeps.isFeatureEnabled(context, KEY_DESTROY_FROZEN_SOCKETS_VERSION));
         mDelayDestroyFrozenSockets = mDeps.isAtLeastU()
                 && mDeps.isFeatureEnabled(context, DELAY_DESTROY_FROZEN_SOCKETS_VERSION);
         mAllowSysUiConnectivityReports = mDeps.isFeatureNotChickenedOut(
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index b60f0b4..624855e 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -34,6 +34,7 @@
 import static android.net.connectivity.ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER;
 import static android.net.nsd.NsdManager.FAILURE_BAD_PARAMETERS;
 import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
+import static android.net.nsd.NsdManager.FAILURE_MAX_LIMIT;
 import static android.net.nsd.NsdManager.FAILURE_OPERATION_NOT_RUNNING;
 
 import static com.android.networkstack.apishim.api33.ConstantsShim.REGISTER_NSD_OFFLOAD_ENGINE;
@@ -131,10 +132,12 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.InOrder;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
@@ -257,6 +260,10 @@
             mThread.quitSafely();
             mThread.join();
         }
+
+        // Clear inline mocks as there are possible memory leaks if not done (see mockito
+        // doc for clearInlineMocks), and some tests create many of them.
+        Mockito.framework().clearInlineMocks();
     }
 
     // Native mdns provided by Netd is removed after U.
@@ -717,6 +724,86 @@
                 true /* isLegacy */, getAddrId, 10L /* durationMs */);
     }
 
+    @EnableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+    @Test
+    public void testPerClientListenerLimit() throws Exception {
+        final NsdManager client1 = connectClient(mService);
+        final NsdManager client2 = connectClient(mService);
+
+        final String testType1 = "_testtype1._tcp";
+        final NsdServiceInfo testServiceInfo1 = new NsdServiceInfo("MyTestService1", testType1);
+        testServiceInfo1.setPort(12345);
+        final String testType2 = "_testtype2._tcp";
+        final NsdServiceInfo testServiceInfo2 = new NsdServiceInfo("MyTestService2", testType2);
+        testServiceInfo2.setPort(12345);
+
+        // Each client can register 200 requests (for example 100 discover and 100 register).
+        final int numEachListener = 100;
+        final ArrayList<DiscoveryListener> discListeners = new ArrayList<>(numEachListener);
+        final ArrayList<RegistrationListener> regListeners = new ArrayList<>(numEachListener);
+        for (int i = 0; i < numEachListener; i++) {
+            final DiscoveryListener discListener1 = mock(DiscoveryListener.class);
+            discListeners.add(discListener1);
+            final RegistrationListener regListener1 = mock(RegistrationListener.class);
+            regListeners.add(regListener1);
+            final DiscoveryListener discListener2 = mock(DiscoveryListener.class);
+            discListeners.add(discListener2);
+            final RegistrationListener regListener2 = mock(RegistrationListener.class);
+            regListeners.add(regListener2);
+            client1.discoverServices(testType1, NsdManager.PROTOCOL_DNS_SD,
+                    (Network) null, Runnable::run, discListener1);
+            client1.registerService(testServiceInfo1, NsdManager.PROTOCOL_DNS_SD, Runnable::run,
+                    regListener1);
+
+            client2.registerService(testServiceInfo2, NsdManager.PROTOCOL_DNS_SD, Runnable::run,
+                    regListener2);
+            client2.discoverServices(testType2, NsdManager.PROTOCOL_DNS_SD,
+                    (Network) null, Runnable::run, discListener2);
+        }
+
+        // Use a longer timeout than usual for the handler to process all the events. The
+        // registrations take about 1s on a high-end 2013 device.
+        HandlerUtils.waitForIdle(mHandler, 30_000L);
+        for (int i = 0; i < discListeners.size(); i++) {
+            // Callbacks are sent on the manager handler which is different from mHandler, so use
+            // a short timeout (each callback should come quickly after the previous one).
+            verify(discListeners.get(i), timeout(TEST_TIME_MS))
+                    .onDiscoveryStarted(i % 2 == 0 ? testType1 : testType2);
+
+            // registerService does not get a callback before probing finishes (will not happen as
+            // this is mocked)
+            verifyNoMoreInteractions(regListeners.get(i));
+        }
+
+        // The next registrations should fail
+        final DiscoveryListener failDiscListener1 = mock(DiscoveryListener.class);
+        final RegistrationListener failRegListener1 = mock(RegistrationListener.class);
+        final DiscoveryListener failDiscListener2 = mock(DiscoveryListener.class);
+        final RegistrationListener failRegListener2 = mock(RegistrationListener.class);
+
+        client1.discoverServices(testType1, NsdManager.PROTOCOL_DNS_SD,
+                (Network) null, Runnable::run, failDiscListener1);
+        verify(failDiscListener1, timeout(TEST_TIME_MS))
+                .onStartDiscoveryFailed(testType1, FAILURE_MAX_LIMIT);
+
+        client1.registerService(testServiceInfo1, NsdManager.PROTOCOL_DNS_SD, Runnable::run,
+                failRegListener1);
+        verify(failRegListener1, timeout(TEST_TIME_MS)).onRegistrationFailed(
+                argThat(a -> testServiceInfo1.getServiceName().equals(a.getServiceName())),
+                eq(FAILURE_MAX_LIMIT));
+
+        client1.discoverServices(testType2, NsdManager.PROTOCOL_DNS_SD,
+                (Network) null, Runnable::run, failDiscListener2);
+        verify(failDiscListener2, timeout(TEST_TIME_MS))
+                .onStartDiscoveryFailed(testType2, FAILURE_MAX_LIMIT);
+
+        client1.registerService(testServiceInfo2, NsdManager.PROTOCOL_DNS_SD, Runnable::run,
+                failRegListener2);
+        verify(failRegListener2, timeout(TEST_TIME_MS)).onRegistrationFailed(
+                argThat(a -> testServiceInfo2.getServiceName().equals(a.getServiceName())),
+                eq(FAILURE_MAX_LIMIT));
+    }
+
     @Test
     @DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
     @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)