Merge "[CLATJ#28] ClatCoordinator: spawn clatd with flag POSIX_SPAWN_CLOEXEC_DEFAULT" into tm-dev
diff --git a/nearby/framework/java/android/nearby/INearbyManager.aidl b/nearby/framework/java/android/nearby/INearbyManager.aidl
index 62e109e..3fd5ecc 100644
--- a/nearby/framework/java/android/nearby/INearbyManager.aidl
+++ b/nearby/framework/java/android/nearby/INearbyManager.aidl
@@ -28,12 +28,13 @@
  */
 interface INearbyManager {
 
-    int registerScanListener(in ScanRequest scanRequest, in IScanListener listener);
+    int registerScanListener(in ScanRequest scanRequest, in IScanListener listener,
+            String packageName, @nullable String attributionTag);
 
     void unregisterScanListener(in IScanListener listener);
 
     void startBroadcast(in BroadcastRequestParcelable broadcastRequest,
-            in IBroadcastListener callback);
+            in IBroadcastListener callback, String packageName, @nullable String attributionTag);
 
     void stopBroadcast(in IBroadcastListener callback);
 }
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/IScanListener.aidl b/nearby/framework/java/android/nearby/IScanListener.aidl
index 54033aa..3e3b107 100644
--- a/nearby/framework/java/android/nearby/IScanListener.aidl
+++ b/nearby/framework/java/android/nearby/IScanListener.aidl
@@ -32,4 +32,7 @@
 
         /** Reports a {@link NearbyDevice} is no longer within range. */
         void onLost(in NearbyDeviceParcelable nearbyDeviceParcelable);
+
+        /** Reports when there is an error during scanning. */
+        void onError();
 }
diff --git a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
index 1ad3571..a9d7cf7 100644
--- a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
+++ b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
@@ -20,7 +20,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.bluetooth.le.ScanRecord;
-import android.bluetooth.le.ScanResult;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -89,6 +88,7 @@
     @Nullable private final String mBluetoothAddress;
     @Nullable private final String mFastPairModelId;
     @Nullable private final byte[] mData;
+    @Nullable private final byte[] mSalt;
 
     private NearbyDeviceParcelable(
             @ScanRequest.ScanType int scanType,
@@ -100,7 +100,8 @@
             PublicCredential publicCredential,
             @Nullable String fastPairModelId,
             @Nullable String bluetoothAddress,
-            @Nullable byte[] data) {
+            @Nullable byte[] data,
+            @Nullable byte[] salt) {
         mScanType = scanType;
         mName = name;
         mMedium = medium;
@@ -111,6 +112,7 @@
         mFastPairModelId = fastPairModelId;
         mBluetoothAddress = bluetoothAddress;
         mData = data;
+        mSalt = salt;
     }
 
     /** No special parcel contents. */
@@ -149,6 +151,11 @@
             dest.writeInt(mData.length);
             dest.writeByteArray(mData);
         }
+        dest.writeInt(mSalt == null ? 0 : 1);
+        if (mSalt != null) {
+            dest.writeInt(mSalt.length);
+            dest.writeByteArray(mSalt);
+        }
     }
 
     /** Returns a string representation of this ScanRequest. */
@@ -171,6 +178,8 @@
                 + mFastPairModelId
                 + ", data="
                 + Arrays.toString(mData)
+                + ", salt="
+                + Arrays.toString(mSalt)
                 + "]";
     }
 
@@ -189,7 +198,8 @@
                             mBluetoothAddress, otherNearbyDeviceParcelable.mBluetoothAddress))
                     && (Objects.equals(
                             mFastPairModelId, otherNearbyDeviceParcelable.mFastPairModelId))
-                    && (Arrays.equals(mData, otherNearbyDeviceParcelable.mData));
+                    && (Arrays.equals(mData, otherNearbyDeviceParcelable.mData))
+                    && (Arrays.equals(mSalt, otherNearbyDeviceParcelable.mSalt));
         }
         return false;
     }
@@ -204,7 +214,8 @@
                 mPublicCredential.hashCode(),
                 mBluetoothAddress,
                 mFastPairModelId,
-                Arrays.hashCode(mData));
+                Arrays.hashCode(mData),
+                Arrays.hashCode(mSalt));
     }
 
     /**
@@ -217,7 +228,11 @@
         return mScanType;
     }
 
-    /** Gets the name of the NearbyDeviceParcelable. Returns {@code null} If there is no name. */
+    /**
+     * Gets the name of the NearbyDeviceParcelable. Returns {@code null} If there is no name.
+     *
+     * Used in Fast Pair.
+     */
     @Nullable
     public String getName() {
         return mName;
@@ -226,6 +241,8 @@
     /**
      * Gets the {@link android.nearby.NearbyDevice.Medium} of the NearbyDeviceParcelable over which
      * it is discovered.
+     *
+     * Used in Fast Pair and Nearby Presence.
      */
     @NearbyDevice.Medium
     public int getMedium() {
@@ -235,6 +252,8 @@
     /**
      * Gets the transmission power in dBm.
      *
+     * Used in Fast Pair.
+     *
      * @hide
      */
     @IntRange(from = -127, to = 126)
@@ -242,7 +261,11 @@
         return mTxPower;
     }
 
-    /** Gets the received signal strength in dBm. */
+    /**
+     * Gets the received signal strength in dBm.
+     *
+     * Used in Fast Pair and Nearby Presence.
+     */
     @IntRange(from = -127, to = 126)
     public int getRssi() {
         return mRssi;
@@ -251,6 +274,8 @@
     /**
      * Gets the Action.
      *
+     * Used in Nearby Presence.
+     *
      * @hide
      */
     @IntRange(from = -127, to = 126)
@@ -261,6 +286,8 @@
     /**
      * Gets the public credential.
      *
+     * Used in Nearby Presence.
+     *
      * @hide
      */
     @NonNull
@@ -271,6 +298,8 @@
     /**
      * Gets the Fast Pair identifier. Returns {@code null} if there is no Model ID or this is not a
      * Fast Pair device.
+     *
+     * Used in Fast Pair.
      */
     @Nullable
     public String getFastPairModelId() {
@@ -280,18 +309,36 @@
     /**
      * Gets the Bluetooth device hardware address. Returns {@code null} if the device is not
      * discovered by Bluetooth.
+     *
+     * Used in Fast Pair.
      */
     @Nullable
     public String getBluetoothAddress() {
         return mBluetoothAddress;
     }
 
-    /** Gets the raw data from the scanning. Returns {@code null} if there is no extra data. */
+    /**
+     * Gets the raw data from the scanning.
+     * Returns {@code null} if there is no extra data or this is not a Fast Pair device.
+     *
+     * Used in Fast Pair.
+     */
     @Nullable
     public byte[] getData() {
         return mData;
     }
 
+    /**
+     * Gets the salt in the advertisement from the Nearby Presence device.
+     * Returns {@code null} if this is not a Nearby Presence device.
+     *
+     * Used in Nearby Presence.
+     */
+    @Nullable
+    public byte[] getSalt() {
+        return mSalt;
+    }
+
     /** Builder class for {@link NearbyDeviceParcelable}. */
     public static final class Builder {
         @Nullable private String mName;
@@ -304,6 +351,7 @@
         @Nullable private String mFastPairModelId;
         @Nullable private String mBluetoothAddress;
         @Nullable private byte[] mData;
+        @Nullable private byte[] mSalt;
 
         /**
          * Sets the scan type of the NearbyDeviceParcelable.
@@ -410,7 +458,7 @@
          * Sets the scanned raw data.
          *
          * @param data Data the scan. For example, {@link ScanRecord#getServiceData()} if scanned by
-         *     Bluetooth.
+         *             Bluetooth.
          */
         @NonNull
         public Builder setData(@Nullable byte[] data) {
@@ -418,6 +466,17 @@
             return this;
         }
 
+        /**
+         * Sets the slat in the advertisement from the Nearby Presence device.
+         *
+         * @param salt in the advertisement from the Nearby Presence device.
+         */
+        @NonNull
+        public Builder setSalt(@Nullable byte[] salt) {
+            mSalt = salt;
+            return this;
+        }
+
         /** Builds a ScanResult. */
         @NonNull
         public NearbyDeviceParcelable build() {
@@ -431,7 +490,8 @@
                     mPublicCredential,
                     mFastPairModelId,
                     mBluetoothAddress,
-                    mData);
+                    mData,
+                    mSalt);
         }
     }
 }
