Special case & sanitize exposure notification results

Bug: 162031802
Tag: #feature
Test: verified do not get results before this change,
      and after the change, get results but only the
      RSSI & payload for EN

Change-Id: I9dca7d398ecc1543fc001921deb95c07b1b84e15
(cherry picked from commit a79caa334896377c5247fa02b83a71abc8973a58)
diff --git a/res/values/config.xml b/res/values/config.xml
index c73d7e6..6ceacf7 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -114,4 +114,7 @@
     <!-- Flag whether or not to keep polling AG with CLCC for call information every 2 seconds -->
     <bool name="hfp_clcc_poll_during_call">true</bool>
 
+    <!-- Package that is providing the exposure notification service -->
+    <string name="exposure_notification_package">com.google.android.gms</string>
+
 </resources>
diff --git a/src/com/android/bluetooth/gatt/ContextMap.java b/src/com/android/bluetooth/gatt/ContextMap.java
index 987f2ef..0d82eba 100644
--- a/src/com/android/bluetooth/gatt/ContextMap.java
+++ b/src/com/android/bluetooth/gatt/ContextMap.java
@@ -107,6 +107,8 @@
         /** Whether the calling app has the network setup wizard permission */
         boolean mHasScanWithoutLocationPermission;
 
+        boolean mEligibleForSanitizedExposureNotification;
+
         public List<String> mAssociatedDevices;
 
         /** Internal callback info queue, waiting to be send on congestion clear */
diff --git a/src/com/android/bluetooth/gatt/GattService.java b/src/com/android/bluetooth/gatt/GattService.java
index 0c29f59..87e6e52 100644
--- a/src/com/android/bluetooth/gatt/GattService.java
+++ b/src/com/android/bluetooth/gatt/GattService.java
@@ -56,6 +56,7 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.WorkSource;
+import android.provider.Settings;
 import android.util.Log;
 
 import com.android.bluetooth.BluetoothMetricsProto;
@@ -185,6 +186,7 @@
     private ScanManager mScanManager;
     private AppOpsManager mAppOps;
     private ICompanionDeviceManager mCompanionManager;
+    private String mExposureNotificationPackage;
 
     private static GattService sGattService;
 
@@ -207,6 +209,9 @@
         if (DBG) {
             Log.d(TAG, "start()");
         }
