[Offload] Add DataElement to filter CHRE only scan request

Test: Unit test
Bug: 263431028
Ignore-AOSP-First: nearby_not_in_aosp_yet
Change-Id: I913f61dc34e31f1e944593ed6c880e7fbd3ae5bb
diff --git a/nearby/framework/java/android/nearby/DataElement.java b/nearby/framework/java/android/nearby/DataElement.java
index 0aa3499..4592c33 100644
--- a/nearby/framework/java/android/nearby/DataElement.java
+++ b/nearby/framework/java/android/nearby/DataElement.java
@@ -54,11 +54,14 @@
             DataType.EDDYSTONE_EPHEMERAL_IDENTIFIER,
             DataType.ACCOUNT_KEY_DATA,
             DataType.CONNECTION_STATUS,
-            DataType.BATTERY
+            DataType.BATTERY,
+            DataType.SCAN_MODE
     })
     public @interface DataType {
         int BLE_SERVICE_DATA = 100;
         int BLE_ADDRESS = 101;
+        // This is to indicate if the scan is offload only
+        int SCAN_MODE = 102;
         int SALT = 0;
         int PRIVATE_IDENTITY = 1;
         int TRUSTED_IDENTITY = 2;
@@ -80,6 +83,7 @@
         return type == DataType.BLE_SERVICE_DATA
                 || type == DataType.ACCOUNT_KEY_DATA
                 || type == DataType.BLE_ADDRESS
+                || type == DataType.SCAN_MODE
                 || type == DataType.SALT
                 || type == DataType.PRIVATE_IDENTITY
                 || type == DataType.TRUSTED_IDENTITY
diff --git a/nearby/framework/java/android/nearby/ScanRequest.java b/nearby/framework/java/android/nearby/ScanRequest.java
index c717ac7..9421820 100644
--- a/nearby/framework/java/android/nearby/ScanRequest.java
+++ b/nearby/framework/java/android/nearby/ScanRequest.java
@@ -62,6 +62,12 @@
      */
     public static final int SCAN_MODE_NO_POWER = -1;
     /**
+     * A special scan mode to indicate that client only wants to use CHRE to scan.
+     *
+     * @hide
+     */
+    public static final int SCAN_MODE_CHRE_ONLY = 3;
+    /**
      * Used to read a ScanRequest from a Parcel.
      */
     @NonNull
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 85b1228..e4c56e4 100644
--- a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
@@ -23,6 +23,7 @@
 import android.annotation.Nullable;
 import android.app.AppOpsManager;
 import android.content.Context;
+import android.nearby.DataElement;
 import android.nearby.IScanListener;
 import android.nearby.NearbyDeviceParcelable;
 import android.nearby.PresenceScanFilter;
@@ -51,6 +52,7 @@
 
 /** 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;
@@ -123,6 +125,18 @@
         mInjector = injector;
     }
 
+    @VisibleForTesting
+    DiscoveryProviderManager(Context context, Injector injector,
+            BleDiscoveryProvider bleDiscoveryProvider,
+            ChreDiscoveryProvider chreDiscoveryProvider,
+            Map<IBinder, ScanListenerRecord> scanTypeScanListenerRecordMap) {
+        mContext = context;
+        mInjector = injector;
+        mBleDiscoveryProvider = bleDiscoveryProvider;
+        mChreDiscoveryProvider = chreDiscoveryProvider;
+        mScanTypeScanListenerRecordMap = scanTypeScanListenerRecordMap;
+    }
+
     /** Called after boot completed. */
     public void init() {
         if (mInjector.getContextHubManager() != null) {
@@ -225,34 +239,68 @@
 
     // Returns false when fail to start all the providers. Returns true if any one of the provider
     // starts successfully.
-    private boolean startProviders(ScanRequest scanRequest) {
-        if (scanRequest.isBleEnabled()) {
-            if (mChreDiscoveryProvider.available()
-                    && scanRequest.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
-                startChreProvider();
-            } else {
-                startBleProvider();
+    @VisibleForTesting
+    boolean startProviders(ScanRequest scanRequest) {
+        if (!scanRequest.isBleEnabled()) {
+            Log.w(TAG, "failed to start any provider because client disabled BLE");
+            return false;
+        }
+        List<ScanFilter> scanFilters = getPresenceScanFilters();
+
+        if (!mChreDiscoveryProvider.available()) {
+            if (scanRequest.getScanType() == SCAN_TYPE_NEARBY_PRESENCE && isChreOnly(scanFilters)) {
+                Log.w(TAG, "failed to start any provider because client wants CHRE only and CHRE"
+                        + " is not available");
+                return false;
             }
+            startBleProvider(scanFilters);
             return true;
         }
+
+        if (scanRequest.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
+            startChreProvider(scanFilters);
+            return true;
+        }
+
+        startBleProvider(scanFilters);
+        return true;
+    }
+
+    private static boolean isChreOnly(List<ScanFilter> scanFilters) {
+        for (ScanFilter scanFilter : scanFilters) {
+            List<DataElement> dataElements =
+                    ((PresenceScanFilter) scanFilter).getExtendedProperties();
+            for (DataElement dataElement : dataElements) {
+                if (dataElement.getKey() != DataElement.DataType.SCAN_MODE) {
+                    continue;
+                }
+                byte[] scanModeValue = dataElement.getValue();
+                if (scanModeValue == null || scanModeValue.length == 0) {
+                    break;
+                }
+                if (Byte.toUnsignedInt(scanModeValue[0]) == ScanRequest.SCAN_MODE_CHRE_ONLY) {
+                    return true;
+                }
+            }
+
+        }
         return false;
     }
 
-    private void startBleProvider() {
+    private void startBleProvider(List<ScanFilter> scanFilters) {
         if (!mBleDiscoveryProvider.getController().isStarted()) {
             Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
             mBleDiscoveryProvider.getController().setListener(this);
             mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
-            mBleDiscoveryProvider.getController().setProviderScanFilters(
-                    getPresenceScanFilters());
+            mBleDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
             mBleDiscoveryProvider.getController().start();
         }
     }
 
     @VisibleForTesting
-    void startChreProvider() {
+    void startChreProvider(List<ScanFilter> scanFilters) {
         Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning.");
-        mChreDiscoveryProvider.getController().setProviderScanFilters(getPresenceScanFilters());
+        mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
         mChreDiscoveryProvider.getController().setProviderScanMode(mScanMode);
         mChreDiscoveryProvider.getController().start();
     }
@@ -329,7 +377,8 @@
         }
     }
 
-    private static class ScanListenerRecord {
+    @VisibleForTesting
+    static class ScanListenerRecord {
 
         private final ScanRequest mScanRequest;
 
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/DiscoveryProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/DiscoveryProviderManagerTest.java
index 1af959d..b1737e9 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/DiscoveryProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/DiscoveryProviderManagerTest.java
@@ -19,30 +19,54 @@
 import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
 import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
 
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.AppOpsManager;
 import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
 import android.nearby.NearbyDeviceParcelable;
 import android.nearby.PresenceScanFilter;
 import android.nearby.PublicCredential;
-import android.nearby.ScanFilter;
+import android.nearby.ScanRequest;
+import android.os.IBinder;
 
 import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.util.identity.CallerIdentity;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 public class DiscoveryProviderManagerTest {
+    private static final int SCAN_MODE_CHRE_ONLY = 3;
+    private static final int DATA_TYPE_SCAN_MODE = 102;
+
     @Mock Injector mInjector;
     @Mock Context mContext;
     @Mock AppOpsManager mAppOpsManager;
+    @Mock BleDiscoveryProvider mBleDiscoveryProvider;
+    @Mock ChreDiscoveryProvider mChreDiscoveryProvider;
+    @Mock DiscoveryProviderController mBluetoothController;
+    @Mock DiscoveryProviderController mChreController;
+    @Mock IScanListener mScanListener;
+    @Mock CallerIdentity mCallerIdentity;
+    @Mock DiscoveryProviderManager.ScanListenerDeathRecipient mScanListenerDeathRecipient;
+    @Mock IBinder mIBinder;
+
     private DiscoveryProviderManager mDiscoveryProviderManager;
+    private Map<IBinder, DiscoveryProviderManager.ScanListenerRecord>
+            mScanTypeScanListenerRecordMap;
 
     private static final int RSSI = -60;
 
@@ -50,11 +74,15 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
         when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
+        when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
+        when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
+        mScanTypeScanListenerRecordMap = new HashMap<>();
         mDiscoveryProviderManager =
-                new DiscoveryProviderManager(mContext, mInjector);
+                new DiscoveryProviderManager(mContext, mInjector, mBleDiscoveryProvider,
+                        mChreDiscoveryProvider,
+                        mScanTypeScanListenerRecordMap);
     }
 
-
     @Test
     public void testOnNearbyDeviceDiscovered() {
         NearbyDeviceParcelable nearbyDeviceParcelable = new NearbyDeviceParcelable.Builder()
@@ -69,8 +97,95 @@
     }
 
     @Test
+    public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+        DiscoveryProviderManager.ScanListenerRecord record =
+                new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+
+        boolean startProviders = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, never()).start();
+        assertThat(startProviders).isTrue();
+    }
+
+    @Test
+    public void testStartProviders_chreOnlyChreAvailable_multipleFilters_bleProviderNotStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getChreOnlyPresenceScanFilter())
+                .addScanFilter(getPresenceScanFilter()).build();
+        DiscoveryProviderManager.ScanListenerRecord record =
+                new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        boolean startProviders = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, never()).start();
+        assertThat(startProviders).isTrue();
+    }
+
+    @Test
+    public void testStartProviders_chreOnlyChreUnavailable_bleProviderNotStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(false);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+        DiscoveryProviderManager.ScanListenerRecord record =
+                new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        boolean startProviders = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, never()).start();
+        assertThat(startProviders).isFalse();
+    }
+
+    @Test
+    public void testStartProviders_notChreOnlyChreAvailable_bleProviderNotStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getPresenceScanFilter()).build();
+        DiscoveryProviderManager.ScanListenerRecord record =
+                new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        boolean startProviders = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, never()).start();
+        assertThat(startProviders).isTrue();
+    }
+
+    @Test
+    public void testStartProviders_notChreOnlyChreUnavailable_bleProviderStarted() {
+        when(mChreDiscoveryProvider.available()).thenReturn(false);
+
+        ScanRequest scanRequest = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .addScanFilter(getPresenceScanFilter()).build();
+        DiscoveryProviderManager.ScanListenerRecord record =
+                new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
+                        mCallerIdentity, mScanListenerDeathRecipient);
+        mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+        boolean startProviders = mDiscoveryProviderManager.startProviders(scanRequest);
+        verify(mBluetoothController, atLeastOnce()).start();
+        assertThat(startProviders).isTrue();
+    }
+
+    @Test
     public void testStartChreProvider() {
-        mDiscoveryProviderManager.startChreProvider();
+        mDiscoveryProviderManager.startChreProvider(List.of(getPresenceScanFilter()));
     }
 
     private static PresenceScanFilter getPresenceScanFilter() {
@@ -92,4 +207,27 @@
                 .addPresenceAction(action)
                 .build();
     }
+
+    private static PresenceScanFilter getChreOnlyPresenceScanFilter() {
+        final byte[] secretId = new byte[]{1, 2, 3, 4};
+        final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+        final byte[] publicKey = new byte[]{1, 1, 2, 2};
+        final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+        final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+
+        PublicCredential credential = new PublicCredential.Builder(
+                secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+                .setIdentityType(IDENTITY_TYPE_PRIVATE)
+                .build();
+
+        final int action = 123;
+        DataElement scanModeElement = new DataElement(DATA_TYPE_SCAN_MODE,
+                new byte[]{SCAN_MODE_CHRE_ONLY});
+        return new PresenceScanFilter.Builder()
+                .addCredential(credential)
+                .setMaxPathLoss(RSSI)
+                .addPresenceAction(action)
+                .addExtendedProperty(scanModeElement)
+                .build();
+    }
 }