diff --git a/nearby/framework/java/android/nearby/NearbyFrameworkInitializer.java b/nearby/framework/java/android/nearby/NearbyFrameworkInitializer.java
index 3780fbb..b732d67 100644
--- a/nearby/framework/java/android/nearby/NearbyFrameworkInitializer.java
+++ b/nearby/framework/java/android/nearby/NearbyFrameworkInitializer.java
@@ -43,7 +43,7 @@
                 NearbyManager.class,
                 (context, serviceBinder) -> {
                     INearbyManager service = INearbyManager.Stub.asInterface(serviceBinder);
-                    return new NearbyManager(service);
+                    return new NearbyManager(context, service);
                 }
         );
     }
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index 3dd08da..9073f78 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -28,6 +28,7 @@
 import android.content.Context;
 import android.os.RemoteException;
 import android.provider.Settings;
+import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
@@ -85,6 +86,7 @@
     private static final WeakHashMap<BroadcastCallback, WeakReference<BroadcastListenerTransport>>
             sBroadcastListeners = new WeakHashMap<>();
 
+    private final Context mContext;
     private final INearbyManager mService;
 
     /**
@@ -92,7 +94,10 @@
      *
      * @param service the service object
      */
-    NearbyManager(@NonNull INearbyManager service) {
+    NearbyManager(@NonNull Context context, @NonNull INearbyManager service) {
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(service);
+        mContext = context;
         mService = service;
     }
 
@@ -109,6 +114,26 @@
                     .setBluetoothAddress(nearbyDeviceParcelable.getBluetoothAddress())
                     .setData(nearbyDeviceParcelable.getData()).build();
         }
+
+        if (scanType == ScanRequest.SCAN_TYPE_NEARBY_PRESENCE) {
+            PublicCredential publicCredential = nearbyDeviceParcelable.getPublicCredential();
+            if (publicCredential == null) {
+                return null;
+            }
+            byte[] salt = nearbyDeviceParcelable.getSalt();
+            if (salt == null) {
+                salt = new byte[0];
+            }
+            return new PresenceDevice.Builder(
+                    // Use the public credential hash as the device Id.
+                    String.valueOf(publicCredential.hashCode()),
+                    salt,
+                    publicCredential.getSecretId(),
+                    publicCredential.getEncryptedMetadata())
+                    .setRssi(nearbyDeviceParcelable.getRssi())
+                    .addMedium(nearbyDeviceParcelable.getMedium())
+                    .build();
+        }
         return null;
     }
 
@@ -143,7 +168,8 @@
                     Preconditions.checkState(transport.isRegistered());
                     transport.setExecutor(executor);
                 }
-                @ScanStatus int status = mService.registerScanListener(scanRequest, transport);
+                @ScanStatus int status = mService.registerScanListener(scanRequest, transport,
+                        mContext.getPackageName(), mContext.getAttributionTag());
                 if (status != ScanStatus.SUCCESS) {
                     return status;
                 }
@@ -208,8 +234,8 @@
                     Preconditions.checkState(transport.isRegistered());
                     transport.setExecutor(executor);
                 }
-                mService.startBroadcast(new BroadcastRequestParcelable(broadcastRequest),
-                        transport);
+                mService.startBroadcast(new BroadcastRequestParcelable(broadcastRequest), transport,
+                        mContext.getPackageName(), mContext.getAttributionTag());
                 sBroadcastListeners.put(callback, new WeakReference<>(transport));
             }
         } catch (RemoteException e) {
@@ -330,6 +356,15 @@
                 }
             });
         }
