diff options
Diffstat (limited to 'android/companion/BluetoothLeDeviceFilter.java')
-rw-r--r-- | android/companion/BluetoothLeDeviceFilter.java | 434 |
1 files changed, 434 insertions, 0 deletions
diff --git a/android/companion/BluetoothLeDeviceFilter.java b/android/companion/BluetoothLeDeviceFilter.java new file mode 100644 index 00000000..7fb768c6 --- /dev/null +++ b/android/companion/BluetoothLeDeviceFilter.java @@ -0,0 +1,434 @@ +/* + * 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<ScanResult> { + + 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<BluetoothLeDeviceFilter> CREATOR + = new Creator<BluetoothLeDeviceFilter>() { + @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<BluetoothLeDeviceFilter> { + 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); + } + } +} |