+        mExposureNotificationPackage = getString(R.string.exposure_notification_package);
+        Settings.Global.putInt(
+                getContentResolver(), "bluetooth_sanitized_exposure_notification_supported", 1);
         initializeNative();
         mAdapter = BluetoothAdapter.getDefaultAdapter();
         mCompanionManager = ICompanionDeviceManager.Stub.asInterface(
@@ -960,6 +965,56 @@
      * Callback functions - CLIENT
      *************************************************************************/
 
+    // EN format defined here:
+    // https://blog.google/documents/70/Exposure_Notification_-_Bluetooth_Specification_v1.2.2.pdf
+    private static final byte[] EXPOSURE_NOTIFICATION_FLAGS_PREAMBLE = new byte[] {
+        // size 2, flag field, flags byte (value is not important)
+        (byte) 0x02, (byte) 0x01
+    };
+    private static final int EXPOSURE_NOTIFICATION_FLAGS_LENGTH = 0x2 + 1;
+    private static final byte[] EXPOSURE_NOTIFICATION_PAYLOAD_PREAMBLE = new byte[] {
+        // size 3, complete 16 bit UUID, EN UUID
+        (byte) 0x03, (byte) 0x03, (byte) 0x6F, (byte) 0xFD,
+        // size 23, data for 16 bit UUID, EN UUID
+        (byte) 0x17, (byte) 0x16, (byte) 0x6F, (byte) 0xFD,
+        // ...payload
+    };
+    private static final int EXPOSURE_NOTIFICATION_PAYLOAD_LENGTH = 0x03 + 0x17 + 2;
+
+    private static boolean arrayStartsWith(byte[] array, byte[] prefix) {
+        if (array.length < prefix.length) {
+            return false;
+        }
+        for (int i = 0; i < prefix.length; i++) {
+            if (prefix[i] != array[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    ScanResult getSanitizedExposureNotification(ScanResult result) {
+        ScanRecord record = result.getScanRecord();
+        // Remove the flags part of the payload, if present
+        if (record.getBytes().length > EXPOSURE_NOTIFICATION_FLAGS_LENGTH
+                && arrayStartsWith(record.getBytes(), EXPOSURE_NOTIFICATION_FLAGS_PREAMBLE)) {
+            record = ScanRecord.parseFromBytes(
+                    Arrays.copyOfRange(
+                            record.getBytes(),
+                            EXPOSURE_NOTIFICATION_FLAGS_LENGTH,
+                            record.getBytes().length));
+        }
+
+        if (record.getBytes().length != EXPOSURE_NOTIFICATION_PAYLOAD_LENGTH) {
+            return null;
+        }
+        if (!arrayStartsWith(record.getBytes(), EXPOSURE_NOTIFICATION_PAYLOAD_PREAMBLE)) {
+            return null;
+        }
+
+        return new ScanResult(null, 0, 0, 0, 0, 0, result.getRssi(), 0, record, 0);
+    }
+
     void onScanResult(int eventType, int addressType, String address, int primaryPhy,
             int secondaryPhy, int advertisingSid, int txPower, int rssi, int periodicAdvInt,
             byte[] advData) {
@@ -1027,6 +1082,13 @@
                     }
                 }
             }
+            if (!hasPermission && client.eligibleForSanitizedExposureNotification) {
+                ScanResult sanitized = getSanitizedExposureNotification(result);
+                if (sanitized != null) {
+                    hasPermission = true;
+                    result = sanitized;
+                }
+            }
             if (!hasPermission || !matchesFilters(client, result)) {
                 continue;
             }
@@ -1986,6 +2048,8 @@
         final ScanClient scanClient = new ScanClient(scannerId, settings, filters, storages);
         scanClient.userHandle = UserHandle.of(UserHandle.getCallingUserId());
         mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
+        scanClient.eligibleForSanitizedExposureNotification =
+                callingPackage.equals(mExposureNotificationPackage);
         scanClient.isQApp = Utils.isQApp(this, callingPackage);
         if (scanClient.isQApp) {
             scanClient.hasLocationPermission = Utils.checkCallerHasFineLocation(this, mAppOps,
@@ -2046,6 +2110,8 @@
         ScannerMap.App app = mScannerMap.add(uuid, null, null, piInfo, this);
         app.mUserHandle = UserHandle.of(UserHandle.getCallingUserId());
         mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
+        app.mEligibleForSanitizedExposureNotification =
+                callingPackage.equals(mExposureNotificationPackage);
         app.mIsQApp = Utils.isQApp(this, callingPackage);
         try {
             if (app.mIsQApp) {
@@ -2076,6 +2142,8 @@
         scanClient.hasLocationPermission = app.hasLocationPermission;
         scanClient.userHandle = app.mUserHandle;
         scanClient.isQApp = app.mIsQApp;
+        scanClient.eligibleForSanitizedExposureNotification =
+                app.mEligibleForSanitizedExposureNotification;
         scanClient.hasNetworkSettingsPermission = app.mHasNetworkSettingsPermission;
         scanClient.hasNetworkSetupWizardPermission = app.mHasNetworkSetupWizardPermission;
         scanClient.hasScanWithoutLocationPermission = app.mHasScanWithoutLocationPermission;
diff --git a/src/com/android/bluetooth/gatt/ScanClient.java b/src/com/android/bluetooth/gatt/ScanClient.java
index 9b8b77a..9f17cc0 100644
--- a/src/com/android/bluetooth/gatt/ScanClient.java
+++ b/src/com/android/bluetooth/gatt/ScanClient.java
@@ -44,6 +44,7 @@
     public boolean hasLocationPermission;
     public UserHandle userHandle;
     public boolean isQApp;
+    public boolean eligibleForSanitizedExposureNotification;
     public boolean hasNetworkSettingsPermission;
     public boolean hasNetworkSetupWizardPermission;
     public boolean hasScanWithoutLocationPermission;