Home | History | Annotate | Download | only in companion
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.companion;
     18 
     19 import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal;
     20 import static android.companion.BluetoothDeviceFilterUtils.patternFromString;
     21 import static android.companion.BluetoothDeviceFilterUtils.patternToString;
     22 
     23 import static com.android.internal.util.Preconditions.checkArgument;
     24 import static com.android.internal.util.Preconditions.checkState;
     25 
     26 import android.annotation.NonNull;
     27 import android.annotation.Nullable;
     28 import android.bluetooth.BluetoothDevice;
     29 import android.bluetooth.le.ScanFilter;
     30 import android.bluetooth.le.ScanRecord;
     31 import android.bluetooth.le.ScanResult;
     32 import android.os.Parcel;
     33 import android.provider.OneTimeUseBuilder;
     34 import android.text.TextUtils;
     35 import android.util.Log;
     36 
     37 import com.android.internal.util.BitUtils;
     38 import com.android.internal.util.ObjectUtils;
     39 import com.android.internal.util.Preconditions;
     40 
     41 import java.nio.ByteOrder;
     42 import java.util.Arrays;
     43 import java.util.Objects;
     44 import java.util.regex.Pattern;
     45 
     46 /**
     47  * A filter for Bluetooth LE devices
     48  *
     49  * @see ScanFilter
     50  */
     51 public final class BluetoothLeDeviceFilter implements DeviceFilter<ScanResult> {
     52 
     53     private static final boolean DEBUG = false;
     54     private static final String LOG_TAG = "BluetoothLeDeviceFilter";
     55 
     56     private static final int RENAME_PREFIX_LENGTH_LIMIT = 10;
     57 
     58     private final Pattern mNamePattern;
     59     private final ScanFilter mScanFilter;
     60     private final byte[] mRawDataFilter;
     61     private final byte[] mRawDataFilterMask;
     62     private final String mRenamePrefix;
     63     private final String mRenameSuffix;
     64     private final int mRenameBytesFrom;
     65     private final int mRenameBytesLength;
     66     private final int mRenameNameFrom;
     67     private final int mRenameNameLength;
     68     private final boolean mRenameBytesReverseOrder;
     69 
     70     private BluetoothLeDeviceFilter(Pattern namePattern, ScanFilter scanFilter,
     71             byte[] rawDataFilter, byte[] rawDataFilterMask, String renamePrefix,
     72             String renameSuffix, int renameBytesFrom, int renameBytesLength,
     73             int renameNameFrom, int renameNameLength, boolean renameBytesReverseOrder) {
     74         mNamePattern = namePattern;
     75         mScanFilter = ObjectUtils.firstNotNull(scanFilter, ScanFilter.EMPTY);
     76         mRawDataFilter = rawDataFilter;
     77         mRawDataFilterMask = rawDataFilterMask;
     78         mRenamePrefix = renamePrefix;
     79         mRenameSuffix = renameSuffix;
     80         mRenameBytesFrom = renameBytesFrom;
     81         mRenameBytesLength = renameBytesLength;
     82         mRenameNameFrom = renameNameFrom;
     83         mRenameNameLength = renameNameLength;
     84         mRenameBytesReverseOrder = renameBytesReverseOrder;
     85     }
     86 
     87     /** @hide */
     88     @Nullable
     89     public Pattern getNamePattern() {
     90         return mNamePattern;
     91     }
     92 
     93     /** @hide */
     94     @NonNull
     95     public ScanFilter getScanFilter() {
     96         return mScanFilter;
     97     }
     98 
     99     /** @hide */
    100     @Nullable
    101     public byte[] getRawDataFilter() {
    102         return mRawDataFilter;
    103     }
    104 
    105     /** @hide */
    106     @Nullable
    107     public byte[] getRawDataFilterMask() {
    108         return mRawDataFilterMask;
    109     }
    110 
    111     /** @hide */
    112     @Nullable
    113     public String getRenamePrefix() {
    114         return mRenamePrefix;
    115     }
    116 
    117     /** @hide */
    118     @Nullable
    119     public String getRenameSuffix() {
    120         return mRenameSuffix;
    121     }
    122 
    123     /** @hide */
    124     public int getRenameBytesFrom() {
    125         return mRenameBytesFrom;
    126     }
    127 
    128     /** @hide */
    129     public int getRenameBytesLength() {
    130         return mRenameBytesLength;
    131     }
    132 
    133     /** @hide */
    134     public boolean isRenameBytesReverseOrder() {
    135         return mRenameBytesReverseOrder;
    136     }
    137 
    138     /** @hide */
    139     @Override
    140     @Nullable
    141     public String getDeviceDisplayName(ScanResult sr) {
    142         if (mRenameBytesFrom < 0 && mRenameNameFrom < 0) {
    143             return getDeviceDisplayNameInternal(sr.getDevice());
    144         }
    145         final StringBuilder sb = new StringBuilder(TextUtils.emptyIfNull(mRenamePrefix));
    146         if (mRenameBytesFrom >= 0) {
    147             final byte[] bytes = sr.getScanRecord().getBytes();
    148             int startInclusive = mRenameBytesFrom;
    149             int endInclusive = mRenameBytesFrom + mRenameBytesLength -1;
    150             int initial = mRenameBytesReverseOrder ? endInclusive : startInclusive;
    151             int step = mRenameBytesReverseOrder ? -1 : 1;
    152             for (int i = initial; startInclusive <= i && i <= endInclusive; i += step) {
    153                 sb.append(Byte.toHexString(bytes[i], true));
    154             }
    155         } else {
    156             sb.append(
    157                     getDeviceDisplayNameInternal(sr.getDevice())
    158                             .substring(mRenameNameFrom, mRenameNameFrom + mRenameNameLength));
    159         }
    160         return sb.append(TextUtils.emptyIfNull(mRenameSuffix)).toString();
    161     }
    162 
    163     /** @hide */
    164     @Override
    165     public boolean matches(ScanResult device) {
    166         boolean result = matches(device.getDevice())
    167                 && (mRawDataFilter == null
    168                     || BitUtils.maskedEquals(device.getScanRecord().getBytes(),
    169                             mRawDataFilter, mRawDataFilterMask));
    170         if (DEBUG) Log.i(LOG_TAG, "matches(this = " + this + ", device = " + device +
    171                 ") -> " + result);
    172         return result;
    173     }
    174 
    175     private boolean matches(BluetoothDevice device) {
    176         return BluetoothDeviceFilterUtils.matches(getScanFilter(), device)
    177                 && BluetoothDeviceFilterUtils.matchesName(getNamePattern(), device);
    178     }
    179 
    180     /** @hide */
    181     @Override
    182     public int getMediumType() {
    183         return DeviceFilter.MEDIUM_TYPE_BLUETOOTH_LE;
    184     }
    185 
    186     @Override
    187     public boolean equals(Object o) {
    188         if (this == o) return true;
    189         if (o == null || getClass() != o.getClass()) return false;
    190         BluetoothLeDeviceFilter that = (BluetoothLeDeviceFilter) o;
    191         return mRenameBytesFrom == that.mRenameBytesFrom &&
    192                 mRenameBytesLength == that.mRenameBytesLength &&
    193                 mRenameNameFrom == that.mRenameNameFrom &&
    194                 mRenameNameLength == that.mRenameNameLength &&
    195                 mRenameBytesReverseOrder == that.mRenameBytesReverseOrder &&
    196                 Objects.equals(mNamePattern, that.mNamePattern) &&
    197                 Objects.equals(mScanFilter, that.mScanFilter) &&
    198                 Arrays.equals(mRawDataFilter, that.mRawDataFilter) &&
    199                 Arrays.equals(mRawDataFilterMask, that.mRawDataFilterMask) &&
    200                 Objects.equals(mRenamePrefix, that.mRenamePrefix) &&
    201                 Objects.equals(mRenameSuffix, that.mRenameSuffix);
    202     }
    203 
    204     @Override
    205     public int hashCode() {
    206         return Objects.hash(mNamePattern, mScanFilter, mRawDataFilter, mRawDataFilterMask,
    207                 mRenamePrefix, mRenameSuffix, mRenameBytesFrom, mRenameBytesLength,
    208                 mRenameNameFrom, mRenameNameLength, mRenameBytesReverseOrder);
    209     }
    210 
    211     @Override
    212     public void writeToParcel(Parcel dest, int flags) {
    213         dest.writeString(patternToString(getNamePattern()));
    214         dest.writeParcelable(mScanFilter, flags);
    215         dest.writeByteArray(mRawDataFilter);
    216         dest.writeByteArray(mRawDataFilterMask);
    217         dest.writeString(mRenamePrefix);
    218         dest.writeString(mRenameSuffix);
    219         dest.writeInt(mRenameBytesFrom);
    220         dest.writeInt(mRenameBytesLength);
    221         dest.writeInt(mRenameNameFrom);
    222         dest.writeInt(mRenameNameLength);
    223         dest.writeBoolean(mRenameBytesReverseOrder);
    224     }
    225 
    226     @Override
    227     public int describeContents() {
    228         return 0;
    229     }
    230 
    231     @Override
    232     public String toString() {
    233         return "BluetoothLEDeviceFilter{" +
    234                 "mNamePattern=" + mNamePattern +
    235                 ", mScanFilter=" + mScanFilter +
    236                 ", mRawDataFilter=" + Arrays.toString(mRawDataFilter) +
    237                 ", mRawDataFilterMask=" + Arrays.toString(mRawDataFilterMask) +
    238                 ", mRenamePrefix='" + mRenamePrefix + '\'' +
    239                 ", mRenameSuffix='" + mRenameSuffix + '\'' +
    240                 ", mRenameBytesFrom=" + mRenameBytesFrom +
    241                 ", mRenameBytesLength=" + mRenameBytesLength +
    242                 ", mRenameNameFrom=" + mRenameNameFrom +
    243                 ", mRenameNameLength=" + mRenameNameLength +
    244                 ", mRenameBytesReverseOrder=" + mRenameBytesReverseOrder +
    245                 '}';
    246     }
    247 
    248     public static final Creator<BluetoothLeDeviceFilter> CREATOR
    249             = new Creator<BluetoothLeDeviceFilter>() {
    250         @Override
    251         public BluetoothLeDeviceFilter createFromParcel(Parcel in) {
    252             Builder builder = new Builder()
    253                     .setNamePattern(patternFromString(in.readString()))
    254                     .setScanFilter(in.readParcelable(null));
    255             byte[] rawDataFilter = in.createByteArray();
    256             byte[] rawDataFilterMask = in.createByteArray();
    257             if (rawDataFilter != null) {
    258                 builder.setRawDataFilter(rawDataFilter, rawDataFilterMask);
    259             }
    260             String renamePrefix = in.readString();
    261             String suffix = in.readString();
    262             int bytesFrom = in.readInt();
    263             int bytesTo = in.readInt();
    264             int nameFrom = in.readInt();
    265             int nameTo = in.readInt();
    266             boolean bytesReverseOrder = in.readBoolean();
    267             if (renamePrefix != null) {
    268                 if (bytesFrom >= 0) {
    269                     builder.setRenameFromBytes(renamePrefix, suffix, bytesFrom, bytesTo,
    270                             bytesReverseOrder ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
    271                 } else {
    272                     builder.setRenameFromName(renamePrefix, suffix, nameFrom, nameTo);
    273                 }
    274             }
    275             return builder.build();
    276         }
    277 
    278         @Override
    279         public BluetoothLeDeviceFilter[] newArray(int size) {
    280             return new BluetoothLeDeviceFilter[size];
    281         }
    282     };
    283 
    284     public static int getRenamePrefixLengthLimit() {
    285         return RENAME_PREFIX_LENGTH_LIMIT;
    286     }
    287 
    288     /**
    289      * Builder for {@link BluetoothLeDeviceFilter}
    290      */
    291     public static final class Builder extends OneTimeUseBuilder<BluetoothLeDeviceFilter> {
    292         private ScanFilter mScanFilter;
    293         private Pattern mNamePattern;
    294         private byte[] mRawDataFilter;
    295         private byte[] mRawDataFilterMask;
    296         private String mRenamePrefix;
    297         private String mRenameSuffix;
    298         private int mRenameBytesFrom = -1;
    299         private int mRenameBytesLength;
    300         private int mRenameNameFrom = -1;
    301         private int mRenameNameLength;
    302         private boolean mRenameBytesReverseOrder = false;
    303 
    304         /**
    305          * @param regex if set, only devices with {@link BluetoothDevice#getName name} matching the
    306          *              given regular expression will be shown
    307          * @return self for chaining
    308          */
    309         public Builder setNamePattern(@Nullable Pattern regex) {
    310             checkNotUsed();
    311             mNamePattern = regex;
    312             return this;
    313         }
    314 
    315         /**
    316          * @param scanFilter a {@link ScanFilter} to filter devices by
    317          *
    318          * @return self for chaining
    319          * @see ScanFilter for specific details on its various fields
    320          */
    321         @NonNull
    322         public Builder setScanFilter(@Nullable ScanFilter scanFilter) {
    323             checkNotUsed();
    324             mScanFilter = scanFilter;
    325             return this;
    326         }
    327 
    328         /**
    329          * Filter devices by raw advertisement data, as obtained by {@link ScanRecord#getBytes}
    330          *
    331          * @param rawDataFilter bit values that have to match against advertized data
    332          * @param rawDataFilterMask bits that have to be matched
    333          * @return self for chaining
    334          */
    335         @NonNull
    336         public Builder setRawDataFilter(@NonNull byte[] rawDataFilter,
    337                 @Nullable byte[] rawDataFilterMask) {
    338             checkNotUsed();
    339             Preconditions.checkNotNull(rawDataFilter);
    340             checkArgument(rawDataFilterMask == null ||
    341                     rawDataFilter.length == rawDataFilterMask.length,
    342                     "Mask and filter should be the same length");
    343             mRawDataFilter = rawDataFilter;
    344             mRawDataFilterMask = rawDataFilterMask;
    345             return this;
    346         }
    347 
    348         /**
    349          * Rename the devices shown in the list, using specific bytes from the raw advertisement
    350          * data ({@link ScanRecord#getBytes}) in hexadecimal format, as well as a custom
    351          * prefix/suffix around them
    352          *
    353          * Note that the prefix length is limited to {@link #getRenamePrefixLengthLimit} characters
    354          * to ensure that there's enough space to display the byte data
    355          *
    356          * The range of bytes to be displayed cannot be empty
    357          *
    358          * @param prefix to be displayed before the byte data
    359          * @param suffix to be displayed after the byte data
    360          * @param bytesFrom the start byte index to be displayed (inclusive)
    361          * @param bytesLength the number of bytes to be displayed from the given index
    362          * @param byteOrder whether the given range of bytes is big endian (will be displayed
    363          *                   in same order) or little endian (will be flipped before displaying)
    364          * @return self for chaining
    365          */
    366         @NonNull
    367         public Builder setRenameFromBytes(@NonNull String prefix, @NonNull String suffix,
    368                 int bytesFrom, int bytesLength, ByteOrder byteOrder) {
    369             checkRenameNotSet();
    370             checkRangeNotEmpty(bytesLength);
    371             mRenameBytesFrom = bytesFrom;
    372             mRenameBytesLength = bytesLength;
    373             mRenameBytesReverseOrder = byteOrder == ByteOrder.LITTLE_ENDIAN;
    374             return setRename(prefix, suffix);
    375         }
    376 
    377         /**
    378          * Rename the devices shown in the list, using specific characters from the advertised name,
    379          * as well as a custom prefix/suffix around them
    380          *
    381          * Note that the prefix length is limited to {@link #getRenamePrefixLengthLimit} characters
    382          * to ensure that there's enough space to display the byte data
    383          *
    384          * The range of name characters to be displayed cannot be empty
    385          *
    386          * @param prefix to be displayed before the byte data
    387          * @param suffix to be displayed after the byte data
    388          * @param nameFrom the start name character index to be displayed (inclusive)
    389          * @param nameLength the number of characters to be displayed from the given index
    390          * @return self for chaining
    391          */
    392         @NonNull
    393         public Builder setRenameFromName(@NonNull String prefix, @NonNull String suffix,
    394                 int nameFrom, int nameLength) {
    395             checkRenameNotSet();
    396             checkRangeNotEmpty(nameLength);
    397             mRenameNameFrom = nameFrom;
    398             mRenameNameLength = nameLength;
    399             mRenameBytesReverseOrder = false;
    400             return setRename(prefix, suffix);
    401         }
    402 
    403         private void checkRenameNotSet() {
    404             checkState(mRenamePrefix == null, "Renaming rule can only be set once");
    405         }
    406 
    407         private void checkRangeNotEmpty(int length) {
    408             checkArgument(length > 0, "Range must be non-empty");
    409         }
    410 
    411         @NonNull
    412         private Builder setRename(@NonNull String prefix, @NonNull String suffix) {
    413             checkNotUsed();
    414             checkArgument(TextUtils.length(prefix) <= getRenamePrefixLengthLimit(),
    415                     "Prefix is too long");
    416             mRenamePrefix = prefix;
    417             mRenameSuffix = suffix;
    418             return this;
    419         }
    420 
    421         /** @inheritDoc */
    422         @Override
    423         @NonNull
    424         public BluetoothLeDeviceFilter build() {
    425             markUsed();
    426             return new BluetoothLeDeviceFilter(mNamePattern, mScanFilter,
    427                     mRawDataFilter, mRawDataFilterMask,
    428                     mRenamePrefix, mRenameSuffix,
    429                     mRenameBytesFrom, mRenameBytesLength,
    430                     mRenameNameFrom, mRenameNameLength,
    431                     mRenameBytesReverseOrder);
    432         }
    433     }
    434 }
    435