/* * Copyright (C) 2017 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. */ package android.companion; import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal; import static android.companion.BluetoothDeviceFilterUtils.patternFromString; import static android.companion.BluetoothDeviceFilterUtils.patternToString; import static com.android.internal.util.Preconditions.checkArgument; import static com.android.internal.util.Preconditions.checkState; import android.annotation.NonNull; import android.annotation.Nullable; import android.bluetooth.BluetoothDevice; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanRecord; import android.bluetooth.le.ScanResult; import android.os.Parcel; import android.provider.OneTimeUseBuilder; import android.text.TextUtils; import android.util.Log; import com.android.internal.util.BitUtils; import com.android.internal.util.ObjectUtils; import com.android.internal.util.Preconditions; import java.nio.ByteOrder; import java.util.Arrays; import java.util.Objects; import java.util.regex.Pattern; /** * A filter for Bluetooth LE devices * * @see ScanFilter */ public final class BluetoothLeDeviceFilter implements DeviceFilter { private static final boolean DEBUG = false; private static final String LOG_TAG = "BluetoothLeDeviceFilter"; private static final int RENAME_PREFIX_LENGTH_LIMIT = 10; private final Pattern mNamePattern; private final ScanFilter mScanFilter; private final byte[] mRawDataFilter; private final byte[] mRawDataFilterMask; private final String mRenamePrefix; private final String mRenameSuffix; private final int mRenameBytesFrom; private final int mRenameBytesLength; private final int mRenameNameFrom; private final int mRenameNameLength; private final boolean mRenameBytesReverseOrder; private BluetoothLeDeviceFilter(Pattern namePattern, ScanFilter scanFilter, byte[] rawDataFilter, byte[] rawDataFilterMask, String renamePrefix, String renameSuffix, int renameBytesFrom, int renameBytesLength, int renameNameFrom, int renameNameLength, boolean renameBytesReverseOrder) { mNamePattern = namePattern; mScanFilter = ObjectUtils.firstNotNull(scanFilter, ScanFilter.EMPTY); mRawDataFilter = rawDataFilter; mRawDataFilterMask = rawDataFilterMask; mRenamePrefix = renamePrefix; mRenameSuffix = renameSuffix; mRenameBytesFrom = renameBytesFrom; mRenameBytesLength = renameBytesLength; mRenameNameFrom = renameNameFrom; mRenameNameLength = renameNameLength; mRenameBytesReverseOrder = renameBytesReverseOrder; } /** @hide */ @Nullable public Pattern getNamePattern() { return mNamePattern; } /** @hide */ @NonNull public ScanFilter getScanFilter() { return mScanFilter; } /** @hide */ @Nullable public byte[] getRawDataFilter() { return mRawDataFilter; } /** @hide */ @Nullable public byte[] getRawDataFilterMask() { return mRawDataFilterMask; } /** @hide */ @Nullable public String getRenamePrefix() { return mRenamePrefix; } /** @hide */ @Nullable public String getRenameSuffix() { return mRenameSuffix; } /** @hide */ public int getRenameBytesFrom() { return mRenameBytesFrom; } /** @hide */ public int getRenameBytesLength() { return mRenameBytesLength; } /** @hide */ public boolean isRenameBytesReverseOrder() { return mRenameBytesReverseOrder; } /** @hide */ @Override @Nullable public String getDeviceDisplayName(ScanResult sr) { if (mRenameBytesFrom < 0 && mRenameNameFrom < 0) { return getDeviceDisplayNameInternal(sr.getDevice()); } final StringBuilder sb = new StringBuilder(TextUtils.emptyIfNull(mRenamePrefix)); if (mRenameBytesFrom >= 0) { final byte[] bytes = sr.getScanRecord().getBytes(); int startInclusive = mRenameBytesFrom; int endInclusive = mRenameBytesFrom + mRenameBytesLength -1; int initial = mRenameBytesReverseOrder ? endInclusive : startInclusive; int step = mRenameBytesReverseOrder ? -1 : 1; for (int i = initial; startInclusive <= i && i <= endInclusive; i += step) { sb.append(Byte.toHexString(bytes[i], true)); } } else { sb.append( getDeviceDisplayNameInternal(sr.getDevice()) .substring(mRenameNameFrom, mRenameNameFrom + mRenameNameLength)); } return sb.append(TextUtils.emptyIfNull(mRenameSuffix)).toString(); } /** @hide */ @Override public boolean matches(ScanResult device) { boolean result = matches(device.getDevice()) && (mRawDataFilter == null || BitUtils.maskedEquals(device.getScanRecord().getBytes(), mRawDataFilter, mRawDataFilterMask)); if (DEBUG) Log.i(LOG_TAG, "matches(this = " + this + ", device = " + device + ") -> " + result); return result; } private boolean matches(BluetoothDevice device) { return BluetoothDeviceFilterUtils.matches(getScanFilter(), device) && BluetoothDeviceFilterUtils.matchesName(getNamePattern(), device); } /** @hide */ @Override public int getMediumType() { return DeviceFilter.MEDIUM_TYPE_BLUETOOTH_LE; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; BluetoothLeDeviceFilter that = (BluetoothLeDeviceFilter) o; return mRenameBytesFrom == that.mRenameBytesFrom && mRenameBytesLength == that.mRenameBytesLength && mRenameNameFrom == that.mRenameNameFrom && mRenameNameLength == that.mRenameNameLength && mRenameBytesReverseOrder == that.mRenameBytesReverseOrder && Objects.equals(mNamePattern, that.mNamePattern) && Objects.equals(mScanFilter, that.mScanFilter) && Arrays.equals(mRawDataFilter, that.mRawDataFilter) && Arrays.equals(mRawDataFilterMask, that.mRawDataFilterMask) && Objects.equals(mRenamePrefix, that.mRenamePrefix) && Objects.equals(mRenameSuffix, that.mRenameSuffix); } @Override public int hashCode() { return Objects.hash(mNamePattern, mScanFilter, mRawDataFilter, mRawDataFilterMask, mRenamePrefix, mRenameSuffix, mRenameBytesFrom, mRenameBytesLength, mRenameNameFrom, mRenameNameLength, mRenameBytesReverseOrder); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(patternToString(getNamePattern())); dest.writeParcelable(mScanFilter, flags); dest.writeByteArray(mRawDataFilter); dest.writeByteArray(mRawDataFilterMask); dest.writeString(mRenamePrefix); dest.writeString(mRenameSuffix); dest.writeInt(mRenameBytesFrom); dest.writeInt(mRenameBytesLength); dest.writeInt(mRenameNameFrom); dest.writeInt(mRenameNameLength); dest.writeBoolean(mRenameBytesReverseOrder); } @Override public int describeContents() { return 0; } @Override public String toString() { return "BluetoothLEDeviceFilter{" + "mNamePattern=" + mNamePattern + ", mScanFilter=" + mScanFilter + ", mRawDataFilter=" + Arrays.toString(mRawDataFilter) + ", mRawDataFilterMask=" + Arrays.toString(mRawDataFilterMask) + ", mRenamePrefix='" + mRenamePrefix + '\'' + ", mRenameSuffix='" + mRenameSuffix + '\'' + ", mRenameBytesFrom=" + mRenameBytesFrom + ", mRenameBytesLength=" + mRenameBytesLength + ", mRenameNameFrom=" + mRenameNameFrom + ", mRenameNameLength=" + mRenameNameLength + ", mRenameBytesReverseOrder=" + mRenameBytesReverseOrder + '}'; } public static final Creator CREATOR = new Creator() { @Override public BluetoothLeDeviceFilter createFromParcel(Parcel in) { Builder builder = new Builder() .setNamePattern(patternFromString(in.readString())) .setScanFilter(in.readParcelable(null)); byte[] rawDataFilter = in.createByteArray(); byte[] rawDataFilterMask = in.createByteArray(); if (rawDataFilter != null) { builder.setRawDataFilter(rawDataFilter, rawDataFilterMask); } String renamePrefix = in.readString(); String suffix = in.readString(); int bytesFrom = in.readInt(); int bytesTo = in.readInt(); int nameFrom = in.readInt(); int nameTo = in.readInt(); boolean bytesReverseOrder = in.readBoolean(); if (renamePrefix != null) { if (bytesFrom >= 0) { builder.setRenameFromBytes(renamePrefix, suffix, bytesFrom, bytesTo, bytesReverseOrder ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN); } else { builder.setRenameFromName(renamePrefix, suffix, nameFrom, nameTo); } } return builder.build(); } @Override public BluetoothLeDeviceFilter[] newArray(int size) { return new BluetoothLeDeviceFilter[size]; } }; public static int getRenamePrefixLengthLimit() { return RENAME_PREFIX_LENGTH_LIMIT; } /** * Builder for {@link BluetoothLeDeviceFilter} */ public static final class Builder extends OneTimeUseBuilder { private ScanFilter mScanFilter; private Pattern mNamePattern; private byte[] mRawDataFilter; private byte[] mRawDataFilterMask; private String mRenamePrefix; private String mRenameSuffix; private int mRenameBytesFrom = -1; private int mRenameBytesLength; private int mRenameNameFrom = -1; private int mRenameNameLength; private boolean mRenameBytesReverseOrder = false; /** * @param regex if set, only devices with {@link BluetoothDevice#getName name} matching the * given regular expression will be shown * @return self for chaining */ public Builder setNamePattern(@Nullable Pattern regex) { checkNotUsed(); mNamePattern = regex; return this; } /** * @param scanFilter a {@link ScanFilter} to filter devices by * * @return self for chaining * @see ScanFilter for specific details on its various fields */ @NonNull public Builder setScanFilter(@Nullable ScanFilter scanFilter) { checkNotUsed(); mScanFilter = scanFilter; return this; } /** * Filter devices by raw advertisement data, as obtained by {@link ScanRecord#getBytes} * * @param rawDataFilter bit values that have to match against advertized data * @param rawDataFilterMask bits that have to be matched * @return self for chaining */ @NonNull public Builder setRawDataFilter(@NonNull byte[] rawDataFilter, @Nullable byte[] rawDataFilterMask) { checkNotUsed(); Preconditions.checkNotNull(rawDataFilter); checkArgument(rawDataFilterMask == null || rawDataFilter.length == rawDataFilterMask.length, "Mask and filter should be the same length"); mRawDataFilter = rawDataFilter; mRawDataFilterMask = rawDataFilterMask; return this; } /** * Rename the devices shown in the list, using specific bytes from the raw advertisement * data ({@link ScanRecord#getBytes}) in hexadecimal format, as well as a custom * prefix/suffix around them * * Note that the prefix length is limited to {@link #getRenamePrefixLengthLimit} characters * to ensure that there's enough space to display the byte data * * The range of bytes to be displayed cannot be empty * * @param prefix to be displayed before the byte data * @param suffix to be displayed after the byte data * @param bytesFrom the start byte index to be displayed (inclusive) * @param bytesLength the number of bytes to be displayed from the given index * @param byteOrder whether the given range of bytes is big endian (will be displayed * in same order) or little endian (will be flipped before displaying) * @return self for chaining */ @NonNull public Builder setRenameFromBytes(@NonNull String prefix, @NonNull String suffix, int bytesFrom, int bytesLength, ByteOrder byteOrder) { checkRenameNotSet(); checkRangeNotEmpty(bytesLength); mRenameBytesFrom = bytesFrom; mRenameBytesLength = bytesLength; mRenameBytesReverseOrder = byteOrder == ByteOrder.LITTLE_ENDIAN; return setRename(prefix, suffix); } /** * Rename the devices shown in the list, using specific characters from the advertised name, * as well as a custom prefix/suffix around them * * Note that the prefix length is limited to {@link #getRenamePrefixLengthLimit} characters * to ensure that there's enough space to display the byte data * * The range of name characters to be displayed cannot be empty * * @param prefix to be displayed before the byte data * @param suffix to be displayed after the byte data * @param nameFrom the start name character index to be displayed (inclusive) * @param nameLength the number of characters to be displayed from the given index * @return self for chaining */ @NonNull public Builder setRenameFromName(@NonNull String prefix, @NonNull String suffix, int nameFrom, int nameLength) { checkRenameNotSet(); checkRangeNotEmpty(nameLength); mRenameNameFrom = nameFrom; mRenameNameLength = nameLength; mRenameBytesReverseOrder = false; return setRename(prefix, suffix); } private void checkRenameNotSet() { checkState(mRenamePrefix == null, "Renaming rule can only be set once"); } private void checkRangeNotEmpty(int length) { checkArgument(length > 0, "Range must be non-empty"); } @NonNull private Builder setRename(@NonNull String prefix, @NonNull String suffix) { checkNotUsed(); checkArgument(TextUtils.length(prefix) <= getRenamePrefixLengthLimit(), "Prefix is too long"); mRenamePrefix = prefix; mRenameSuffix = suffix; return this; } /** @inheritDoc */ @Override @NonNull public BluetoothLeDeviceFilter build() { markUsed(); return new BluetoothLeDeviceFilter(mNamePattern, mScanFilter, mRawDataFilter, mRawDataFilterMask, mRenamePrefix, mRenameSuffix, mRenameBytesFrom, mRenameBytesLength, mRenameNameFrom, mRenameNameLength, mRenameBytesReverseOrder); } } }