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");
+            }
+        }
+    }
+}