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

Commit 36e23d34 authored by Jeff Sharkey's avatar Jeff Sharkey Committed by Android (Google) Code Review
Browse files

Merge "Implement per-field matching of ScanRecord." into sc-dev

parents 78af4257 e6e4c052
Loading
Loading
Loading
Loading
+22 −0
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import java.util.ArrayList;
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 @@ public final class ScanRecord {
        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,
+270 −203
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.bluetooth.BluetoothUuid;
import android.net.MacAddress;
import android.text.TextUtils;
import android.util.Log;

import com.android.internal.util.HexDump;
@@ -44,8 +45,10 @@ import java.util.function.Predicate;
public class BytesMatcher implements Predicate<byte[]> {
    private static final String TAG = "BytesMatcher";

   private static final char TYPE_ACCEPT = '+';
   private static final char TYPE_REJECT = '-';
    private static final char TYPE_EXACT_ACCEPT = '+';
    private static final char TYPE_EXACT_REJECT = '-';
    private static final char TYPE_PREFIX_ACCEPT = '⊆';
    private static final char TYPE_PREFIX_REJECT = '⊈';

    private final ArrayList<Rule> mRules = new ArrayList<>();

@@ -81,9 +84,20 @@ public class BytesMatcher implements Predicate<byte[]> {
        }

        public boolean test(@NonNull byte[] value) {
            switch (type) {
                case TYPE_EXACT_ACCEPT:
                case TYPE_EXACT_REJECT:
                    if (value.length != this.value.length) {
                        return false;
                    }
                    break;
                case TYPE_PREFIX_ACCEPT:
                case TYPE_PREFIX_REJECT:
                    if (value.length < this.value.length) {
                        return false;
                    }
                    break;
            }
            for (int i = 0; i < this.value.length; i++) {
                byte local = this.value[i];
                byte remote = value[i];
@@ -101,7 +115,25 @@ public class BytesMatcher implements Predicate<byte[]> {

    /**
     * Add a rule that will result in {@link #test(byte[])} returning
    * {@code true} when a value being tested matches it.
     * {@code true} when a value being tested matches it. This rule will only
     * match values of the exact same length.
     * <p>
     * Rules are tested in the order in which they were originally added, which
     * means a narrow rule can reject a specific value before a later broader
     * rule might accept that same value, or vice versa.
     *
     * @param value to be matched
     * @param mask to be applied to both values before testing for equality; if
     *            {@code null} then both values must match exactly
     */
    public void addExactAcceptRule(@NonNull byte[] value, @Nullable byte[] mask) {
        mRules.add(new Rule(TYPE_EXACT_ACCEPT, value, mask));
    }

    /**
     * Add a rule that will result in {@link #test(byte[])} returning
     * {@code false} when a value being tested matches it. This rule will only
     * match values of the exact same length.
     * <p>
     * Rules are tested in the order in which they were originally added, which
     * means a narrow rule can reject a specific value before a later broader
@@ -111,13 +143,14 @@ public class BytesMatcher implements Predicate<byte[]> {
     * @param mask to be applied to both values before testing for equality; if
     *            {@code null} then both values must match exactly
     */
   public void addAcceptRule(@NonNull byte[] value, @Nullable byte[] mask) {
       mRules.add(new Rule(TYPE_ACCEPT, value, mask));
    public void addExactRejectRule(@NonNull byte[] value, @Nullable byte[] mask) {
        mRules.add(new Rule(TYPE_EXACT_REJECT, value, mask));
    }

    /**
     * Add a rule that will result in {@link #test(byte[])} returning
    * {@code false} when a value being tested matches it.
     * {@code true} when a value being tested matches it. This rule will match
     * values of the exact same length or longer.
     * <p>
     * Rules are tested in the order in which they were originally added, which
     * means a narrow rule can reject a specific value before a later broader
@@ -127,8 +160,25 @@ public class BytesMatcher implements Predicate<byte[]> {
     * @param mask to be applied to both values before testing for equality; if
     *            {@code null} then both values must match exactly
     */
   public void addRejectRule(@NonNull byte[] value, @Nullable byte[] mask) {
       mRules.add(new Rule(TYPE_REJECT, value, mask));
    public void addPrefixAcceptRule(@NonNull byte[] value, @Nullable byte[] mask) {
        mRules.add(new Rule(TYPE_PREFIX_ACCEPT, value, mask));
    }

    /**
     * Add a rule that will result in {@link #test(byte[])} returning
     * {@code false} when a value being tested matches it. This rule will match
     * values of the exact same length or longer.
     * <p>
     * Rules are tested in the order in which they were originally added, which
     * means a narrow rule can reject a specific value before a later broader
     * rule might accept that same value, or vice versa.
     *
     * @param value to be matched
     * @param mask to be applied to both values before testing for equality; if
     *            {@code null} then both values must match exactly
     */
    public void addPrefixRejectRule(@NonNull byte[] value, @Nullable byte[] mask) {
        mRules.add(new Rule(TYPE_PREFIX_REJECT, value, mask));
    }

    /**
@@ -165,7 +215,14 @@ public class BytesMatcher implements Predicate<byte[]> {
        for (int i = 0; i < size; i++) {
            final Rule rule = mRules.get(i);
            if (rule.test(value)) {
               return (rule.type == TYPE_ACCEPT);
                switch (rule.type) {
                    case TYPE_EXACT_ACCEPT:
                    case TYPE_PREFIX_ACCEPT:
                        return true;
                    case TYPE_EXACT_REJECT:
                    case TYPE_PREFIX_REJECT:
                        return false;
                }
            }
        }
        return defaultValue;
@@ -191,7 +248,9 @@ public class BytesMatcher implements Predicate<byte[]> {
            rule.encode(builder);
            builder.append(',');
        }
        if (builder.length() > 0) {
            builder.deleteCharAt(builder.length() - 1);
        }
        return builder.toString();
    }

@@ -207,8 +266,10 @@ public class BytesMatcher implements Predicate<byte[]> {
     *
     * @see #encode(BytesMatcher)
     */
   public static @NonNull BytesMatcher decode(@NonNull String value) {
    public static @NonNull BytesMatcher decode(@Nullable String value) {
        final BytesMatcher matcher = new BytesMatcher();
        if (TextUtils.isEmpty(value)) return matcher;

        final int length = value.length();
        for (int i = 0; i < length;) {
            final char type = value.charAt(i);
@@ -230,11 +291,17 @@ public class BytesMatcher implements Predicate<byte[]> {
            }

            switch (type) {
               case TYPE_ACCEPT:
                   matcher.addAcceptRule(ruleValue, ruleMask);
                case TYPE_EXACT_ACCEPT:
                    matcher.addExactAcceptRule(ruleValue, ruleMask);
                    break;
                case TYPE_EXACT_REJECT:
                    matcher.addExactRejectRule(ruleValue, ruleMask);
                    break;
                case TYPE_PREFIX_ACCEPT:
                    matcher.addPrefixAcceptRule(ruleValue, ruleMask);
                    break;
               case TYPE_REJECT:
                   matcher.addRejectRule(ruleValue, ruleMask);
                case TYPE_PREFIX_REJECT:
                    matcher.addPrefixRejectRule(ruleValue, ruleMask);
                    break;
                default:
                    Log.w(TAG, "Ignoring unknown type " + type);
+32 −0
Original line number 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 Diff line number Diff line
@@ -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 @@ import java.util.Arrays;
 * '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 @@ 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));
    }
}
+49 −4
Original line number Diff line number Diff line
@@ -58,6 +58,19 @@ public class BytesMatcherTest extends TestCase {
        assertFalse(matcher.test(hexStringToByteArray("cafe00")));
    }

    @Test
    public void testPrefix() throws Exception {
        BytesMatcher matcher = BytesMatcher.decode("⊆cafe,⊆beef/ff00");
        assertTrue(matcher.test(hexStringToByteArray("cafe")));
        assertFalse(matcher.test(hexStringToByteArray("caff")));
        assertTrue(matcher.test(hexStringToByteArray("cafecafe")));
        assertFalse(matcher.test(hexStringToByteArray("ca")));
        assertTrue(matcher.test(hexStringToByteArray("beef")));
        assertTrue(matcher.test(hexStringToByteArray("beff")));
        assertTrue(matcher.test(hexStringToByteArray("beffbeff")));
        assertFalse(matcher.test(hexStringToByteArray("be")));
    }

    @Test
    public void testMacAddress() throws Exception {
        BytesMatcher matcher = BytesMatcher.decode("+cafe00112233/ffffff000000");
@@ -94,13 +107,23 @@ public class BytesMatcherTest extends TestCase {
    }

    @Test
    public void testSerialize() throws Exception {
    public void testSerialize_Empty() throws Exception {
        BytesMatcher matcher = new BytesMatcher();
        matcher = BytesMatcher.decode(BytesMatcher.encode(matcher));

        // Also very empty and null values
        BytesMatcher.decode("");
        BytesMatcher.decode(null);
    }

    @Test
    public void testSerialize_Exact() throws Exception {
        BytesMatcher matcher = new BytesMatcher();
        matcher.addRejectRule(hexStringToByteArray("cafe00112233"),
        matcher.addExactRejectRule(hexStringToByteArray("cafe00112233"),
                hexStringToByteArray("ffffff000000"));
        matcher.addRejectRule(hexStringToByteArray("beef00112233"),
        matcher.addExactRejectRule(hexStringToByteArray("beef00112233"),
                null);
        matcher.addAcceptRule(hexStringToByteArray("000000000000"),
        matcher.addExactAcceptRule(hexStringToByteArray("000000000000"),
                hexStringToByteArray("000000000000"));

        assertFalse(matcher.test(hexStringToByteArray("cafe00ffffff")));
@@ -115,6 +138,28 @@ public class BytesMatcherTest extends TestCase {
        assertTrue(matcher.test(hexStringToByteArray("beef00ffffff")));
    }

    @Test
    public void testSerialize_Prefix() throws Exception {
        BytesMatcher matcher = new BytesMatcher();
        matcher.addExactRejectRule(hexStringToByteArray("aa"), null);
        matcher.addExactAcceptRule(hexStringToByteArray("bb"), null);
        matcher.addPrefixAcceptRule(hexStringToByteArray("aa"), null);
        matcher.addPrefixRejectRule(hexStringToByteArray("bb"), null);

        assertFalse(matcher.test(hexStringToByteArray("aa")));
        assertTrue(matcher.test(hexStringToByteArray("bb")));
        assertTrue(matcher.test(hexStringToByteArray("aaaa")));
        assertFalse(matcher.test(hexStringToByteArray("bbbb")));

        // Bounce through serialization pass and confirm it still works
        matcher = BytesMatcher.decode(BytesMatcher.encode(matcher));

        assertFalse(matcher.test(hexStringToByteArray("aa")));
        assertTrue(matcher.test(hexStringToByteArray("bb")));
        assertTrue(matcher.test(hexStringToByteArray("aaaa")));
        assertFalse(matcher.test(hexStringToByteArray("bbbb")));
    }

    @Test
    public void testOrdering_RejectFirst() throws Exception {
        BytesMatcher matcher = BytesMatcher.decode("-ff/0f,+ff/f0");