content: /*
   * 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);
          }
      }
  }
  