+
+        @Override
+        public void onError() {
+            mExecutor.execute(() -> {
+                if (mScanCallback != null) {
+                    Log.e("NearbyManager", "onError: There is an error in scan.");
+                }
+            });
+        }
     }
 
     private static class BroadcastListenerTransport extends IBroadcastListener.Stub {
diff --git a/nearby/framework/java/android/nearby/PresenceDevice.java b/nearby/framework/java/android/nearby/PresenceDevice.java
index 12fc2a3..cb406e4 100644
--- a/nearby/framework/java/android/nearby/PresenceDevice.java
+++ b/nearby/framework/java/android/nearby/PresenceDevice.java
@@ -268,6 +268,11 @@
          */
         public Builder(@NonNull String deviceId, @NonNull byte[] salt, @NonNull byte[] secretId,
                 @NonNull byte[] encryptedIdentity) {
+            Objects.requireNonNull(deviceId);
+            Objects.requireNonNull(salt);
+            Objects.requireNonNull(secretId);
+            Objects.requireNonNull(encryptedIdentity);
+
             mDeviceId = deviceId;
             mSalt = salt;
             mSecretId = secretId;
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index e3e5b5d..2dee835 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -17,9 +17,12 @@
 package com.android.server.nearby;
 
 import static com.android.server.SystemService.PHASE_BOOT_COMPLETED;
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.SystemService.PHASE_THIRD_PARTY_APPS_CAN_START;
 
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.app.AppOpsManager;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothManager;
 import android.content.BroadcastReceiver;
@@ -35,6 +38,7 @@
 import android.nearby.ScanRequest;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.nearby.common.locator.LocatorContextWrapper;
 import com.android.server.nearby.fastpair.FastPairManager;
 import com.android.server.nearby.injector.ContextHubManagerAdapter;
@@ -43,13 +47,16 @@
 import com.android.server.nearby.provider.BroadcastProviderManager;
 import com.android.server.nearby.provider.DiscoveryProviderManager;
 import com.android.server.nearby.provider.FastPairDataProvider;
+import com.android.server.nearby.util.identity.CallerIdentity;
+import com.android.server.nearby.util.permissions.BroadcastPermissions;
+import com.android.server.nearby.util.permissions.DiscoveryPermissions;
 
 /** Service implementing nearby functionality. */
 public class NearbyService extends INearbyManager.Stub {
     public static final String TAG = "NearbyService";
 
     private final Context mContext;
-    private final SystemInjector mSystemInjector;
+    private Injector mInjector;
     private final FastPairManager mFastPairManager;
     private final PresenceManager mPresenceManager;
     private final BroadcastReceiver mBluetoothReceiver =
@@ -60,11 +67,11 @@
                             intent.getIntExtra(
                                     BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
                     if (state == BluetoothAdapter.STATE_ON) {
-                        if (mSystemInjector != null) {
+                        if (mInjector != null && mInjector instanceof SystemInjector) {
                             // Have to do this logic in listener. Even during PHASE_BOOT_COMPLETED
                             // phase, BluetoothAdapter is not null, the BleScanner is null.
                             Log.v(TAG, "Initiating BluetoothAdapter when Bluetooth is turned on.");
-                            mSystemInjector.initializeBluetoothAdapter();
+                            ((SystemInjector) mInjector).initializeBluetoothAdapter();
                         }
                     }
                 }
@@ -74,18 +81,29 @@
 
     public NearbyService(Context context) {
         mContext = context;
-        mSystemInjector = new SystemInjector(context);
-        mProviderManager = new DiscoveryProviderManager(context, mSystemInjector);
-        mBroadcastProviderManager = new BroadcastProviderManager(context, mSystemInjector);
+        mInjector = new SystemInjector(context);
+        mProviderManager = new DiscoveryProviderManager(context, mInjector);
+        mBroadcastProviderManager = new BroadcastProviderManager(context, mInjector);
         final LocatorContextWrapper lcw = new LocatorContextWrapper(context, null);
         mFastPairManager = new FastPairManager(lcw);
         mPresenceManager = new PresenceManager(lcw);
     }
 
+    @VisibleForTesting
+    void setInjector(Injector injector) {
+        this.mInjector = injector;
+    }
+
     @Override
     @NearbyManager.ScanStatus
-    public int registerScanListener(ScanRequest scanRequest, IScanListener listener) {
-        if (mProviderManager.registerScanListener(scanRequest, listener)) {
+    public int registerScanListener(ScanRequest scanRequest, IScanListener listener,
+            String packageName, @Nullable String attributionTag) {
+        // Permissions check
+        enforceBluetoothPrivilegedPermission(mContext);
+        CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
+        DiscoveryPermissions.enforceDiscoveryPermission(mContext, identity);
+
+        if (mProviderManager.registerScanListener(scanRequest, listener, identity)) {
             return NearbyManager.ScanStatus.SUCCESS;
         }
         return NearbyManager.ScanStatus.ERROR;
@@ -98,10 +116,12 @@
 
     @Override
     public void startBroadcast(BroadcastRequestParcelable broadcastRequestParcelable,
-            IBroadcastListener listener) {
+            IBroadcastListener listener, String packageName, @Nullable String attributionTag) {
+        enforceBluetoothPrivilegedPermission(mContext);
+        BroadcastPermissions.enforceBroadcastPermission(
+                mContext, CallerIdentity.fromBinder(mContext, packageName, attributionTag));
         mBroadcastProviderManager.startBroadcast(
-                broadcastRequestParcelable.getBroadcastRequest(),
-                listener);
+                broadcastRequestParcelable.getBroadcastRequest(), listener);
     }
 
     @Override
@@ -116,28 +136,48 @@
      */
     public void onBootPhase(int phase) {
         switch (phase) {
+            case PHASE_SYSTEM_SERVICES_READY:
+                if (mInjector instanceof SystemInjector) {
+                    ((SystemInjector) mInjector).initializeAppOpsManager();
+                }
+                break;
             case PHASE_THIRD_PARTY_APPS_CAN_START:
                 // Ensures that a fast pair data provider exists which will work in direct boot.
                 FastPairDataProvider.init(mContext);
                 break;
             case PHASE_BOOT_COMPLETED:
-                // The nearby service must be functioning after this boot phase.
-                mSystemInjector.initializeBluetoothAdapter();
+                if (mInjector instanceof SystemInjector) {
+                    // The nearby service must be functioning after this boot phase.
+                    ((SystemInjector) mInjector).initializeBluetoothAdapter();
+                    // Initialize ContextManager for CHRE scan.
+                    ((SystemInjector) mInjector).initializeContextHubManagerAdapter();
+                }
                 mContext.registerReceiver(
                         mBluetoothReceiver,
                         new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
                 mFastPairManager.initiate();
-                // Initialize ContextManager for CHRE scan.
-                mSystemInjector.initializeContextHubManagerAdapter();
                 mPresenceManager.initiate();
                 break;
         }
     }
 
+    /**
+     * If the calling process of has not been granted
+     * {@link android.Manifest.permission.BLUETOOTH_PRIVILEGED} permission,
+     * throw a {@link SecurityException}.
+     */
+    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+    private static void enforceBluetoothPrivilegedPermission(Context context) {
+        context.enforceCallingOrSelfPermission(
+                android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+                "Need BLUETOOTH PRIVILEGED permission");
+    }
+
     private static final class SystemInjector implements Injector {
         private final Context mContext;
         @Nullable private BluetoothAdapter mBluetoothAdapter;
         @Nullable private ContextHubManagerAdapter mContextHubManagerAdapter;
+        @Nullable private AppOpsManager mAppOpsManager;
 
         SystemInjector(Context context) {
             mContext = context;
@@ -155,6 +195,12 @@
             return mContextHubManagerAdapter;
         }
 
+        @Override
+        @Nullable
+        public AppOpsManager getAppOpsManager() {
+            return mAppOpsManager;
+        }
+
         synchronized void initializeBluetoothAdapter() {
             if (mBluetoothAdapter != null) {
                 return;
@@ -176,5 +222,12 @@
             }
             mContextHubManagerAdapter = new ContextHubManagerAdapter(manager);
         }
+
+        synchronized void initializeAppOpsManager() {
+            if (mAppOpsManager != null) {
+                return;
+            }
+            mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
+        }
     }
 }
diff --git a/nearby/service/java/com/android/server/nearby/injector/Injector.java b/nearby/service/java/com/android/server/nearby/injector/Injector.java
index f990dc9..57784a9 100644
--- a/nearby/service/java/com/android/server/nearby/injector/Injector.java
+++ b/nearby/service/java/com/android/server/nearby/injector/Injector.java
@@ -16,6 +16,7 @@
 
 package com.android.server.nearby.injector;
 
+import android.app.AppOpsManager;
 import android.bluetooth.BluetoothAdapter;
 
 /**
@@ -29,4 +30,7 @@
 
     /** Get the ContextHubManagerAdapter for ChreDiscoveryProvider to scan. */
     ContextHubManagerAdapter getContextHubManagerAdapter();
+
+    /** Get the AppOpsManager to control access. */
+    AppOpsManager getAppOpsManager();
 }
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java b/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
index 80ad88d..d1c72ae 100644
--- a/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
@@ -30,9 +30,14 @@
 
     /** Creates a {@link PresenceDiscoveryResult} from the scan data. */
     public static PresenceDiscoveryResult fromDevice(NearbyDeviceParcelable device) {
+        byte[] salt = device.getSalt();
+        if (salt == null) {
+            salt = new byte[0];
+        }
         return new PresenceDiscoveryResult.Builder()
                 .setTxPower(device.getTxPower())
                 .setRssi(device.getRssi())
+                .setSalt(salt)
                 .addPresenceAction(device.getAction())
                 .setPublicCredential(device.getPublicCredential())
                 .build();
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
index 3602787..67392ad 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
@@ -25,8 +25,8 @@
 import android.os.ParcelUuid;
 
 import com.android.server.nearby.injector.Injector;
-import com.android.server.nearby.presence.PresenceConstants;
 
+import java.util.UUID;
 import java.util.concurrent.Executor;
 
 /**
@@ -69,11 +69,13 @@
                                 .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
                                 .setConnectable(true)
                                 .build();
+
+                // TODO(b/230538655) Use empty data until Presence V1 protocol is implemented.
+                ParcelUuid emptyParcelUuid = new ParcelUuid(new UUID(0L, 0L));
+                byte[] emptyAdvertisementPackets = new byte[0];
                 AdvertiseData advertiseData =
                         new AdvertiseData.Builder()
-                                .addServiceData(new ParcelUuid(PresenceConstants.PRESENCE_UUID),
-                                        advertisementPackets).build();
-
+                                .addServiceData(emptyParcelUuid, emptyAdvertisementPackets).build();
                 try {
                     mBroadcastListener = listener;
                     bluetoothLeAdvertiser.startAdvertising(settings, advertiseData, this);
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
index a70ef13..f20c6d8 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
@@ -33,11 +33,11 @@
 
 import com.google.protobuf.InvalidProtocolBufferException;
 
-import service.proto.Blefilter;
-
 import java.util.Collections;
 import java.util.concurrent.Executor;
 
+import service.proto.Blefilter;
+
 /** Discovery provider that uses CHRE Nearby Nanoapp to do scanning. */
 public class ChreDiscoveryProvider extends AbstractDiscoveryProvider {
     // Nanoapp ID reserved for Nearby Presence.
diff --git a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
index 53d61c2..bdeab51 100644
--- a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
@@ -21,6 +21,7 @@
 import static com.android.server.nearby.NearbyService.TAG;
 
 import android.annotation.Nullable;
+import android.app.AppOpsManager;
 import android.content.Context;
 import android.nearby.IScanListener;
 import android.nearby.NearbyDeviceParcelable;
@@ -35,6 +36,8 @@
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.metrics.NearbyMetrics;
 import com.android.server.nearby.presence.PresenceDiscoveryResult;
+import com.android.server.nearby.util.identity.CallerIdentity;
+import com.android.server.nearby.util.permissions.DiscoveryPermissions;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -53,6 +56,7 @@
     private final BleDiscoveryProvider mBleDiscoveryProvider;
     @Nullable private final ChreDiscoveryProvider mChreDiscoveryProvider;
     private @ScanRequest.ScanMode int mScanMode;
+    private final Injector mInjector;
 
     @GuardedBy("mLock")
     private Map<IBinder, ScanListenerRecord> mScanTypeScanListenerRecordMap;
@@ -60,12 +64,26 @@
     @Override
     public void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice) {
         synchronized (mLock) {
+            AppOpsManager appOpsManager = Objects.requireNonNull(mInjector.getAppOpsManager());
             for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
                 ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
                 if (record == null) {
                     Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
                     continue;
                 }
+                CallerIdentity callerIdentity = record.getCallerIdentity();
+                if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
+                        appOpsManager, callerIdentity)) {
+                    Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+                            + "- not forwarding results");
+                    try {
+                        record.getScanListener().onError();
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+                    }
+                    return;
+                }
+
                 if (nearbyDevice.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
                     List<ScanFilter> presenceFilters =
                             record.getScanRequest().getScanFilters().stream()
@@ -103,13 +121,14 @@
                 new ChreDiscoveryProvider(
                         mContext, new ChreCommunication(injector, executor), executor);
         mScanTypeScanListenerRecordMap = new HashMap<>();
+        mInjector = injector;
     }
 
     /**
      * Registers the listener in the manager and starts scan according to the requested scan mode.
      */
-    public boolean registerScanListener(ScanRequest scanRequest, IScanListener listener) {
-        Log.i(TAG, "DiscoveryProviderManager registerScanListener");
+    public boolean registerScanListener(ScanRequest scanRequest, IScanListener listener,
+            CallerIdentity callerIdentity) {
         synchronized (mLock) {
             IBinder listenerBinder = listener.asBinder();
             if (mScanTypeScanListenerRecordMap.containsKey(listener.asBinder())) {
@@ -120,7 +139,8 @@
                     return true;
                 }
             }
-            ScanListenerRecord scanListenerRecord = new ScanListenerRecord(scanRequest, listener);
+            ScanListenerRecord scanListenerRecord =
+                    new ScanListenerRecord(scanRequest, listener, callerIdentity);
             mScanTypeScanListenerRecordMap.put(listenerBinder, scanListenerRecord);
 
             if (!startProviders(scanRequest)) {
@@ -273,9 +293,13 @@
 
         private final IScanListener mScanListener;
 
-        ScanListenerRecord(ScanRequest scanRequest, IScanListener iScanListener) {
+        private final CallerIdentity mCallerIdentity;
+
+        ScanListenerRecord(ScanRequest scanRequest, IScanListener iScanListener,
+                CallerIdentity callerIdentity) {
             mScanListener = iScanListener;
             mScanRequest = scanRequest;
+            mCallerIdentity = callerIdentity;
         }
 
         IScanListener getScanListener() {
@@ -286,6 +310,10 @@
             return mScanRequest;
         }
 
+        CallerIdentity getCallerIdentity() {
+            return mCallerIdentity;
+        }
+
         @Override
         public boolean equals(Object other) {
             if (other instanceof ScanListenerRecord) {
diff --git a/nearby/service/java/com/android/server/nearby/util/identity/CallerIdentity.java b/nearby/service/java/com/android/server/nearby/util/identity/CallerIdentity.java
new file mode 100644
index 0000000..b5c80b9
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/identity/CallerIdentity.java
@@ -0,0 +1,169 @@
+/*
+ * 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.util.identity;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Binder;
+import android.os.Process;
+
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Objects;
+
+/**
+ * Identifying information on a caller.
+ *
+ * @hide
+ */
+public final class CallerIdentity {
+
+    /**
+     * Creates a CallerIdentity from the current binder identity, using the given package, feature
+     * id, and listener id. The package will be checked to enforce it belongs to the calling uid,
+     * and a security exception will be thrown if it is invalid.
+     */
+    public static CallerIdentity fromBinder(Context context, String packageName,
+            @Nullable String attributionTag) {
+        int uid = Binder.getCallingUid();
+        if (!contains(context.getPackageManager().getPackagesForUid(uid), packageName)) {
+            throw new SecurityException("invalid package \"" + packageName + "\" for uid " + uid);
+        }
+        return fromBinderUnsafe(packageName, attributionTag);
+    }
+
+    /**
+     * Construct a CallerIdentity for test purposes.
+     */
+    @VisibleForTesting
+    public static CallerIdentity forTest(int uid, int pid, String packageName,
+            @Nullable String attributionTag) {
+        return new CallerIdentity(uid, pid, packageName, attributionTag);
+    }
+
+    /**
+     * Creates a CallerIdentity from the current binder identity, using the given package, feature
+     * id, and listener id. The package will not be checked to enforce that it belongs to the
+     * calling uid - this method should only be used if the package will be validated by some other
+     * means, such as an appops call.
+     */
+    public static CallerIdentity fromBinderUnsafe(String packageName,
+            @Nullable String attributionTag) {
+        return new CallerIdentity(Binder.getCallingUid(), Binder.getCallingPid(),
+                packageName, attributionTag);
+    }
+
+    private final int mUid;
+
+    private final int mPid;
+
+    private final String mPackageName;
+
+    private final @Nullable String mAttributionTag;
+
+
+    private CallerIdentity(int uid, int pid, String packageName,
+            @Nullable String attributionTag) {
+        this.mUid = uid;
+        this.mPid = pid;
+        this.mPackageName = Objects.requireNonNull(packageName);
+        this.mAttributionTag = attributionTag;
+    }
+
+    /** The calling UID. */
+    public int getUid() {
+        return mUid;
+    }
+
+    /** The calling PID. */
+    public int getPid() {
+        return mPid;
+    }
+
+    /** The calling package name. */
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /** The calling attribution tag. */
+    public String getAttributionTag() {
+        return mAttributionTag;
+    }
+
+    /** Returns true if this represents a system server identity. */
+    public boolean isSystemServer() {
+        return mUid == Process.SYSTEM_UID;
+    }
+
+    @Override
+    public String toString() {
+        int length = 10 + mPackageName.length();
+        if (mAttributionTag != null) {
+            length += mAttributionTag.length();
+        }
+
+        StringBuilder builder = new StringBuilder(length);
+        builder.append(mUid).append("/").append(mPackageName);
+        if (mAttributionTag != null) {
+            builder.append("[");
+            if (mAttributionTag.startsWith(mPackageName)) {
+                builder.append(mAttributionTag.substring(mPackageName.length()));
+            } else {
+                builder.append(mAttributionTag);
+            }
+            builder.append("]");
+        }
+        return builder.toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof CallerIdentity)) {
+            return false;
+        }
+        CallerIdentity that = (CallerIdentity) o;
+        return mUid == that.mUid
+                && mPid == that.mPid
+                && mPackageName.equals(that.mPackageName)
+                && Objects.equals(mAttributionTag, that.mAttributionTag);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mUid, mPid, mPackageName, mAttributionTag);
+    }
+
+    private static <T> boolean contains(@Nullable T[] array, T value) {
+        return indexOf(array, value) != -1;
+    }
+
+    /**
+     * Return first index of {@code value} in {@code array}, or {@code -1} if
+     * not found.
+     */
+    private static <T> int indexOf(@Nullable T[] array, T value) {
+        if (array == null) return -1;
+        for (int i = 0; i < array.length; i++) {
+            if (Objects.equals(array[i], value)) return i;
+        }
+        return -1;
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/permissions/BroadcastPermissions.java b/nearby/service/java/com/android/server/nearby/util/permissions/BroadcastPermissions.java
new file mode 100644
index 0000000..c11c234
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/permissions/BroadcastPermissions.java
@@ -0,0 +1,105 @@
+/*
+ * 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.util.permissions;
+
+import static android.Manifest.permission.BLUETOOTH_ADVERTISE;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import static java.lang.annotation.ElementType.TYPE_USE;
+
+import android.content.Context;
+
+import androidx.annotation.IntDef;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/** Utilities for handling presence broadcast runtime permissions. */
+public class BroadcastPermissions {
+
+    /** Indicates no permissions are present, or no permissions are required. */
+    public static final int PERMISSION_NONE = 0;
+
+    /** Indicates only the Bluetooth advertise permission is present, or is required. */
+    public static final int PERMISSION_BLUETOOTH_ADVERTISE = 1;
+
+    /** Broadcast permission levels. */
+    @IntDef({
+            PERMISSION_NONE,
+            PERMISSION_BLUETOOTH_ADVERTISE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @Target({TYPE_USE})
+    public @interface BroadcastPermissionLevel {}
+
+    /**
+     * Throws a security exception if the caller does not hold the required broadcast permissions.
+     */
+    public static void enforceBroadcastPermission(Context context, CallerIdentity callerIdentity) {
+        if (!checkCallerBroadcastPermission(context, callerIdentity)) {
+            throw new SecurityException("uid " + callerIdentity.getUid()
+                    + " does not have " + BLUETOOTH_ADVERTISE + ".");
+        }
+    }
+
+    /**
+     * Checks if the app has the permission to broadcast.
+     *
+     * @return true if the app does have the permission, false otherwise.
+     */
+    public static boolean checkCallerBroadcastPermission(Context context,
+            CallerIdentity callerIdentity) {
+        int uid = callerIdentity.getUid();
+        int pid = callerIdentity.getPid();
+
+        if (!checkBroadcastPermission(
+                getPermissionLevel(context, uid, pid), PERMISSION_BLUETOOTH_ADVERTISE)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /** Returns the permission level of the caller. */
+    @VisibleForTesting
+    @BroadcastPermissionLevel
+    public static int getPermissionLevel(
+            Context context, int uid, int pid) {
+        boolean isBluetoothAdvertiseGranted =
+                context.checkPermission(BLUETOOTH_ADVERTISE, pid, uid)
+                        == PERMISSION_GRANTED;
+        if (isBluetoothAdvertiseGranted) {
+            return PERMISSION_BLUETOOTH_ADVERTISE;
+        }
+
+        return PERMISSION_NONE;
+    }
+
+    /** Returns false if the given permission level does not meet the required permission level. */
+    private static boolean checkBroadcastPermission(
+            @BroadcastPermissionLevel int permissionLevel,
+            @BroadcastPermissionLevel int requiredPermissionLevel) {
+        return permissionLevel >= requiredPermissionLevel;
+    }
+
+    private BroadcastPermissions() {}
+}
+
diff --git a/nearby/service/java/com/android/server/nearby/util/permissions/DiscoveryPermissions.java b/nearby/service/java/com/android/server/nearby/util/permissions/DiscoveryPermissions.java
new file mode 100644
index 0000000..b0888ba
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/permissions/DiscoveryPermissions.java
@@ -0,0 +1,123 @@
+/*
+ * 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.util.permissions;
+
+import static android.Manifest.permission.BLUETOOTH_SCAN;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import static java.lang.annotation.ElementType.TYPE_USE;
+
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.content.Context;
+
+import androidx.annotation.IntDef;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/** Utilities for handling presence discovery runtime permissions. */
+public class DiscoveryPermissions {
+
+    /** Indicates no permissions are present, or no permissions are required. */
+    public static final int PERMISSION_NONE = 0;
+
+    /** Indicates only the Bluetooth scan permission is present, or is required. */
+    public static final int PERMISSION_BLUETOOTH_SCAN = 1;
+
+    // String in AppOpsManager
+    @VisibleForTesting
+    public static final String OPSTR_BLUETOOTH_SCAN = "android:bluetooth_scan";
+
+    /** Discovery permission levels. */
+    @IntDef({
+            PERMISSION_NONE,
+            PERMISSION_BLUETOOTH_SCAN
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @Target({TYPE_USE})
+    public @interface DiscoveryPermissionLevel {}
+
+    /**
+     * Throws a security exception if the caller does not hold the required scan permissions.
+     */
+    public static void enforceDiscoveryPermission(Context context, CallerIdentity callerIdentity) {
+        if (!checkCallerDiscoveryPermission(context, callerIdentity)) {
+            throw new SecurityException("uid " + callerIdentity.getUid() + " does not have "
+                    + BLUETOOTH_SCAN + ".");
+        }
+    }
+
+    /**
+     * Checks if the caller has the permission to scan.
+     */
+    public static boolean checkCallerDiscoveryPermission(Context context,
+            CallerIdentity callerIdentity) {
+        int uid = callerIdentity.getUid();
+        int pid = callerIdentity.getPid();
+
+        return checkDiscoveryPermission(
+                getPermissionLevel(context, uid, pid), PERMISSION_BLUETOOTH_SCAN);
+    }
+
+    /**
+     * Checks if the caller is allowed by AppOpsManager to scan.
+     */
+    public static boolean noteDiscoveryResultDelivery(AppOpsManager appOpsManager,
+            CallerIdentity callerIdentity) {
+        return noteAppOpAllowed(appOpsManager, callerIdentity, /* message= */ null);
+    }
+
+    private static boolean noteAppOpAllowed(AppOpsManager appOpsManager,
+            CallerIdentity identity, @Nullable String message) {
+        return appOpsManager.noteOp(asAppOp(PERMISSION_BLUETOOTH_SCAN),
+                identity.getUid(), identity.getPackageName(), identity.getAttributionTag(), message)
+                == AppOpsManager.MODE_ALLOWED;
+    }
+
+    /** Returns the permission level of the caller. */
+    public static @DiscoveryPermissionLevel int getPermissionLevel(
+            Context context, int uid, int pid) {
+        boolean isBluetoothScanGranted =
+                context.checkPermission(BLUETOOTH_SCAN, pid, uid) == PERMISSION_GRANTED;
+        if (isBluetoothScanGranted) {
+            return PERMISSION_BLUETOOTH_SCAN;
+        }
+        return PERMISSION_NONE;
+    }
+
+    /** Returns false if the given permission lev`el does not meet the required permission level. */
+    private static boolean checkDiscoveryPermission(
+            @DiscoveryPermissionLevel int permissionLevel,
+            @DiscoveryPermissionLevel int requiredPermissionLevel) {
+        return permissionLevel >= requiredPermissionLevel;
+    }
+
+    /** Returns the app op string according to the permission level. */
+    private static String asAppOp(@DiscoveryPermissionLevel int permissionLevel) {
+        if (permissionLevel == PERMISSION_BLUETOOTH_SCAN) {
+            return "android:bluetooth_scan";
+        }
+        throw new IllegalArgumentException();
+    }
+
+    private DiscoveryPermissions() {}
+}
diff --git a/nearby/tests/cts/fastpair/AndroidManifest.xml b/nearby/tests/cts/fastpair/AndroidManifest.xml
index ce841f2..96e2783 100644
--- a/nearby/tests/cts/fastpair/AndroidManifest.xml
+++ b/nearby/tests/cts/fastpair/AndroidManifest.xml
@@ -19,6 +19,8 @@
     package="android.nearby.cts">
   <uses-sdk android:minSdkVersion="32" android:targetSdkVersion="32" />
   <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+  <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
+  <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
   <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
 
   <application>
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 b9ab95f..6b9bce9 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
@@ -80,7 +80,7 @@
                         "NearbyDeviceParcelable[name=testDevice, medium=BLE, txPower=0, rssi=-60,"
                                 + " action=0, bluetoothAddress="
                                 + BLUETOOTH_ADDRESS
-                                + ", fastPairModelId=null, data=null]");
+                                + ", fastPairModelId=null, data=null, salt=null]");
     }
 
     @Test
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 9720865..6824ca6 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -16,6 +16,7 @@
 
 package android.nearby.cts;
 
+import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
 import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
 import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
@@ -23,6 +24,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
+
 import android.app.UiAutomation;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothManager;
@@ -51,6 +54,7 @@
 
 import java.util.Collections;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 
@@ -73,10 +77,32 @@
     private UiAutomation mUiAutomation =
             InstrumentationRegistry.getInstrumentation().getUiAutomation();
 
+    private ScanRequest mScanRequest = new ScanRequest.Builder()
+            .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR)
+            .setScanMode(ScanRequest.SCAN_MODE_LOW_LATENCY)
+            .setBleEnabled(true)
+            .build();
+    private  ScanCallback mScanCallback = new ScanCallback() {
+        @Override
+        public void onDiscovered(@NonNull NearbyDevice device) {
+        }
+
+        @Override
+        public void onUpdated(@NonNull NearbyDevice device) {
+        }
+
+        @Override
+        public void onLost(@NonNull NearbyDevice device) {
+        }
+    };
+    private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
+
     @Before
     public void setUp() {
-        mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG);
-        DeviceConfig.setProperty(NAMESPACE_TETHERING, "nearby_enable_presence_broadcast_legacy",
+        mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG,
+                BLUETOOTH_PRIVILEGED);
+        DeviceConfig.setProperty(NAMESPACE_TETHERING,
+                "nearby_enable_presence_broadcast_legacy",
                 "true", false);
 
         mContext = InstrumentationRegistry.getContext();
@@ -88,28 +114,16 @@
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void test_startAndStopScan() {
-        ScanRequest scanRequest = new ScanRequest.Builder()
-                .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR)
-                .setScanMode(ScanRequest.SCAN_MODE_LOW_LATENCY)
-                .setBleEnabled(true)
-                .build();
-        ScanCallback scanCallback = new ScanCallback() {
-            @Override
-            public void onDiscovered(@NonNull NearbyDevice device) {
-            }
+        mNearbyManager.startScan(mScanRequest, EXECUTOR, mScanCallback);
+        mNearbyManager.stopScan(mScanCallback);
+    }
 
-            @Override
-            public void onUpdated(@NonNull NearbyDevice device) {
-
-            }
-
-            @Override
-            public void onLost(@NonNull NearbyDevice device) {
-
-            }
-        };
-        mNearbyManager.startScan(scanRequest, Executors.newSingleThreadExecutor(), scanCallback);
-        mNearbyManager.stopScan(scanCallback);
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void test_startScan_noPrivilegedPermission() {
+        mUiAutomation.dropShellPermissionIdentity();
+        assertThrows(SecurityException.class, () -> mNearbyManager
+                .startScan(mScanRequest, EXECUTOR, mScanCallback));
     }
 
     @Test
diff --git a/nearby/tests/unit/AndroidManifest.xml b/nearby/tests/unit/AndroidManifest.xml
index 88c0f5f..9f58baf 100644
--- a/nearby/tests/unit/AndroidManifest.xml
+++ b/nearby/tests/unit/AndroidManifest.xml
@@ -22,6 +22,7 @@
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.BLUETOOTH" />
     <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
 
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
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 31965a4..e250254 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
@@ -16,10 +16,18 @@
 
 package com.android.server.nearby;
 
+import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
 import static android.Manifest.permission.READ_DEVICE_CONFIG;
 
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 import static org.mockito.MockitoAnnotations.initMocks;
 
+import android.app.AppOpsManager;
 import android.app.UiAutomation;
 import android.content.Context;
 import android.nearby.IScanListener;
@@ -27,12 +35,17 @@
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.util.permissions.DiscoveryPermissions;
+
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
 
 public final class NearbyServiceTest {
 
+    private static final String PACKAGE_NAME = "android.nearby.test";
     private Context mContext;
     private NearbyService mService;
     private ScanRequest mScanRequest;
@@ -41,19 +54,36 @@
 
     @Mock
     private IScanListener mScanListener;
+    @Mock
+    private AppOpsManager mMockAppOpsManager;
 
     @Before
-    public void setup() {
+    public void setUp()  {
         initMocks(this);
-        mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG);
+        mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, BLUETOOTH_PRIVILEGED);
         mContext = InstrumentationRegistry.getInstrumentation().getContext();
         mService = new NearbyService(mContext);
         mScanRequest = createScanRequest();
     }
 
+    @After
+    public void tearDown() {
+        mUiAutomation.dropShellPermissionIdentity();
+    }
+
     @Test
     public void test_register() {
-        mService.registerScanListener(mScanRequest, mScanListener);
+        setMockInjector(/* isMockOpsAllowed= */ true);
+        mService.registerScanListener(mScanRequest, mScanListener, PACKAGE_NAME,
+                /* attributionTag= */ null);
+    }
+
+    @Test
+    public void test_register_noPrivilegedPermission_throwsException() {
+        mUiAutomation.dropShellPermissionIdentity();
+        assertThrows(java.lang.SecurityException.class,
+                () -> mService.registerScanListener(mScanRequest, mScanListener, PACKAGE_NAME,
+                        /* attributionTag= */ null));
     }
 
     @Test
@@ -67,4 +97,14 @@
                 .setBleEnabled(true)
                 .build();
     }
+
+    private void setMockInjector(boolean isMockOpsAllowed) {
+        Injector injector = mock(Injector.class);
+        when(injector.getAppOpsManager()).thenReturn(mMockAppOpsManager);
+        when(mMockAppOpsManager.noteOp(eq(DiscoveryPermissions.OPSTR_BLUETOOTH_SCAN),
+                anyInt(), eq(PACKAGE_NAME), nullable(String.class), nullable(String.class)))
+                .thenReturn(isMockOpsAllowed
+                        ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_ERRORED);
+        mService.setInjector(injector);
+    }
 }
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
index d32e325..3b34655 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
@@ -16,9 +16,6 @@
 
 package com.android.server.nearby.presence;
 
-
-
-
 import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
@@ -26,7 +23,6 @@
 import org.mockito.MockitoAnnotations;
 
 public class PresenceManagerTest {
-    private PresenceManager mPresenceManager;
 
     @Before
     public void setup() {
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
index f485e18..d06a785 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
@@ -20,6 +20,7 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import android.app.AppOpsManager;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothManager;
 import android.bluetooth.le.AdvertiseSettings;
@@ -61,7 +62,6 @@
     public void testOnStatus_success() {
         byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
         mBleBroadcastProvider.start(advertiseBytes, mBroadcastListener);
-        verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
 
         AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
         mBleBroadcastProvider.onStartSuccess(settings);
@@ -74,7 +74,8 @@
         mBleBroadcastProvider.start(advertiseBytes, mBroadcastListener);
 
         mBleBroadcastProvider.onStartFailure(BroadcastCallback.STATUS_FAILURE);
-        verify(mBroadcastListener, times(2)).onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
+        verify(mBroadcastListener, times(1))
+                .onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
     }
 
     private static class TestInjector implements Injector {
@@ -90,5 +91,10 @@
         public ContextHubManagerAdapter getContextHubManagerAdapter() {
             return null;
         }
+
+        @Override
+        public AppOpsManager getAppOpsManager() {
+            return null;
+        }
     }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
index 8e97443..902cc33 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.MockitoAnnotations.initMocks;
 
+import android.app.AppOpsManager;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothManager;
@@ -87,6 +88,11 @@
         public ContextHubManagerAdapter getContextHubManagerAdapter() {
             return null;
         }
+
+        @Override
+        public AppOpsManager getAppOpsManager() {
+            return null;
+        }
     }
 
     private ScanResult createScanResult() {
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java
new file mode 100644
index 0000000..1a22412
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.util;
+
+import static android.Manifest.permission.BLUETOOTH_ADVERTISE;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import static com.android.server.nearby.util.permissions.BroadcastPermissions.PERMISSION_BLUETOOTH_ADVERTISE;
+import static com.android.server.nearby.util.permissions.BroadcastPermissions.PERMISSION_NONE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.Context;
+
+import com.android.server.nearby.util.identity.CallerIdentity;
+import com.android.server.nearby.util.permissions.BroadcastPermissions;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+/**
+ * Unit test for {@link BroadcastPermissions}
+ */
+public final class BroadcastPermissionsTest {
+
+    private static final String PACKAGE_NAME = "android.nearby.test";
+    private static final int UID = 1234;
+    private static final int PID = 5678;
+    private CallerIdentity mCallerIdentity;
+
+    @Mock private Context mMockContext;
+
+    @Before
+    public void setup() {
+        initMocks(this);
+        mCallerIdentity = CallerIdentity
+                .forTest(UID, PID, PACKAGE_NAME, /* attributionTag= */ null);
+    }
+
+    @Test
+    public void test_checkCallerBroadcastPermission_granted() {
+        when(mMockContext.checkPermission(BLUETOOTH_ADVERTISE, PID, UID))
+                .thenReturn(PERMISSION_GRANTED);
+
+        assertThat(BroadcastPermissions
+                .checkCallerBroadcastPermission(mMockContext, mCallerIdentity))
+                .isTrue();
+    }
+
+    @Test
+    public void test_checkCallerBroadcastPermission_deniedPermission() {
+        when(mMockContext.checkPermission(BLUETOOTH_ADVERTISE, PID, UID))
+                .thenReturn(PERMISSION_DENIED);
+
+        assertThat(BroadcastPermissions
+                .checkCallerBroadcastPermission(mMockContext, mCallerIdentity))
+                .isFalse();
+    }
+
+    @Test
+    public void test_getPermissionLevel_none() {
+        when(mMockContext.checkPermission(BLUETOOTH_ADVERTISE, PID, UID))
+                .thenReturn(PERMISSION_DENIED);
+
+        assertThat(BroadcastPermissions.getPermissionLevel(mMockContext, UID, PID))
+                .isEqualTo(PERMISSION_NONE);
+    }
+
+    @Test
+    public void test_getPermissionLevel_advertising() {
+        when(mMockContext.checkPermission(BLUETOOTH_ADVERTISE, PID, UID))
+                .thenReturn(PERMISSION_GRANTED);
+
+        assertThat(BroadcastPermissions.getPermissionLevel(mMockContext, UID, PID))
+                .isEqualTo(PERMISSION_BLUETOOTH_ADVERTISE);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/DiscoveryPermissionsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/DiscoveryPermissionsTest.java
new file mode 100644
index 0000000..d953a60
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/DiscoveryPermissionsTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.util;
+
+import static android.Manifest.permission.BLUETOOTH_SCAN;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import static com.android.server.nearby.util.permissions.DiscoveryPermissions.PERMISSION_BLUETOOTH_SCAN;
+import static com.android.server.nearby.util.permissions.DiscoveryPermissions.PERMISSION_NONE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+
+import com.android.server.nearby.util.identity.CallerIdentity;
+import com.android.server.nearby.util.permissions.DiscoveryPermissions;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+/**
+ * Unit test for {@link DiscoveryPermissions}
+ */
+public final class DiscoveryPermissionsTest {
+
+    private static final String PACKAGE_NAME = "android.nearby.test";
+    private static final int UID = 1234;
+    private static final int PID = 5678;
+    private CallerIdentity mCallerIdentity;
+
+    @Mock
+    private Context mMockContext;
+    @Mock private AppOpsManager mMockAppOps;
+
+    @Before
+    public void setup() {
+        initMocks(this);
+        mCallerIdentity = CallerIdentity
+                .forTest(UID, PID, PACKAGE_NAME, /* attributionTag= */ null);
+    }
+
+    @Test
+    public void test_enforceCallerDiscoveryPermission_exception() {
+        when(mMockContext.checkPermission(BLUETOOTH_SCAN, PID, UID)).thenReturn(PERMISSION_DENIED);
+
+        assertThrows(SecurityException.class,
+                () -> DiscoveryPermissions
+                        .enforceDiscoveryPermission(mMockContext, mCallerIdentity));
+    }
+
+    @Test
+    public void test_checkCallerDiscoveryPermission_granted() {
+        when(mMockContext.checkPermission(BLUETOOTH_SCAN, PID, UID)).thenReturn(PERMISSION_GRANTED);
+
+        assertThat(DiscoveryPermissions
+                .checkCallerDiscoveryPermission(mMockContext, mCallerIdentity))
+                .isTrue();
+    }
+
+    @Test
+    public void test_checkCallerDiscoveryPermission_denied() {
+        when(mMockContext.checkPermission(BLUETOOTH_SCAN, PID, UID)).thenReturn(PERMISSION_DENIED);
+
+        assertThat(DiscoveryPermissions
+                .checkCallerDiscoveryPermission(mMockContext, mCallerIdentity))
+                .isFalse();
+    }
+
+    @Test
+    public void test_checkNoteOpPermission_granted() {
+        when(mMockAppOps.noteOp(DiscoveryPermissions.OPSTR_BLUETOOTH_SCAN, UID, PACKAGE_NAME,
+                null, null)).thenReturn(AppOpsManager.MODE_ALLOWED);
+
+        assertThat(DiscoveryPermissions
+                .noteDiscoveryResultDelivery(mMockAppOps, mCallerIdentity))
+                .isTrue();
+    }
+
+    @Test
+    public void test_checkNoteOpPermission_denied() {
+        when(mMockAppOps.noteOp(DiscoveryPermissions.OPSTR_BLUETOOTH_SCAN, UID, PACKAGE_NAME,
+                null, null)).thenReturn(AppOpsManager.MODE_ERRORED);
+
+        assertThat(DiscoveryPermissions
+                .noteDiscoveryResultDelivery(mMockAppOps, mCallerIdentity))
+                .isFalse();
+    }
+
+    @Test
+    public void test_getPermissionLevel_none() {
+        when(mMockContext.checkPermission(BLUETOOTH_SCAN, PID, UID)).thenReturn(PERMISSION_DENIED);
+
+        assertThat(DiscoveryPermissions
+                .getPermissionLevel(mMockContext, UID, PID))
+                .isEqualTo(PERMISSION_NONE);
+    }
+
+    @Test
+    public void test_getPermissionLevel_scan() {
+        when(mMockContext.checkPermission(BLUETOOTH_SCAN, PID, UID))
+                .thenReturn(PERMISSION_GRANTED);
+
+        assertThat(DiscoveryPermissions
+                .getPermissionLevel(mMockContext, UID, PID)).isEqualTo(PERMISSION_BLUETOOTH_SCAN);
+    }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index eb7d1ea..e4a9ebe 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -916,7 +916,7 @@
         final Intent intent = new Intent();
         if (type == TYPE_COMPONENT_ACTIVTIY) {
             intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_ACTIVITY_CLASS))
-                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
         } else if (type == TYPE_COMPONENT_FOREGROUND_SERVICE) {
             intent.setComponent(new ComponentName(TEST_APP2_PKG, TEST_APP2_SERVICE_CLASS))
                     .setFlags(1);
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
new file mode 100644
index 0000000..098f295
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ConnOnActivityStartTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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;
+
+
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getUiDevice;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
+import static com.android.cts.net.hostside.Property.APP_STANDBY_MODE;
+import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
+import static com.android.cts.net.hostside.Property.DOZE_MODE;
+import static com.android.cts.net.hostside.Property.METERED_NETWORK;
+import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
+
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+@RequiredProperties({NON_METERED_NETWORK})
+public class ConnOnActivityStartTest extends AbstractRestrictBackgroundNetworkTestCase {
+    private static final int TEST_ITERATION_COUNT = 5;
+
+    @Before
+    public final void setUp() throws Exception {
+        super.setUp();
+        resetDeviceState();
+    }
+
+    @After
+    public final void tearDown() throws Exception {
+        super.tearDown();
+        resetDeviceState();
+    }
+
+    private void resetDeviceState() throws Exception {
+        resetBatteryState();
+        setBatterySaverMode(false);
+        setRestrictBackground(false);
+        setAppIdle(false);
+        setDozeMode(false);
+    }
+
+
+    @Test
+    @RequiredProperties({BATTERY_SAVER_MODE})
+    public void testStartActivity_batterySaver() throws Exception {
+        setBatterySaverMode(true);
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_batterySaver");
+    }
+
+    @Test
+    @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK})
+    public void testStartActivity_dataSaver() throws Exception {
+        setRestrictBackground(true);
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_dataSaver");
+    }
+
+    @Test
+    @RequiredProperties({DOZE_MODE})
+    public void testStartActivity_doze() throws Exception {
+        setDozeMode(true);
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_doze");
+    }
+
+    @Test
+    @RequiredProperties({APP_STANDBY_MODE})
+    public void testStartActivity_appStandby() throws Exception {
+        turnBatteryOn();
+        setAppIdle(true);
+        assertLaunchedActivityHasNetworkAccess("testStartActivity_appStandby");
+    }
+
+    private void assertLaunchedActivityHasNetworkAccess(String testName) throws Exception {
+        for (int i = 0; i < TEST_ITERATION_COUNT; ++i) {
+            Log.i(TAG, testName + " start #" + i);
+            launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+            getUiDevice().pressHome();
+            assertBackgroundState();
+            Log.i(TAG, testName + " end #" + i);
+        }
+    }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
index 0a0f24b..7842eec 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java
@@ -57,6 +57,7 @@
 import android.util.Log;
 
 import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.UiDevice;
 
 import com.android.compatibility.common.util.AppStandbyUtils;
 import com.android.compatibility.common.util.BatteryUtils;
@@ -438,6 +439,10 @@
         return InstrumentationRegistry.getInstrumentation();
     }
 
+    public static UiDevice getUiDevice() {
+        return UiDevice.getInstance(getInstrumentation());
+    }
+
     // When power saver mode or restrict background enabled or adding any white/black list into
     // those modes, NetworkPolicy may need to take some time to update the rules of uids. So having
     // this function and using PollingCheck to try to make sure the uid has updated and reduce the
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
index eb7dca7..a337fe2 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java
@@ -39,6 +39,33 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         Log.d(TAG, "MyActivity.onCreate()");
+    }
+
+    @Override
+    public void finish() {
+        if (finishCommandReceiver != null) {
+            unregisterReceiver(finishCommandReceiver);
+        }
+        super.finish();
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        Log.d(TAG, "MyActivity.onStart()");
+    }
+
+    @Override
+    protected void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+        Log.d(TAG, "MyActivity.onNewIntent()");
+        setIntent(intent);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        Log.d(TAG, "MyActivity.onResume(): " + getIntent());
         Common.notifyNetworkStateObserver(this, getIntent(), TYPE_COMPONENT_ACTIVTY);
         finishCommandReceiver = new BroadcastReceiver() {
             @Override
@@ -57,20 +84,6 @@
     }
 
     @Override
-    public void finish() {
-        if (finishCommandReceiver != null) {
-            unregisterReceiver(finishCommandReceiver);
-        }
-        super.finish();
-    }
-
-    @Override
-    protected void onStart() {
-        super.onStart();
-        Log.d(TAG, "MyActivity.onStart()");
-    }
-
-    @Override
     protected void onDestroy() {
         Log.d(TAG, "MyActivity.onDestroy()");
         super.onDestroy();
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
new file mode 100644
index 0000000..3387fd7
--- /dev/null
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideConnOnActivityStartTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+public class HostsideConnOnActivityStartTest extends HostsideNetworkTestCase {
+    private static final String TEST_CLASS = TEST_PKG + ".ConnOnActivityStartTest";
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        uninstallPackage(TEST_APP2_PKG, false);
+        installPackage(TEST_APP2_APK);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+
+        uninstallPackage(TEST_APP2_PKG, true);
+    }
+
+    public void testStartActivity_batterySaver() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_batterySaver");
+    }
+
+    public void testStartActivity_dataSaver() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_dataSaver");
+    }
+
+    public void testStartActivity_doze() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_doze");
+    }
+
+    public void testStartActivity_appStandby() throws Exception {
+        runDeviceTests(TEST_PKG, TEST_CLASS, "testStartActivity_appStandby");
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/DnsResolverTest.java b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
index c6fc38f..0c53411 100644
--- a/tests/cts/net/src/android/net/cts/DnsResolverTest.java
+++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java
@@ -49,6 +49,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.platform.test.annotations.AppModeFull;
+import android.provider.Settings;
 import android.system.ErrnoException;
 import android.util.Log;
 
@@ -727,6 +728,18 @@
 
     @Test
     public void testPrivateDnsBypass() throws InterruptedException {
+        final String dataStallSetting = Settings.Global.getString(mCR,
+                Settings.Global.DATA_STALL_RECOVERY_ON_BAD_NETWORK);
+        Settings.Global.putInt(mCR, Settings.Global.DATA_STALL_RECOVERY_ON_BAD_NETWORK, 0);
+        try {
+            doTestPrivateDnsBypass();
+        } finally {
+            Settings.Global.putString(mCR, Settings.Global.DATA_STALL_RECOVERY_ON_BAD_NETWORK,
+                    dataStallSetting);
+        }
+    }
+
+    private void doTestPrivateDnsBypass() throws InterruptedException {
         final Network[] testNetworks = getTestableNetworks();
 
         // Set an invalid private DNS server