/*
|
* 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.annotation.UnsupportedAppUsage;
|
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
|
@UnsupportedAppUsage
|
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 @android.annotation.NonNull 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);
|
}
|
}
|
}
|