Merge "Implement Presence Broadcast." into tm-dev
diff --git a/nearby/framework/java/android/nearby/BroadcastRequest.aidl b/nearby/framework/java/android/nearby/BroadcastRequest.aidl
new file mode 100644
index 0000000..53f7d42
--- /dev/null
+++ b/nearby/framework/java/android/nearby/BroadcastRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+parcelable BroadcastRequest;
diff --git a/nearby/framework/java/android/nearby/IBroadcastListener.aidl b/nearby/framework/java/android/nearby/IBroadcastListener.aidl
new file mode 100644
index 0000000..98c7e17
--- /dev/null
+++ b/nearby/framework/java/android/nearby/IBroadcastListener.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+/**
+ * Callback when brodacast status changes.
+ *
+ * {@hide}
+ */
+oneway interface IBroadcastListener {
+    /** Called when the broadcast status changes. */
+    void onStatusChanged(int status);
+}
diff --git a/nearby/framework/java/android/nearby/INearbyManager.aidl b/nearby/framework/java/android/nearby/INearbyManager.aidl
index 4fff563..91dd485 100644
--- a/nearby/framework/java/android/nearby/INearbyManager.aidl
+++ b/nearby/framework/java/android/nearby/INearbyManager.aidl
@@ -16,7 +16,9 @@
 
 package android.nearby;
 
+import android.nearby.IBroadcastListener;
 import android.nearby.IScanListener;
