Implement per-field matching of ScanRecord.

As part of building out support for robustly matching Bluetooth LE
devices in the wild, this change checks all "fields" contained in a
ScanRecord against a given BytesMatcher.

To support matching variable-length Eddystone beacons, this change
also expands BytesMatcher to support both exact length and prefix
based rules, which are then used with rules that verify that example
Eddystone and iBeacon values can be detected with these rules:

    Eddystone: ⊆0016AAFE/00FFFFFF
    iBeacon: ⊆00FF4C0002/00FFFFFFFF

Expands testing to confirm all newly added capabilities are working.

Bug: 181812624
Test: atest BluetoothTests:android.bluetooth.le
Test: atest FrameworksCoreTests:android.os.BytesMatcherTest
Change-Id: I1cff8e08604436f4bba6f55aad64c3ce5969bf56
diff --git a/framework/java/android/bluetooth/le/ScanRecord.java b/framework/java/android/bluetooth/le/ScanRecord.java
index c0c1aa1..794b512 100644
--- a/framework/java/android/bluetooth/le/ScanRecord.java
+++ b/framework/java/android/bluetooth/le/ScanRecord.java
@@ -29,6 +29,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Predicate;
 
 /**
  * Represents a scan record from Bluetooth LE scan.
@@ -168,6 +169,27 @@
         return mBytes;
     }
 
+    /**
+     * Test if any fields contained inside this scan record are matched by the
+     * given matcher.
+     *
+     * @hide
+     */
+    public boolean matchesAnyField(@NonNull Predicate<byte[]> matcher) {
+        int pos = 0;
+        while (pos < mBytes.length) {
+            final int length = mBytes[pos] & 0xFF;
+            if (length == 0) {
+                break;
+            }
+            if (matcher.test(Arrays.copyOfRange(mBytes, pos, pos + length + 1))) {
+                return true;
+            }
+            pos += length + 1;
+        }
+        return false;
+    }
+
     private ScanRecord(List<ParcelUuid> serviceUuids,
             List<ParcelUuid> serviceSolicitationUuids,
             SparseArray<byte[]> manufacturerData,
diff --git a/framework/tests/AndroidTest.xml b/framework/tests/AndroidTest.xml
new file mode 100644
index 0000000..f93c4eb
--- /dev/null
+++ b/framework/tests/AndroidTest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2020 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.
+-->
+<configuration description="Config for Bluetooth test cases">
+    <option name="test-suite-tag" value="apct"/>
+    <option name="test-suite-tag" value="apct-instrumentation"/>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="BluetoothTests.apk" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct"/>
+    <option name="test-tag" value="BluetoothTests"/>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.bluetooth.tests" />
+        <option name="hidden-api-checks" value="false"/>
+        <option name="runner" value="android.bluetooth.BluetoothTestRunner"/>
+    </test>
+</configuration>
diff --git a/framework/tests/src/android/bluetooth/le/ScanRecordTest.java b/framework/tests/src/android/bluetooth/le/ScanRecordTest.java
index 8b3db7e..c287ea9 100644
--- a/framework/tests/src/android/bluetooth/le/ScanRecordTest.java
+++ b/framework/tests/src/android/bluetooth/le/ScanRecordTest.java
@@ -16,13 +16,18 @@
 
 package android.bluetooth.le;
 
-import android.bluetooth.le.ScanRecord;
+import android.os.BytesMatcher;
 import android.os.ParcelUuid;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.internal.util.HexDump;
+
 import junit.framework.TestCase;
 
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
+import java.util.function.Predicate;
 
 /**
  * Unit test cases for {@link ScanRecord}.
@@ -31,6 +36,66 @@
  * 'com.android.bluetooth.tests/android.bluetooth.BluetoothTestRunner'
  */
 public class ScanRecordTest extends TestCase {
+    /**
+     * Example raw beacons captured from a Blue Charm BC011
+     */
+    private static final String RECORD_URL = "0201060303AAFE1716AAFE10EE01626C7565636861726D626561636F6E730009168020691E0EFE13551109426C7565436861726D5F313639363835000000";
+    private static final String RECORD_UUID = "0201060303AAFE1716AAFE00EE626C7565636861726D31000000000001000009168020691E0EFE13551109426C7565436861726D5F313639363835000000";
+    private static final String RECORD_TLM = "0201060303AAFE1116AAFE20000BF017000008874803FB93540916802069080EFE13551109426C7565436861726D5F313639363835000000000000000000";
+    private static final String RECORD_IBEACON = "0201061AFF4C000215426C7565436861726D426561636F6E730EFE1355C509168020691E0EFE13551109426C7565436861726D5F31363936383500000000";
+
+    @SmallTest
+    public void testMatchesAnyField_Eddystone_Parser() {
+        final List<String> found = new ArrayList<>();
+        final Predicate<byte[]> matcher = (v) -> {
+            found.add(HexDump.toHexString(v));
+            return false;
+        };
+        ScanRecord.parseFromBytes(HexDump.hexStringToByteArray(RECORD_URL))
+                .matchesAnyField(matcher);
+
+        assertEquals(Arrays.asList(
+                "020106",
+                "0303AAFE",
+                "1716AAFE10EE01626C7565636861726D626561636F6E7300",
+                "09168020691E0EFE1355",
+                "1109426C7565436861726D5F313639363835"), found);
+    }
+
+    @SmallTest
+    public void testMatchesAnyField_Eddystone() {
+        final BytesMatcher matcher = BytesMatcher.decode("⊆0016AAFE/00FFFFFF");
+        assertMatchesAnyField(RECORD_URL, matcher);
+        assertMatchesAnyField(RECORD_UUID, matcher);
+        assertMatchesAnyField(RECORD_TLM, matcher);
+        assertNotMatchesAnyField(RECORD_IBEACON, matcher);
+    }
+
+    @SmallTest
+    public void testMatchesAnyField_iBeacon_Parser() {
+        final List<String> found = new ArrayList<>();
+        final Predicate<byte[]> matcher = (v) -> {
+            found.add(HexDump.toHexString(v));
+            return false;
+        };
+        ScanRecord.parseFromBytes(HexDump.hexStringToByteArray(RECORD_IBEACON))
+                .matchesAnyField(matcher);
+
+        assertEquals(Arrays.asList(
+                "020106",
+                "1AFF4C000215426C7565436861726D426561636F6E730EFE1355C5",
+                "09168020691E0EFE1355",
+                "1109426C7565436861726D5F313639363835"), found);
+    }
+
+    @SmallTest
+    public void testMatchesAnyField_iBeacon() {
+        final BytesMatcher matcher = BytesMatcher.decode("⊆00FF4C0002/00FFFFFFFF");
+        assertNotMatchesAnyField(RECORD_URL, matcher);
+        assertNotMatchesAnyField(RECORD_UUID, matcher);
+        assertNotMatchesAnyField(RECORD_TLM, matcher);
+        assertMatchesAnyField(RECORD_IBEACON, matcher);
+    }
 
     @SmallTest
     public void testParser() {
@@ -70,4 +135,14 @@
         }
 
     }
+
+    private static void assertMatchesAnyField(String record, BytesMatcher matcher) {
+        assertTrue(ScanRecord.parseFromBytes(HexDump.hexStringToByteArray(record))
+                .matchesAnyField(matcher));
+    }
+
+    private static void assertNotMatchesAnyField(String record, BytesMatcher matcher) {
+        assertFalse(ScanRecord.parseFromBytes(HexDump.hexStringToByteArray(record))
+                .matchesAnyField(matcher));
+    }
 }