Add provider code
Test: Successfully built. Unit test will be added.
Bug: 189954300
Change-Id: I48ffc0d2980dc3a1bae7df49e95c59623eb957d1
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 926df8c..d221722 100644
--- a/nearby/service/java/com/android/server/nearby/injector/Injector.java
+++ b/nearby/service/java/com/android/server/nearby/injector/Injector.java
@@ -21,7 +21,6 @@
/**
* Nearby dependency injector. To be used for accessing various Nearby class instances and as a
* handle for mock injection.
- *
*/
public interface Injector {
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
new file mode 100644
index 0000000..ae7f133
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2021 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 com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.content.Context;
+import android.nearby.NearbyDevice;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.ScanRequest;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import com.android.server.nearby.common.bluetooth.fastpair.Constants;
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.util.ForegroundThread;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Discovery provider that uses Bluetooth Low Energy to do scanning.
+ */
+public class BleDiscoveryProvider extends AbstractDiscoveryProvider {
+
+ private static final ParcelUuid FAST_PAIR_UUID = new ParcelUuid(Constants.FastPairService.ID);
+ // Don't block the thread as it may be used by other services.
+ private static final Executor NEARBY_EXECUTOR = ForegroundThread.getExecutor();
+ private final Injector mInjector;
+ private android.bluetooth.le.ScanCallback mScanCallback =
+ new android.bluetooth.le.ScanCallback() {
+ @Override
+ public void onScanResult(int callbackType, ScanResult scanResult) {
+ NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder();
+ builder.setMedium(NearbyDevice.Medium.BLE)
+ .setRssi(scanResult.getRssi())
+ .setBluetoothAddress(scanResult.getDevice().getAddress());
+
+ ScanRecord record = scanResult.getScanRecord();
+ if (record != null) {
+ String deviceName = record.getDeviceName();
+ if (deviceName != null) {
+ builder.setName(record.getDeviceName());
+ }
+ Map<ParcelUuid, byte[]> serviceDataMap = record.getServiceData();
+ byte[] fastPairData = serviceDataMap.get(FAST_PAIR_UUID);
+ if (fastPairData != null) {
+ builder.setData(serviceDataMap.get(FAST_PAIR_UUID));
+ }
+ }
+ mExecutor.execute(() -> mListener.onNearbyDeviceDiscovered(builder.build()));
+ }
+
+ @Override
+ public void onScanFailed(int errorCode) {
+ Log.w(TAG, "BLE Scan failed with error code " + errorCode);
+ }
+ };
+
+ public BleDiscoveryProvider(Context context, Injector injector) {
+ super(context, NEARBY_EXECUTOR);
+ mInjector = injector;
+ }
+
+ private static List<ScanFilter> getScanFilters() {
+ List<ScanFilter> scanFilterList = new ArrayList<>();
+ scanFilterList.add(
+ new ScanFilter.Builder()
+ .setServiceData(FAST_PAIR_UUID, new byte[]{0}, new byte[]{0})
+ .build());
+ return scanFilterList;
+ }
+
+ private boolean isBleAvailable() {
+ BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+ if (adapter == null) {
+ return false;
+ }
+
+ return adapter.getBluetoothLeScanner() != null;
+ }
+
+ @Nullable
+ private BluetoothLeScanner getBleScanner() {
+ BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+ if (adapter == null) {
+ return null;
+ }
+ return adapter.getBluetoothLeScanner();
+ }
+
+ @Override
+ protected void onStart() {
+ if (isBleAvailable()) {
+ Log.d(TAG, "BleDiscoveryProvider started.");
+ startScan(getScanFilters(), getScanSettings(), mScanCallback);
+ return;
+ }
+ Log.w(TAG, "Cannot start BleDiscoveryProvider because Ble is not available.");
+ mController.stop();
+ }
+
+ @Override
+ protected void onStop() {
+ BluetoothLeScanner bluetoothLeScanner = getBleScanner();
+ if (bluetoothLeScanner == null) {
+ Log.w(TAG, "BleDiscoveryProvider failed to stop BLE scanning "
+ + "because BluetoothLeScanner is null.");
+ return;
+ }
+ bluetoothLeScanner.stopScan(mScanCallback);
+ }
+
+ @Override
+ protected void invalidateScanMode() {
+ onStop();
+ onStart();
+ }
+
+ private void startScan(
+ List<ScanFilter> scanFilters, ScanSettings scanSettings,
+ android.bluetooth.le.ScanCallback scanCallback) {
+ try {
+ BluetoothLeScanner bluetoothLeScanner = getBleScanner();
+ if (bluetoothLeScanner == null) {
+ Log.w(TAG, "BleDiscoveryProvider failed to start BLE scanning "
+ + "because BluetoothLeScanner is null.");
+ }
+ bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallback);
+ } catch (NullPointerException | IllegalStateException | SecurityException e) {
+ // NullPointerException:
+ // - Commonly, on Blackberry devices. b/73299795
+ // - Rarely, on other devices. b/75285249
+ // IllegalStateException:
+ // Caused if we call BluetoothLeScanner.startScan() after Bluetooth has turned off.
+ // SecurityException:
+ // refer to b/177380884
+ Log.w(TAG, "BleDiscoveryProvider failed to start BLE scanning.", e);
+ }
+ }
+
+ private ScanSettings getScanSettings() {
+ int bleScanMode = ScanSettings.SCAN_MODE_LOW_POWER;
+ switch (mController.getProviderScanMode()) {
+ case ScanRequest.SCAN_MODE_LOW_LATENCY:
+ bleScanMode = ScanSettings.SCAN_MODE_LOW_LATENCY;
+ break;
+ case ScanRequest.SCAN_MODE_BALANCED:
+ bleScanMode = ScanSettings.SCAN_MODE_BALANCED;
+ break;
+ case ScanRequest.SCAN_MODE_LOW_POWER:
+ bleScanMode = ScanSettings.SCAN_MODE_LOW_POWER;
+ break;
+ case ScanRequest.SCAN_MODE_NO_POWER:
+ bleScanMode = ScanSettings.SCAN_MODE_OPPORTUNISTIC;
+ break;
+ }
+ return new ScanSettings.Builder().setScanMode(bleScanMode).build();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
new file mode 100644
index 0000000..fb1afb0
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2021 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 com.android.server.nearby.NearbyService.TAG;
+
+import android.content.Context;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.ScanRequest;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.nearby.injector.Injector;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Manages all aspects of discovery providers.
+ */
+public class DiscoveryProviderManager implements AbstractDiscoveryProvider.Listener {
+
+ protected final Object mLock = new Object();
+ private final Context mContext;
+ private final BleDiscoveryProvider mBleDiscoveryProvider;
+ private @ScanRequest.ScanMode int mScanMode;
+
+ @GuardedBy("mLock")
+ private Map<IScanListener, ScanListenerRecord> mScanTypeScanListenerRecordMap;
+
+ @Override
+ public void onNearbyDeviceDiscovered(NearbyDeviceParcelable nearbyDevice) {
+ synchronized (mLock) {
+ for (IScanListener listener : mScanTypeScanListenerRecordMap.keySet()) {
+ ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listener);
+ if (record == null) {
+ Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+ continue;
+ }
+ try {
+ record.getScanListener().onDiscovered(
+ PrivacyFilter.filter(record.getScanRequest().getScanType(),
+ nearbyDevice));
+ } catch (RemoteException e) {
+ Log.w(TAG, "DiscoveryProviderManager failed to report onDiscovered.", e);
+ }
+ }
+ }
+ }
+
+ public DiscoveryProviderManager(Context context, Injector injector) {
+ mContext = context;
+ mBleDiscoveryProvider = new BleDiscoveryProvider(mContext, injector);
+ mScanTypeScanListenerRecordMap = new HashMap<>();
+ }
+
+ /**
+ * Registers the listener in the manager and starts scan according to the requested scan mode.
+ */
+ public void registerScanListener(ScanRequest scanRequest, IScanListener listener) {
+ synchronized (mLock) {
+ if (mScanTypeScanListenerRecordMap.containsKey(listener)) {
+ ScanRequest savedScanRequest = mScanTypeScanListenerRecordMap.get(
+ listener).getScanRequest();
+ if (scanRequest.equals(savedScanRequest)) {
+ Log.d(TAG, "Already registered the scanRequest: " + scanRequest);
+ return;
+ }
+ }
+
+ startProviders(scanRequest);
+
+ mScanTypeScanListenerRecordMap.put(listener,
+ new ScanListenerRecord(scanRequest, listener));
+ if (mScanMode < scanRequest.getScanMode()) {
+ mScanMode = scanRequest.getScanMode();
+ invalidateProviderScanMode();
+ }
+ }
+ }
+
+ /**
+ * Unregisters the listener in the manager and adjusts the scan mode if necessary afterwards.
+ */
+ public void unregisterScanListener(IScanListener listener) {
+ synchronized (mLock) {
+ if (!mScanTypeScanListenerRecordMap.containsKey(listener)) {
+ Log.w(TAG,
+ "Cannot unregister the scanRequest %s because the request is never "
+ + "registered.");
+ return;
+ }
+
+ ScanListenerRecord removedRecord = mScanTypeScanListenerRecordMap.remove(listener);
+ if (mScanTypeScanListenerRecordMap.isEmpty()) {
+ stopProviders();
+ return;
+ }
+
+ // Removes current highest scan mode requested and sets the next highest scan mode.
+ if (removedRecord.getScanRequest().getScanMode() == mScanMode) {
+ @ScanRequest.ScanMode int highestScanModeRequested = ScanRequest.SCAN_MODE_NO_POWER;
+ // find the next highest scan mode;
+ for (ScanListenerRecord record : mScanTypeScanListenerRecordMap.values()) {
+ @ScanRequest.ScanMode int scanMode = record.getScanRequest().getScanMode();
+ if (scanMode > highestScanModeRequested) {
+ highestScanModeRequested = scanMode;
+ }
+ }
+ if (mScanMode != highestScanModeRequested) {
+ mScanMode = highestScanModeRequested;
+ invalidateProviderScanMode();
+ }
+ }
+ }
+ }
+
+ private void startProviders(ScanRequest scanRequest) {
+ if (scanRequest.isEnableBle()) {
+ startBleProvider(scanRequest);
+ }
+ }
+
+ private void startBleProvider(ScanRequest scanRequest) {
+ if (!mBleDiscoveryProvider.getController().isStarted()) {
+ Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+ mBleDiscoveryProvider.getController().start();
+ mBleDiscoveryProvider.getController().setListener(this);
+ mBleDiscoveryProvider.getController().setProviderScanMode(scanRequest.getScanMode());
+ }
+ }
+
+ private void stopProviders() {
+ stopBleProvider();
+ }
+
+ private void stopBleProvider() {
+ mBleDiscoveryProvider.getController().stop();
+ }
+
+ private void invalidateProviderScanMode() {
+ if (!mBleDiscoveryProvider.getController().isStarted()) {
+ Log.d(TAG,
+ "Skip invalidating BleDiscoveryProvider scan mode because the provider not "
+ + "started.");
+ return;
+ }
+ mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+ }
+
+ private static class ScanListenerRecord {
+
+ private final ScanRequest mScanRequest;
+
+ private final IScanListener mScanListener;
+
+
+ ScanListenerRecord(ScanRequest scanRequest, IScanListener iScanListener) {
+ mScanListener = iScanListener;
+ mScanRequest = scanRequest;
+ }
+
+ IScanListener getScanListener() {
+ return mScanListener;
+ }
+
+ ScanRequest getScanRequest() {
+ return mScanRequest;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof ScanListenerRecord) {
+ ScanListenerRecord otherScanListenerRecord = (ScanListenerRecord) other;
+ return Objects.equals(mScanRequest, otherScanListenerRecord.mScanRequest)
+ && Objects.equals(mScanListener, otherScanListenerRecord.mScanListener);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mScanListener, mScanRequest);
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/PrivacyFilter.java b/nearby/service/java/com/android/server/nearby/provider/PrivacyFilter.java
new file mode 100644
index 0000000..5c37f68
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/PrivacyFilter.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2021 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.annotation.Nullable;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.ScanRequest;
+
+/**
+ * Class strips out privacy sensitive data before delivering the callbacks to client.
+ */
+public class PrivacyFilter {
+
+ /**
+ * Strips sensitive data from {@link NearbyDeviceParcelable} according to
+ * different {@link android.nearby.ScanRequest.ScanType}s.
+ */
+ @Nullable
+ public static NearbyDeviceParcelable filter(@ScanRequest.ScanType int scanType,
+ NearbyDeviceParcelable scanResult) {
+ return scanResult;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/ForegroundThread.java b/nearby/service/java/com/android/server/nearby/util/ForegroundThread.java
new file mode 100644
index 0000000..793ab9a
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/ForegroundThread.java
@@ -0,0 +1,113 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * Shared singleton foreground thread.
+ */
+public class ForegroundThread extends HandlerThread {
+ private static final Object sLock = new Object();
+
+ @GuardedBy("sLock")
+ private static ForegroundThread sInstance;
+ @GuardedBy("sLock")
+ private static Handler sHandler;
+ @GuardedBy("sLock")
+ private static Executor sExecutor;
+
+ private ForegroundThread() {
+ super(ForegroundThread.class.getName());
+ }
+
+ @GuardedBy("sLock")
+ private static void ensureInstanceLocked() {
+ if (sInstance == null) {
+ sInstance = new ForegroundThread();
+ sInstance.start();
+ sHandler = new Handler(sInstance.getLooper());
+ sExecutor = new HandlerExecutor(sHandler);
+ }
+ }
+
+ /**
+ * Get the singleton instance of thi class.
+ *
+ * @return the singleton instance of thi class
+ */
+ @NonNull
+ public static ForegroundThread get() {
+ synchronized (sLock) {
+ ensureInstanceLocked();
+ return sInstance;
+ }
+ }
+
+ /**
+ * Get the {@link Handler} for this thread.
+ *
+ * @return the {@link Handler} for this thread.
+ */
+ @NonNull
+ public static Handler getHandler() {
+ synchronized (sLock) {
+ ensureInstanceLocked();
+ return sHandler;
+ }
+ }
+
+ /**
+ * Get the {@link Executor} for this thread.
+ *
+ * @return the {@link Executor} for this thread.
+ */
+ @NonNull
+ public static Executor getExecutor() {
+ synchronized (sLock) {
+ ensureInstanceLocked();
+ return sExecutor;
+ }
+ }
+
+ /**
+ * An adapter {@link Executor} that posts all executed tasks onto the given
+ * {@link Handler}.
+ */
+ private static class HandlerExecutor implements Executor {
+ private final Handler mHandler;
+
+ HandlerExecutor(@NonNull Handler handler) {
+ mHandler = Preconditions.checkNotNull(handler);
+ }
+
+ @Override
+ public void execute(Runnable command) {
+ if (!mHandler.post(command)) {
+ throw new RejectedExecutionException(mHandler + " is shutting down");
+ }
+ }
+ }
+}