+import android.nearby.BroadcastRequest;
 import android.nearby.ScanRequest;
 
 /**
@@ -29,4 +31,8 @@
     void registerScanListener(in ScanRequest scanRequest, in IScanListener listener);
 
     void unregisterScanListener(in IScanListener listener);
+
+    void startBroadcast(in BroadcastRequest broadcastRequest, in IBroadcastListener callback);
+
+    void stopBroadcast(in IBroadcastListener callback);
 }
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index a217677..211ec34 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -59,6 +59,10 @@
     @GuardedBy("sScanListeners")
     private static final WeakHashMap<ScanCallback, WeakReference<ScanListenerTransport>>
             sScanListeners = new WeakHashMap<>();
+    @GuardedBy("sBroadcastListeners")
+    private static final WeakHashMap<BroadcastCallback, WeakReference<BroadcastListenerTransport>>
+            sBroadcastListeners = new WeakHashMap<>();
+
     private final INearbyManager mService;
 
     /**
@@ -157,7 +161,23 @@
      */
     public void startBroadcast(@NonNull BroadcastRequest broadcastRequest,
             @CallbackExecutor @NonNull Executor executor, @NonNull BroadcastCallback callback) {
-        // TODO(b/218187205): implement broadcast.
+        try {
+            synchronized (sBroadcastListeners) {
+                WeakReference<BroadcastListenerTransport> reference = sBroadcastListeners.get(
+                        callback);
+                BroadcastListenerTransport transport = reference != null ? reference.get() : null;
+                if (transport == null) {
+                    transport = new BroadcastListenerTransport(callback, executor);
+                } else {
+                    Preconditions.checkState(transport.isRegistered());
+                    transport.setExecutor(executor);
+                }
+                mService.startBroadcast(broadcastRequest, transport);
+                sBroadcastListeners.put(callback, new WeakReference<>(transport));
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
@@ -167,7 +187,19 @@
      */
     @SuppressLint("ExecutorRegistration")
     public void stopBroadcast(@NonNull BroadcastCallback callback) {
-        // TODO(b/218187205): implement broadcast.
+        try {
+            synchronized (sBroadcastListeners) {
+                WeakReference<BroadcastListenerTransport> reference = sBroadcastListeners.remove(
+                        callback);
+                BroadcastListenerTransport transport = reference != null ? reference.get() : null;
+                if (transport != null) {
+                    transport.unregister();
+                    mService.stopBroadcast(transport);
+                }
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
@@ -248,4 +280,34 @@
                             toClientNearbyDevice(nearbyDeviceParcelable, mScanType)));
         }
     }
+
+    private static class BroadcastListenerTransport extends IBroadcastListener.Stub {
+        private volatile @Nullable BroadcastCallback mBroadcastCallback;
+        private Executor mExecutor;
+
+        BroadcastListenerTransport(BroadcastCallback broadcastCallback,
+                @CallbackExecutor Executor executor) {
+            mBroadcastCallback = broadcastCallback;
+            mExecutor = executor;
+        }
+
+        void setExecutor(Executor executor) {
+            Preconditions.checkArgument(
+                    executor != null, "invalid null executor");
+            mExecutor = executor;
+        }
+
+        boolean isRegistered() {
+            return mBroadcastCallback != null;
+        }
+
+        void unregister() {
+            mBroadcastCallback = null;
+        }
+
+        @Override
+        public void onStatusChanged(int status) {
+            mExecutor.execute(()-> mBroadcastCallback.onStatus(status));
+        }
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
new file mode 100644
index 0000000..8fdac87
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+import android.provider.DeviceConfig;
+
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * A utility class for encapsulating Nearby feature flag configurations.
+ */
+public class NearbyConfiguration {
+
+    /**
+     * Flag use to enable presence legacy broadcast.
+     */
+    public static final String NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY =
+            "nearby_enable_presence_broadcast_legacy";
+
+    private boolean mEnablePresenceBroadcastLegacy;
+
+    public NearbyConfiguration() {
+        mEnablePresenceBroadcastLegacy = getDeviceConfigBoolean(
+                NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, false /* defaultValue */);
+
+    }
+
+    /**
+     * Returns whether broadcasting legacy presence spec is enabled.
+     */
+    public boolean isPresenceBroadcastLegacyEnabled() {
+        return mEnablePresenceBroadcastLegacy;
+    }
+
+    private boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
+        final String value = getDeviceConfigProperty(name);
+        return value != null ? Boolean.parseBoolean(value) : defaultValue;
+    }
+
+    @VisibleForTesting
+    protected String getDeviceConfigProperty(String name) {
+        return DeviceConfig.getProperty(DeviceConfig.NAMESPACE_TETHERING, name);
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index 6d149fc..74b327a 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -27,6 +27,8 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.hardware.location.ContextHubManager;
+import android.nearby.BroadcastRequest;
+import android.nearby.IBroadcastListener;
 import android.nearby.INearbyManager;
 import android.nearby.IScanListener;
 import android.nearby.ScanRequest;
@@ -38,6 +40,7 @@
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.presence.ChreCommunication;
 import com.android.server.nearby.presence.PresenceManager;
+import com.android.server.nearby.provider.BroadcastProviderManager;
 import com.android.server.nearby.provider.DiscoveryProviderManager;
 import com.android.server.nearby.provider.FastPairDataProvider;
 
@@ -71,11 +74,13 @@
                 }
             };
     private DiscoveryProviderManager mProviderManager;
+    private BroadcastProviderManager mBroadcastProviderManager;
 
     public NearbyService(Context context) {
         mContext = context;
         mSystemInjector = new SystemInjector(context);
         mProviderManager = new DiscoveryProviderManager(context, mSystemInjector);
+        mBroadcastProviderManager = new BroadcastProviderManager(context, mSystemInjector);
         final LocatorContextWrapper lcw = new LocatorContextWrapper(context, null);
         mFastPairManager = new FastPairManager(lcw);
         mPresenceManager =
@@ -103,6 +108,16 @@
         mProviderManager.unregisterScanListener(listener);
     }
 
+    @Override
+    public void startBroadcast(BroadcastRequest broadcastRequest, IBroadcastListener listener) {
+        mBroadcastProviderManager.startBroadcast(broadcastRequest, listener);
+    }
+
+    @Override
+    public void stopBroadcast(IBroadcastListener listener) {
+        mBroadcastProviderManager.stopBroadcast(listener);
+    }
+
     /**
      * Called by the service initializer.
      *
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
new file mode 100644
index 0000000..40fc46f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
@@ -0,0 +1,114 @@
+/*
+ * 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.provider;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.le.AdvertiseCallback;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.BluetoothLeAdvertiser;
+import android.nearby.BroadcastCallback;
+import android.os.ParcelUuid;
+
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.presence.PresenceConstants;
+
+/**
+ * A provider for Bluetooth Low Energy advertisement.
+ */
+public class BleBroadcastProvider extends AdvertiseCallback {
+
+    /**
+     * Listener for Broadcast status changes.
+     */
+    interface BroadcastListener {
+        void onStatusChanged(int status);
+    }
+
+    private final Injector mInjector;
+    private BroadcastListener mBroadcastListener;
+    private boolean mIsAdvertising;
+
+    BleBroadcastProvider(Injector injector) {
+        mInjector = injector;
+    }
+
+    void start(byte[] advertisementPackets, BroadcastListener listener) {
+
+        if (mIsAdvertising) {
+            stop();
+        }
+        boolean advertiseStarted = false;
+        BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+        if (adapter != null) {
+            BluetoothLeAdvertiser bluetoothLeAdvertiser =
+                    mInjector.getBluetoothAdapter().getBluetoothLeAdvertiser();
+            if (bluetoothLeAdvertiser != null) {
+                advertiseStarted = true;
+                AdvertiseSettings settings =
+                        new AdvertiseSettings.Builder()
+                                .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
+                                .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
+                                .setConnectable(true)
+                                .build();
+                AdvertiseData advertiseData =
+                        new AdvertiseData.Builder()
+                                .addServiceData(new ParcelUuid(PresenceConstants.PRESENCE_UUID),
+                                        advertisementPackets).build();
+
+                try {
+                    mBroadcastListener = listener;
+                    bluetoothLeAdvertiser.startAdvertising(settings, advertiseData, this);
+                } catch (NullPointerException | IllegalStateException | SecurityException e) {
+                    advertiseStarted = false;
+                }
+            }
+        }
+        if (!advertiseStarted) {
+            listener.onStatusChanged(BroadcastCallback.STATUS_FAILURE);
+        }
+    }
+
+    void stop() {
+        if (mIsAdvertising) {
+            BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+            if (adapter != null) {
+                BluetoothLeAdvertiser bluetoothLeAdvertiser =
+                        mInjector.getBluetoothAdapter().getBluetoothLeAdvertiser();
+                if (bluetoothLeAdvertiser != null) {
+                    bluetoothLeAdvertiser.stopAdvertising(this);
+                }
+            }
+            mBroadcastListener = null;
+            mIsAdvertising = false;
+        }
+    }
+
+    @Override
+    public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+        if (mBroadcastListener != null) {
+            mBroadcastListener.onStatusChanged(BroadcastCallback.STATUS_OK);
+        }
+    }
+
+    @Override
+    public void onStartFailure(int errorCode) {
+        if (mBroadcastListener != null) {
+            mBroadcastListener.onStatusChanged(BroadcastCallback.STATUS_FAILURE);
+        }
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java b/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java
new file mode 100644
index 0000000..6a0f637
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java
@@ -0,0 +1,124 @@
+/*
+ * 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.provider;
+
+import android.content.Context;
+import android.nearby.BroadcastCallback;
+import android.nearby.BroadcastRequest;
+import android.nearby.IBroadcastListener;
+import android.nearby.PresenceBroadcastRequest;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.presence.FastAdvertisement;
+import com.android.server.nearby.util.ForegroundThread;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A manager for nearby broadcasts.
+ */
+public class BroadcastProviderManager implements BleBroadcastProvider.BroadcastListener {
+
+    private static final String TAG = "BroadcastProvider";
+
+    private final Object mLock;
+    private final BleBroadcastProvider mBleBroadcastProvider;
+    private final Executor mExecutor;
+    private final NearbyConfiguration mNearbyConfiguration;
+
+    private IBroadcastListener mBroadcastListener;
+
+    public BroadcastProviderManager(Context context, Injector injector) {
+        this(ForegroundThread.getExecutor(), new BleBroadcastProvider(injector));
+    }
+
+    @VisibleForTesting
+    BroadcastProviderManager(Executor executor, BleBroadcastProvider bleBroadcastProvider) {
+        mExecutor = executor;
+        mBleBroadcastProvider = bleBroadcastProvider;
+        mLock = new Object();
+        mNearbyConfiguration = new NearbyConfiguration();
+        mBroadcastListener = null;
+    }
+
+    /**
+     * Starts a nearby broadcast, the callback is sent through the given listener.
+     */
+    public void startBroadcast(BroadcastRequest broadcastRequest, IBroadcastListener listener) {
+        synchronized (mLock) {
+            if (!mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
+                reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+                return;
+            }
+            if (broadcastRequest.getType() != BroadcastRequest.BROADCAST_TYPE_NEARBY_PRESENCE) {
+                reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+                return;
+            }
+            PresenceBroadcastRequest presenceBroadcastRequest =
+                    (PresenceBroadcastRequest) broadcastRequest;
+            if (presenceBroadcastRequest.getVersion() != BroadcastRequest.PRESENCE_VERSION_V0) {
+                reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+                return;
+            }
+            FastAdvertisement fastAdvertisement = FastAdvertisement.createFromRequest(
+                    presenceBroadcastRequest);
+            byte[] advertisementPackets = fastAdvertisement.toBytes();
+            mBroadcastListener = listener;
+            mExecutor.execute(() -> {
+                mBleBroadcastProvider.start(advertisementPackets, this);
+            });
+        }
+    }
+
+    /**
+     * Stops the nearby broadcast.
+     */
+    public void stopBroadcast(IBroadcastListener listener) {
+        synchronized (mLock) {
+            if (!mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
+                reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+                return;
+            }
+            mBroadcastListener = null;
+            mExecutor.execute(() -> mBleBroadcastProvider.stop());
+        }
+    }
+
+    @Override
+    public void onStatusChanged(int status) {
+        IBroadcastListener listener = null;
+        synchronized (mLock) {
+            listener = mBroadcastListener;
+        }
+        // Don't invoke callback while holding the local lock, as this could cause deadlock.
+        if (listener != null) {
+            reportBroadcastStatus(listener, status);
+        }
+    }
+
+    private void reportBroadcastStatus(IBroadcastListener listener, int status) {
+        try {
+            listener.onStatusChanged(status);
+        } catch (RemoteException exception) {
+            Log.e(TAG, "remote exception when reporting status");
+        }
+    }
+}
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 c20cf22..faa3016 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/NearbyServiceTest.java
@@ -16,8 +16,11 @@
 
 package com.android.server.nearby;
 
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+
 import static org.mockito.MockitoAnnotations.initMocks;
 
+import android.app.UiAutomation;
 import android.content.Context;
 import android.nearby.IScanListener;
 import android.nearby.ScanRequest;
@@ -33,13 +36,16 @@
     private Context mContext;
     private NearbyService mService;
     private ScanRequest mScanRequest;
+    private UiAutomation mUiAutomation =
+            InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
     @Mock
     private IScanListener mScanListener;
 
     @Before
     public void setup() {
         initMocks(this);
-
+        mUiAutomation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG);
         mContext = InstrumentationRegistry.getInstrumentation().getContext();
         mService = new NearbyService(mContext);
         mScanRequest = createScanRequest();
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
new file mode 100644
index 0000000..3467407
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.provider;
+
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.le.AdvertiseSettings;
+import android.content.Context;
+import android.nearby.BroadcastCallback;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.nearby.injector.ContextHubManagerAdapter;
+import com.android.server.nearby.injector.Injector;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/**
+ * Unit test for {@link BleBroadcastProvider}.
+ */
+public class BleBroadcastProviderTest {
+    @Rule
+    public final MockitoRule mocks = MockitoJUnit.rule();
+
+    @Mock
+    private BleBroadcastProvider.BroadcastListener mBroadcastListener;
+    private BleBroadcastProvider mBleBroadcastProvider;
+
+    @Before
+    public void setUp() {
+        mBleBroadcastProvider = new BleBroadcastProvider(new TestInjector());
+    }
+
+    @Test
+    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);
+        verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+    }
+
+    @Test
+    public void testOnStatus_failure() {
+        byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
+        mBleBroadcastProvider.start(advertiseBytes, mBroadcastListener);
+
+        mBleBroadcastProvider.onStartFailure(BroadcastCallback.STATUS_FAILURE);
+        verify(mBroadcastListener, times(2)).onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
+    }
+
+    private static class TestInjector implements Injector {
+
+        @Override
+        public BluetoothAdapter getBluetoothAdapter() {
+            Context context = ApplicationProvider.getApplicationContext();
+            BluetoothManager bluetoothManager = context.getSystemService(BluetoothManager.class);
+            return bluetoothManager.getAdapter();
+        }
+
+        @Override
+        public ContextHubManagerAdapter getContextHubManagerAdapter() {
+            return null;
+        }
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
new file mode 100644
index 0000000..7f2168e
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.provider;
+
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.nearby.BroadcastCallback;
+import android.nearby.BroadcastRequest;
+import android.nearby.IBroadcastListener;
+import android.nearby.PresenceBroadcastRequest;
+import android.nearby.PresenceCredential;
+import android.nearby.PrivateCredential;
+import android.provider.DeviceConfig;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.Collections;
+
+/**
+ * Unit test for {@link BroadcastProviderManager}.
+ */
+public class BroadcastProviderManagerTest {
+    private static final byte[] IDENTITY = new byte[]{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
+    private static final int MEDIUM_TYPE_BLE = 0;
+    private static final byte[] SALT = {2, 3};
+    private static final byte TX_POWER = 4;
+    private static final int PRESENCE_ACTION = 123;
+    private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
+    private static final byte[] AUTHENTICITY_KEY = new byte[]{12, 13, 14};
+
+    @Rule
+    public final MockitoRule mocks = MockitoJUnit.rule();
+
+    @Mock
+    IBroadcastListener mBroadcastListener;
+    @Mock
+    BleBroadcastProvider mBleBroadcastProvider;
+    private Context mContext;
+    private BroadcastProviderManager mBroadcastProviderManager;
+    private BroadcastRequest mBroadcastRequest;
+    private UiAutomation mUiAutomation =
+            InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
+    @Before
+    public void setUp() {
+        mUiAutomation.adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
+        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+                "true", false);
+
+        mContext = ApplicationProvider.getApplicationContext();
+        mBroadcastProviderManager = new BroadcastProviderManager(MoreExecutors.directExecutor(),
+                mBleBroadcastProvider);
+
+        PrivateCredential privateCredential =
+                new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY)
+                        .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+                        .setMetadataEncryptionKey(IDENTITY)
+                        .build();
+        mBroadcastRequest =
+                new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+                        SALT)
+                        .setTxPower(TX_POWER)
+                        .setCredential(privateCredential)
+                        .setVersion(BroadcastRequest.PRESENCE_VERSION_V0)
+                        .addAction(PRESENCE_ACTION).build();
+    }
+
+    @Test
+    public void testStartAdvertising() {
+        mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
+        verify(mBleBroadcastProvider).start(any(byte[].class), any(
+                BleBroadcastProvider.BroadcastListener.class));
+    }
+
+    @Test
+    public void testStartAdvertising_featureDisabled() throws Exception {
+        DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
+                "false", false);
+        mBroadcastProviderManager = new BroadcastProviderManager(MoreExecutors.directExecutor(),
+                mBleBroadcastProvider);
+        mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
+        verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
+    }
+
+    @Test
+    public void testOnStatusChanged() throws Exception {
+        mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
+        mBroadcastProviderManager.onStatusChanged(BroadcastCallback.STATUS_OK);
+        verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+    }
+}