Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ebd19b81 authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

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
parent 089e35e8
Loading
Loading
Loading
Loading
+22 −0
Original line number Original line Diff line number Diff line
@@ -29,6 +29,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Arrays;
import java.util.List;
import java.util.List;
import java.util.Map;
import java.util.Map;
import java.util.function.Predicate;


/**
/**
 * Represents a scan record from Bluetooth LE scan.
 * Represents a scan record from Bluetooth LE scan.
@@ -168,6 +169,27 @@ public final class ScanRecord {
        return mBytes;
        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,
    private ScanRecord(List<ParcelUuid> serviceUuids,
            List<ParcelUuid> serviceSolicitationUuids,
            List<ParcelUuid> serviceSolicitationUuids,
            SparseArray<byte[]> manufacturerData,
            SparseArray<byte[]> manufacturerData,
+32 −0
Original line number Original line Diff line number Diff line
<?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>
+76 −1
Original line number Original line Diff line number Diff line
@@ -16,13 +16,18 @@


package android.bluetooth.le;
package android.bluetooth.le;


import android.bluetooth.le.ScanRecord;
import android.os.BytesMatcher;
import android.os.ParcelUuid;
import android.os.ParcelUuid;
import android.test.suitebuilder.annotation.SmallTest;
import android.test.suitebuilder.annotation.SmallTest;


import com.android.internal.util.HexDump;

import junit.framework.TestCase;
import junit.framework.TestCase;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;


/**
/**
 * Unit test cases for {@link ScanRecord}.
 * Unit test cases for {@link ScanRecord}.
@@ -31,6 +36,66 @@ import java.util.Arrays;
 * 'com.android.bluetooth.tests/android.bluetooth.BluetoothTestRunner'
 * 'com.android.bluetooth.tests/android.bluetooth.BluetoothTestRunner'
 */
 */
public class ScanRecordTest extends TestCase {
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
    @SmallTest
    public void testParser() {
    public void testParser() {
@@ -70,4 +135,14 @@ public class ScanRecordTest extends TestCase {
        }
        }


    }
    }

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