From 36e866b8e0ec08e45b5e7fbc65aeeb3a9bb7b11e Mon Sep 17 00:00:00 2001 From: Eugene Susla Date: Thu, 23 Feb 2017 18:24:39 -0800 Subject: [PATCH] Support multiple filters per association request By supporting multiple filters per one request we should be able to cover multiple kinds of use cases such as: - Letting the user select from a list of devices of more then one medium type (e.g. Bluetooth and BLE) - Allowing to provide multiple criteria for any field (e.g. filtering by more than one service UUID) Bug: 30932767 Test: Provide multiple filters and ensure that devices matching either are shown in the list to choose from. Ensure wifi SSIDs are shown in the list if wifi filter is provided Change-Id: I0a978787551a1ee5750ec5544b241d3bbfed5a7c --- api/current.txt | 30 ++- api/system-current.txt | 30 ++- api/test-current.txt | 30 ++- .../java/android/bluetooth/le/ScanFilter.java | 12 +- .../android/companion/AssociationRequest.java | 107 +++------- .../companion/BluetoothDeviceFilter.java | 32 ++- .../companion/BluetoothDeviceFilterUtils.java | 45 +++- .../companion/BluetoothLEDeviceFilter.java | 199 +++++++++++++++--- .../companion/CompanionDeviceManager.java | 2 +- core/java/android/companion/DeviceFilter.java | 24 ++- .../android/companion/WifiDeviceFilter.java | 125 +++++++++++ core/java/android/os/Parcel.java | 15 +- core/java/android/text/TextUtils.java | 5 + .../com/android/internal/util/ArrayUtils.java | 29 +++ .../com/android/internal/util/BitUtils.java | 58 +++++ .../AndroidManifest.xml | 2 + .../DeviceChooserActivity.java | 16 +- .../DeviceDiscoveryService.java | 197 +++++++++++++---- .../print/CompanionDeviceManagerService.java | 2 +- 19 files changed, 738 insertions(+), 222 deletions(-) create mode 100644 core/java/android/companion/WifiDeviceFilter.java create mode 100644 core/java/com/android/internal/util/BitUtils.java diff --git a/api/current.txt b/api/current.txt index d837786a10b5c..ab7a163b4e593 100644 --- a/api/current.txt +++ b/api/current.txt @@ -8183,18 +8183,17 @@ package android.bluetooth.le { package android.companion { - public final class AssociationRequest implements android.os.Parcelable { + public final class AssociationRequest implements android.os.Parcelable { method public int describeContents(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; } - public static final class AssociationRequest.Builder { - method public android.companion.AssociationRequest build(); - method public static android.companion.AssociationRequest.Builder createForBluetoothDevice(); - method public static android.companion.AssociationRequest.Builder createForBluetoothLEDevice(); - method public android.companion.AssociationRequest.Builder setDeviceFilter(F); - method public android.companion.AssociationRequest.Builder setSingleDevice(boolean); + public static final class AssociationRequest.Builder { + ctor public AssociationRequest.Builder(); + method public android.companion.AssociationRequest.Builder addDeviceFilter(android.companion.DeviceFilter); + method public android.companion.AssociationRequest build(); + method public android.companion.AssociationRequest.Builder setSingleDevice(boolean); } public final class BluetoothDeviceFilter implements android.companion.DeviceFilter { @@ -8213,6 +8212,7 @@ package android.companion { public final class BluetoothLEDeviceFilter implements android.companion.DeviceFilter { method public int describeContents(); + method public static int getRenamePrefixLengthLimit(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; } @@ -8221,11 +8221,13 @@ package android.companion { ctor public BluetoothLEDeviceFilter.Builder(); method public android.companion.BluetoothLEDeviceFilter build(); method public android.companion.BluetoothLEDeviceFilter.Builder setNamePattern(java.util.regex.Pattern); + method public android.companion.BluetoothLEDeviceFilter.Builder setRawDataFilter(byte[], byte[]); + method public android.companion.BluetoothLEDeviceFilter.Builder setRename(java.lang.String, java.lang.String, int, int, boolean); method public android.companion.BluetoothLEDeviceFilter.Builder setScanFilter(android.bluetooth.le.ScanFilter); } public final class CompanionDeviceManager { - method public void associate(android.companion.AssociationRequest, android.companion.CompanionDeviceManager.Callback, android.os.Handler); + method public void associate(android.companion.AssociationRequest, android.companion.CompanionDeviceManager.Callback, android.os.Handler); method public void disassociate(java.lang.String); method public java.util.List getAssociations(); field public static final java.lang.String EXTRA_DEVICE = "android.companion.extra.DEVICE"; @@ -8240,6 +8242,18 @@ package android.companion { public abstract interface DeviceFilter implements android.os.Parcelable { } + public final class WifiDeviceFilter implements android.companion.DeviceFilter { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class WifiDeviceFilter.Builder { + ctor public WifiDeviceFilter.Builder(); + method public android.companion.WifiDeviceFilter build(); + method public android.companion.WifiDeviceFilter.Builder setNamePattern(java.util.regex.Pattern); + } + } package android.content { diff --git a/api/system-current.txt b/api/system-current.txt index b95f27a8df60c..98ba7ebc64192 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -8683,18 +8683,17 @@ package android.bluetooth.le { package android.companion { - public final class AssociationRequest implements android.os.Parcelable { + public final class AssociationRequest implements android.os.Parcelable { method public int describeContents(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; } - public static final class AssociationRequest.Builder { - method public android.companion.AssociationRequest build(); - method public static android.companion.AssociationRequest.Builder createForBluetoothDevice(); - method public static android.companion.AssociationRequest.Builder createForBluetoothLEDevice(); - method public android.companion.AssociationRequest.Builder setDeviceFilter(F); - method public android.companion.AssociationRequest.Builder setSingleDevice(boolean); + public static final class AssociationRequest.Builder { + ctor public AssociationRequest.Builder(); + method public android.companion.AssociationRequest.Builder addDeviceFilter(android.companion.DeviceFilter); + method public android.companion.AssociationRequest build(); + method public android.companion.AssociationRequest.Builder setSingleDevice(boolean); } public final class BluetoothDeviceFilter implements android.companion.DeviceFilter { @@ -8713,6 +8712,7 @@ package android.companion { public final class BluetoothLEDeviceFilter implements android.companion.DeviceFilter { method public int describeContents(); + method public static int getRenamePrefixLengthLimit(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; } @@ -8721,11 +8721,13 @@ package android.companion { ctor public BluetoothLEDeviceFilter.Builder(); method public android.companion.BluetoothLEDeviceFilter build(); method public android.companion.BluetoothLEDeviceFilter.Builder setNamePattern(java.util.regex.Pattern); + method public android.companion.BluetoothLEDeviceFilter.Builder setRawDataFilter(byte[], byte[]); + method public android.companion.BluetoothLEDeviceFilter.Builder setRename(java.lang.String, java.lang.String, int, int, boolean); method public android.companion.BluetoothLEDeviceFilter.Builder setScanFilter(android.bluetooth.le.ScanFilter); } public final class CompanionDeviceManager { - method public void associate(android.companion.AssociationRequest, android.companion.CompanionDeviceManager.Callback, android.os.Handler); + method public void associate(android.companion.AssociationRequest, android.companion.CompanionDeviceManager.Callback, android.os.Handler); method public void disassociate(java.lang.String); method public java.util.List getAssociations(); field public static final java.lang.String EXTRA_DEVICE = "android.companion.extra.DEVICE"; @@ -8740,6 +8742,18 @@ package android.companion { public abstract interface DeviceFilter implements android.os.Parcelable { } + public final class WifiDeviceFilter implements android.companion.DeviceFilter { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class WifiDeviceFilter.Builder { + ctor public WifiDeviceFilter.Builder(); + method public android.companion.WifiDeviceFilter build(); + method public android.companion.WifiDeviceFilter.Builder setNamePattern(java.util.regex.Pattern); + } + } package android.content { diff --git a/api/test-current.txt b/api/test-current.txt index 51ff912569134..8ece524a77a8f 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -8210,18 +8210,17 @@ package android.bluetooth.le { package android.companion { - public final class AssociationRequest implements android.os.Parcelable { + public final class AssociationRequest implements android.os.Parcelable { method public int describeContents(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; } - public static final class AssociationRequest.Builder { - method public android.companion.AssociationRequest build(); - method public static android.companion.AssociationRequest.Builder createForBluetoothDevice(); - method public static android.companion.AssociationRequest.Builder createForBluetoothLEDevice(); - method public android.companion.AssociationRequest.Builder setDeviceFilter(F); - method public android.companion.AssociationRequest.Builder setSingleDevice(boolean); + public static final class AssociationRequest.Builder { + ctor public AssociationRequest.Builder(); + method public android.companion.AssociationRequest.Builder addDeviceFilter(android.companion.DeviceFilter); + method public android.companion.AssociationRequest build(); + method public android.companion.AssociationRequest.Builder setSingleDevice(boolean); } public final class BluetoothDeviceFilter implements android.companion.DeviceFilter { @@ -8240,6 +8239,7 @@ package android.companion { public final class BluetoothLEDeviceFilter implements android.companion.DeviceFilter { method public int describeContents(); + method public static int getRenamePrefixLengthLimit(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; } @@ -8248,11 +8248,13 @@ package android.companion { ctor public BluetoothLEDeviceFilter.Builder(); method public android.companion.BluetoothLEDeviceFilter build(); method public android.companion.BluetoothLEDeviceFilter.Builder setNamePattern(java.util.regex.Pattern); + method public android.companion.BluetoothLEDeviceFilter.Builder setRawDataFilter(byte[], byte[]); + method public android.companion.BluetoothLEDeviceFilter.Builder setRename(java.lang.String, java.lang.String, int, int, boolean); method public android.companion.BluetoothLEDeviceFilter.Builder setScanFilter(android.bluetooth.le.ScanFilter); } public final class CompanionDeviceManager { - method public void associate(android.companion.AssociationRequest, android.companion.CompanionDeviceManager.Callback, android.os.Handler); + method public void associate(android.companion.AssociationRequest, android.companion.CompanionDeviceManager.Callback, android.os.Handler); method public void disassociate(java.lang.String); method public java.util.List getAssociations(); field public static final java.lang.String EXTRA_DEVICE = "android.companion.extra.DEVICE"; @@ -8267,6 +8269,18 @@ package android.companion { public abstract interface DeviceFilter implements android.os.Parcelable { } + public final class WifiDeviceFilter implements android.companion.DeviceFilter { + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class WifiDeviceFilter.Builder { + ctor public WifiDeviceFilter.Builder(); + method public android.companion.WifiDeviceFilter build(); + method public android.companion.WifiDeviceFilter.Builder setNamePattern(java.util.regex.Pattern); + } + } package android.content { diff --git a/core/java/android/bluetooth/le/ScanFilter.java b/core/java/android/bluetooth/le/ScanFilter.java index b89c64a8cac67..457096bf843ef 100644 --- a/core/java/android/bluetooth/le/ScanFilter.java +++ b/core/java/android/bluetooth/le/ScanFilter.java @@ -23,6 +23,8 @@ import android.os.Parcel; import android.os.ParcelUuid; import android.os.Parcelable; +import com.android.internal.util.BitUtils; + import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -345,15 +347,7 @@ public final class ScanFilter implements Parcelable { // Check if the uuid pattern matches the particular service uuid. private static boolean matchesServiceUuid(UUID uuid, UUID mask, UUID data) { - if (mask == null) { - return uuid.equals(data); - } - if ((uuid.getLeastSignificantBits() & mask.getLeastSignificantBits()) != - (data.getLeastSignificantBits() & mask.getLeastSignificantBits())) { - return false; - } - return ((uuid.getMostSignificantBits() & mask.getMostSignificantBits()) == - (data.getMostSignificantBits() & mask.getMostSignificantBits())); + return BitUtils.maskedEquals(data, uuid, mask); } // Check whether the data pattern matches the parsed data. diff --git a/core/java/android/companion/AssociationRequest.java b/core/java/android/companion/AssociationRequest.java index d477f43ac8c27..56f5d4483270a 100644 --- a/core/java/android/companion/AssociationRequest.java +++ b/core/java/android/companion/AssociationRequest.java @@ -16,20 +16,21 @@ package android.companion; -import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; import android.provider.OneTimeUseBuilder; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +import com.android.internal.util.ArrayUtils; + +import java.util.ArrayList; +import java.util.List; /** * A request for the user to select a companion device to associate with. * - * You can optionally set a {@link Builder#setDeviceFilter filter} for which devices to show to the + * You can optionally set {@link Builder#addDeviceFilter filters} for which devices to show to the * user to select from. * The exact type and fields of the filter you can set depend on the * medium type. See {@link Builder}'s static factory methods for specific protocols that are @@ -37,38 +38,22 @@ import java.lang.annotation.RetentionPolicy; * * You can also set {@link Builder#setSingleDevice single device} to request a popup with single * device to be shown instead of a list to choose from - * - * @param Device filter type */ -public final class AssociationRequest implements Parcelable { - - /** @hide */ - public static final int MEDIUM_TYPE_BLUETOOTH = 0; - /** @hide */ - public static final int MEDIUM_TYPE_BLUETOOTH_LE = 1; - /** @hide */ - public static final int MEDIUM_TYPE_WIFI = 2; - - /** @hide */ - @IntDef({MEDIUM_TYPE_BLUETOOTH, MEDIUM_TYPE_BLUETOOTH_LE, MEDIUM_TYPE_WIFI}) - @Retention(RetentionPolicy.SOURCE) - public @interface MediumType {} +public final class AssociationRequest implements Parcelable { private final boolean mSingleDevice; - private final int mMediumType; - private final F mDeviceFilter; + private final List> mDeviceFilters; - private AssociationRequest(boolean singleDevice, int mMediumType, F deviceFilter) { + private AssociationRequest( + boolean singleDevice, @Nullable List> deviceFilters) { this.mSingleDevice = singleDevice; - this.mMediumType = mMediumType; - this.mDeviceFilter = deviceFilter; + this.mDeviceFilters = ArrayUtils.emptyIfNull(deviceFilters); } private AssociationRequest(Parcel in) { this( in.readByte() != 0, - in.readInt(), - in.readParcelable(AssociationRequest.class.getClassLoader())); + in.readParcelableList(new ArrayList<>(), AssociationRequest.class.getClassLoader())); } /** @hide */ @@ -77,22 +62,15 @@ public final class AssociationRequest implements Parcela } /** @hide */ - @MediumType - public int getMediumType() { - return mMediumType; - } - - /** @hide */ - @Nullable - public F getDeviceFilter() { - return mDeviceFilter; + @NonNull + public List> getDeviceFilters() { + return mDeviceFilters; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeByte((byte) (mSingleDevice ? 1 : 0)); - dest.writeInt(mMediumType); - dest.writeParcelable(mDeviceFilter, flags); + dest.writeParcelableList(mDeviceFilters, flags); } @Override @@ -114,45 +92,19 @@ public final class AssociationRequest implements Parcela /** * A builder for {@link AssociationRequest} - * - * @param the type of filter for the request. */ - public static final class Builder - extends OneTimeUseBuilder> { + public static final class Builder extends OneTimeUseBuilder { private boolean mSingleDevice = false; - @MediumType private int mMediumType; - @Nullable private F mDeviceFilter = null; + @Nullable private ArrayList> mDeviceFilters = null; - private Builder() {} - - /** - * Create a new builder for an association request with a Bluetooth LE device - */ - @NonNull - public static Builder createForBluetoothLEDevice() { - return new Builder() - .setMediumType(MEDIUM_TYPE_BLUETOOTH_LE); - } - - /** - * Create a new builder for an association request with a Bluetooth(non-LE) device - */ - @NonNull - public static Builder createForBluetoothDevice() { - return new Builder() - .setMediumType(MEDIUM_TYPE_BLUETOOTH); - } - - //TODO implement, once specific filter classes are available -// public static Builder<> createForWiFiDevice() -// public static Builder<> createForNanDevice() + public Builder() {} /** * @param singleDevice if true, scanning for a device will stop as soon as at least one * fitting device is found */ @NonNull - public Builder setSingleDevice(boolean singleDevice) { + public Builder setSingleDevice(boolean singleDevice) { checkNotUsed(); this.mSingleDevice = singleDevice; return this; @@ -163,29 +115,20 @@ public final class AssociationRequest implements Parcela * user */ @NonNull - public Builder setDeviceFilter(@Nullable F deviceFilter) { + public Builder addDeviceFilter(@Nullable DeviceFilter deviceFilter) { checkNotUsed(); - this.mDeviceFilter = deviceFilter; - return this; - } - - /** - * @param deviceType A type of medium over which to discover devices - * - * @see MediumType - */ - @NonNull - private Builder setMediumType(@MediumType int deviceType) { - mMediumType = deviceType; + if (deviceFilter != null) { + mDeviceFilters = ArrayUtils.add(mDeviceFilters, deviceFilter); + } return this; } /** @inheritDoc */ @NonNull @Override - public AssociationRequest build() { + public AssociationRequest build() { markUsed(); - return new AssociationRequest<>(mSingleDevice, mMediumType, mDeviceFilter); + return new AssociationRequest(mSingleDevice, mDeviceFilters); } } } diff --git a/core/java/android/companion/BluetoothDeviceFilter.java b/core/java/android/companion/BluetoothDeviceFilter.java index 5a69955429aad..0f16b7b90165a 100644 --- a/core/java/android/companion/BluetoothDeviceFilter.java +++ b/core/java/android/companion/BluetoothDeviceFilter.java @@ -16,6 +16,7 @@ package android.companion; +import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal; import static android.companion.BluetoothDeviceFilterUtils.matchesAddress; import static android.companion.BluetoothDeviceFilterUtils.matchesName; import static android.companion.BluetoothDeviceFilterUtils.matchesServiceUuids; @@ -40,8 +41,6 @@ import java.util.regex.Pattern; */ public final class BluetoothDeviceFilter implements DeviceFilter { - private static BluetoothDeviceFilter NO_OP; - private final Pattern mNamePattern; private final String mAddress; private final List mServiceUuids; @@ -67,22 +66,7 @@ public final class BluetoothDeviceFilter implements DeviceFilter readUuids(Parcel in) { - final ArrayList list = new ArrayList<>(); - in.readParcelableList(list, ParcelUuid.class.getClassLoader()); - return list; - } - - /** @hide */ - @NonNull - public static BluetoothDeviceFilter nullsafe(@Nullable BluetoothDeviceFilter nullable) { - return nullable != null ? nullable : noOp(); - } - - /** @hide */ - @NonNull - public static BluetoothDeviceFilter noOp() { - if (NO_OP == null) NO_OP = new Builder().build(); - return NO_OP; + return in.readParcelableList(new ArrayList<>(), ParcelUuid.class.getClassLoader()); } /** @hide */ @@ -93,6 +77,18 @@ public final class BluetoothDeviceFilter implements DeviceFilter { +public final class BluetoothLEDeviceFilter implements DeviceFilter { - private static BluetoothLEDeviceFilter NO_OP; + 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 mRenameBytesTo; + private final boolean mRenameBytesReverseOrder; - private BluetoothLEDeviceFilter(Pattern namePattern, ScanFilter scanFilter) { + private BluetoothLEDeviceFilter(Pattern namePattern, ScanFilter scanFilter, + byte[] rawDataFilter, byte[] rawDataFilterMask, String renamePrefix, + String renameSuffix, int renameBytesFrom, int renameBytesTo, + boolean renameBytesReverseOrder) { mNamePattern = namePattern; mScanFilter = ObjectUtils.firstNotNull(scanFilter, ScanFilter.EMPTY); - } - - @SuppressLint("ParcelClassLoader") - private BluetoothLEDeviceFilter(Parcel in) { - this( - patternFromString(in.readString()), - in.readParcelable(null)); - } - - /** @hide */ - @NonNull - public static BluetoothLEDeviceFilter nullsafe(@Nullable BluetoothLEDeviceFilter nullable) { - return nullable != null ? nullable : noOp(); - } - - /** @hide */ - @NonNull - public static BluetoothLEDeviceFilter noOp() { - if (NO_OP == null) NO_OP = new Builder().build(); - return NO_OP; + mRawDataFilter = rawDataFilter; + mRawDataFilterMask = rawDataFilterMask; + mRenamePrefix = renamePrefix; + mRenameSuffix = renameSuffix; + mRenameBytesFrom = renameBytesFrom; + mRenameBytesTo = renameBytesTo; + mRenameBytesReverseOrder = renameBytesReverseOrder; } /** @hide */ @@ -80,13 +84,81 @@ public final class BluetoothLEDeviceFilter implements DeviceFilter() { @Override public BluetoothLEDeviceFilter createFromParcel(Parcel in) { - return new BluetoothLEDeviceFilter(in); + return new BluetoothLEDeviceFilter.Builder() + .setNamePattern(patternFromString(in.readString())) + .setScanFilter(in.readParcelable(null)) + .setRawDataFilter(in.readBlob(), in.readBlob()) + .setRename(in.readString(), in.readString(), + in.readInt(), in.readInt(), in.readBoolean()) + .build(); } @Override @@ -111,16 +189,28 @@ public final class BluetoothLEDeviceFilter implements DeviceFilter { private ScanFilter mScanFilter; private Pattern mNamePattern; + private byte[] mRawDataFilter; + private byte[] mRawDataFilterMask; + private String mRenamePrefix; + private String mRenameSuffix; + private int mRenameBytesFrom = -1; + private int mRenameBytesTo; + 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(); @@ -131,6 +221,7 @@ public final class BluetoothLEDeviceFilter implements DeviceFilter= getRenamePrefixLengthLimit(), + "Prefix is too short"); + mRenamePrefix = prefix; + mRenameSuffix = suffix; + checkArgument(bytesFrom < bytesTo, "Byte range must be non-empty"); + mRenameBytesFrom = bytesFrom; + mRenameBytesTo = bytesTo; + mRenameBytesReverseOrder = bytesReverseOrder; + return this; + } + /** @inheritDoc */ @Override @NonNull public BluetoothLEDeviceFilter build() { markUsed(); - return new BluetoothLEDeviceFilter(mNamePattern, mScanFilter); + return new BluetoothLEDeviceFilter(mNamePattern, mScanFilter, + mRawDataFilter, mRawDataFilterMask, + mRenamePrefix, mRenameSuffix, + mRenameBytesFrom, mRenameBytesTo, mRenameBytesReverseOrder); } } } diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index 6fa32b4b6944e..5710ad1318d7c 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -117,7 +117,7 @@ public final class CompanionDeviceManager { * @see AssociationRequest */ public void associate( - @NonNull AssociationRequest request, + @NonNull AssociationRequest request, @NonNull Callback callback, @Nullable Handler handler) { final Handler finalHandler = handler != null diff --git a/core/java/android/companion/DeviceFilter.java b/core/java/android/companion/DeviceFilter.java index 8362b2dab8fd8..9b4fdfdf51087 100644 --- a/core/java/android/companion/DeviceFilter.java +++ b/core/java/android/companion/DeviceFilter.java @@ -17,17 +17,28 @@ package android.companion; +import android.annotation.IntDef; import android.annotation.Nullable; import android.os.Parcelable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * A filter for companion devices of type {@code D} * * @param Type of devices, filtered by this filter, - * e.g. {@link android.bluetooth.BluetoothDevice}, {@link android.net.wifi.WifiInfo} + * e.g. {@link android.bluetooth.BluetoothDevice}, {@link android.net.wifi.ScanResult} */ public interface DeviceFilter extends Parcelable { + /** @hide */ + int MEDIUM_TYPE_BLUETOOTH = 0; + /** @hide */ + int MEDIUM_TYPE_BLUETOOTH_LE = 1; + /** @hide */ + int MEDIUM_TYPE_WIFI = 2; + /** * @return whether the given device matches this filter * @@ -35,6 +46,12 @@ public interface DeviceFilter extends Parcelable { */ boolean matches(D device); + /** @hide */ + String getDeviceDisplayName(D device); + + /** @hide */ + @MediumType int getMediumType(); + /** * A nullsafe {@link #matches(Parcelable)}, returning true if the filter is null * @@ -43,4 +60,9 @@ public interface DeviceFilter extends Parcelable { static boolean matches(@Nullable DeviceFilter filter, D device) { return filter == null || filter.matches(device); } + + /** @hide */ + @IntDef({MEDIUM_TYPE_BLUETOOTH, MEDIUM_TYPE_BLUETOOTH_LE, MEDIUM_TYPE_WIFI}) + @Retention(RetentionPolicy.SOURCE) + @interface MediumType {} } diff --git a/core/java/android/companion/WifiDeviceFilter.java b/core/java/android/companion/WifiDeviceFilter.java new file mode 100644 index 0000000000000..1ab9ce11cb0f3 --- /dev/null +++ b/core/java/android/companion/WifiDeviceFilter.java @@ -0,0 +1,125 @@ +/* + * 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 android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.ScanFilter; +import android.net.wifi.ScanResult; +import android.os.Parcel; +import android.provider.OneTimeUseBuilder; + +import java.util.regex.Pattern; + +/** + * A filter for Wifi devices + * + * @see ScanFilter + */ +public final class WifiDeviceFilter implements DeviceFilter { + + private final Pattern mNamePattern; + + private WifiDeviceFilter(Pattern namePattern) { + mNamePattern = namePattern; + } + + @SuppressLint("ParcelClassLoader") + private WifiDeviceFilter(Parcel in) { + this(patternFromString(in.readString())); + } + + /** @hide */ + @Nullable + public Pattern getNamePattern() { + return mNamePattern; + } + + + /** @hide */ + @Override + public boolean matches(ScanResult device) { + return BluetoothDeviceFilterUtils.matchesName(getNamePattern(), device); + } + + /** @hide */ + @Override + public String getDeviceDisplayName(ScanResult device) { + return getDeviceDisplayNameInternal(device); + } + + /** @hide */ + @Override + public int getMediumType() { + return MEDIUM_TYPE_WIFI; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(patternToString(getNamePattern())); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR + = new Creator() { + @Override + public WifiDeviceFilter createFromParcel(Parcel in) { + return new WifiDeviceFilter(in); + } + + @Override + public WifiDeviceFilter[] newArray(int size) { + return new WifiDeviceFilter[size]; + } + }; + + /** + * Builder for {@link WifiDeviceFilter} + */ + public static final class Builder extends OneTimeUseBuilder { + private Pattern mNamePattern; + + /** + * @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; + } + + /** @inheritDoc */ + @Override + @NonNull + public WifiDeviceFilter build() { + markUsed(); + return new WifiDeviceFilter(mNamePattern); + } + } +} diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index f94e89a2785d8..7a39d239f84bf 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -601,6 +601,11 @@ public final class Parcel { nativeWriteString(mNativePtr, val); } + /** @hide */ + public final void writeBoolean(boolean val) { + writeInt(val ? 1 : 0); + } + /** * Write a CharSequence value into the parcel at the current dataPosition(), * growing dataCapacity() if needed. @@ -1964,6 +1969,11 @@ public final class Parcel { return nativeReadString(mNativePtr); } + /** @hide */ + public final boolean readBoolean() { + return readInt() != 0; + } + /** * Read a CharSequence value from the parcel at the current dataPosition(). * @hide @@ -2490,11 +2500,11 @@ public final class Parcel { * @see #writeParcelableList(List, int) * @hide */ - public final void readParcelableList(List list, ClassLoader cl) { + public final List readParcelableList(List list, ClassLoader cl) { final int N = readInt(); if (N == -1) { list.clear(); - return; + return list; } final int M = list.size(); @@ -2508,6 +2518,7 @@ public final class Parcel { for (; i List filter(@Nullable List list, Class c) { + if (isEmpty(list)) return Collections.emptyList(); + ArrayList result = null; + for (int i = 0; i < list.size(); i++) { + final Object item = list.get(i); + if (c.isInstance(item)) { + result = add(result, (T) item); + } + } + return emptyIfNull(result); + } + + public static boolean any(@Nullable List items, + java.util.function.Predicate predicate) { + return find(items, predicate) != null; + } + + @Nullable + public static T find(@Nullable List items, + java.util.function.Predicate predicate) { + if (isEmpty(items)) return null; + for (int i = 0; i < items.size(); i++) { + final T item = items.get(i); + if (predicate.test(item)) return item; + } + return null; + } + public static long total(@Nullable long[] array) { long total = 0; if (array != null) { diff --git a/core/java/com/android/internal/util/BitUtils.java b/core/java/com/android/internal/util/BitUtils.java new file mode 100644 index 0000000000000..a208ccb8f35f1 --- /dev/null +++ b/core/java/com/android/internal/util/BitUtils.java @@ -0,0 +1,58 @@ +/* + * 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 com.android.internal.util; + +import android.annotation.Nullable; + +import libcore.util.Objects; + +import java.util.Arrays; +import java.util.UUID; + +public class BitUtils { + private BitUtils() {} + + public static boolean maskedEquals(long a, long b, long mask) { + return (a & mask) == (b & mask); + } + + public static boolean maskedEquals(byte a, byte b, byte mask) { + return (a & mask) == (b & mask); + } + + public static boolean maskedEquals(byte[] a, byte[] b, @Nullable byte[] mask) { + if (a == null || b == null) return a == b; + Preconditions.checkArgument(a.length == b.length, "Inputs must be of same size"); + if (mask == null) return Arrays.equals(a, b); + Preconditions.checkArgument(a.length == mask.length, "Mask must be of same size as inputs"); + for (int i = 0; i < mask.length; i++) { + if (!maskedEquals(a[i], b[i], mask[i])) return false; + } + return true; + } + + public static boolean maskedEquals(UUID a, UUID b, @Nullable UUID mask) { + if (mask == null) { + return Objects.equal(a, b); + } + return maskedEquals(a.getLeastSignificantBits(), b.getLeastSignificantBits(), + mask.getLeastSignificantBits()) + && maskedEquals(a.getMostSignificantBits(), b.getMostSignificantBits(), + mask.getMostSignificantBits()); + } +} diff --git a/packages/CompanionDeviceManager/AndroidManifest.xml b/packages/CompanionDeviceManager/AndroidManifest.xml index 65cac09a09b3c..34bc4ebcd0aa2 100644 --- a/packages/CompanionDeviceManager/AndroidManifest.xml +++ b/packages/CompanionDeviceManager/AndroidManifest.xml @@ -26,6 +26,8 @@ + + mRequest; - List mDevicesFound; - BluetoothDevice mSelectedDevice; + private List> mFilters; + private List mBLEFilters; + private List mBluetoothFilters; + private List mWifiFilters; + private List mBLEScanFilters; + AssociationRequest mRequest; + List mDevicesFound; + DeviceFilterPair mSelectedDevice; DevicesAdapter mDevicesAdapter; IFindDeviceCallback mFindCallback; ICompanionDeviceDiscoveryServiceCallback mServiceCallback; @@ -95,11 +111,13 @@ public class DeviceDiscoveryService extends Service { private final ScanCallback mBLEScanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { - final BluetoothDevice device = result.getDevice(); + final DeviceFilterPair deviceFilterPair + = DeviceFilterPair.findMatch(result, mBLEFilters); + if (deviceFilterPair == null) return; if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) { - onDeviceLost(device); + onDeviceLost(deviceFilterPair); } else { - onDeviceFound(device); + onDeviceFound(deviceFilterPair); } } }; @@ -109,18 +127,38 @@ public class DeviceDiscoveryService extends Service { private BroadcastReceiver mBluetoothDeviceFoundBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - final BluetoothDevice device = intent.getParcelableExtra( - BluetoothDevice.EXTRA_DEVICE); - if (!mFilter.matches(device)) return; // ignore device - + final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + final DeviceFilterPair deviceFilterPair + = DeviceFilterPair.findMatch(device, mBluetoothFilters); + if (deviceFilterPair == null) return; if (intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) { - onDeviceFound(device); + onDeviceFound(deviceFilterPair); } else { - onDeviceLost(device); + onDeviceLost(deviceFilterPair); } } }; + private BroadcastReceiver mWifiDeviceFoundBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { + List scanResults = mWifiManager.getScanResults(); + + if (DEBUG) { + Log.i(LOG_TAG, "Wifi scan results: " + TextUtils.join("\n", scanResults)); + } + + for (int i = 0; i < scanResults.size(); i++) { + DeviceFilterPair deviceFilterPair = + DeviceFilterPair.findMatch(scanResults.get(i), mWifiFilters); + if (deviceFilterPair != null) onDeviceFound(deviceFilterPair); + } + } + + } + }; + @Override public IBinder onBind(Intent intent) { if (DEBUG) Log.i(LOG_TAG, "onBind(" + intent + ")"); @@ -135,6 +173,7 @@ public class DeviceDiscoveryService extends Service { mBluetoothAdapter = getSystemService(BluetoothManager.class).getAdapter(); mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner(); + mWifiManager = getSystemService(WifiManager.class); mDevicesFound = new ArrayList<>(); mDevicesAdapter = new DevicesAdapter(); @@ -142,23 +181,39 @@ public class DeviceDiscoveryService extends Service { sInstance = this; } - private void startDiscovery(AssociationRequest request) { - //TODO support other protocols as well + private void startDiscovery(AssociationRequest request) { mRequest = request; - mFilter = nullsafe((BluetoothLEDeviceFilter) request.getDeviceFilter()); - mScanFilter = mFilter.getScanFilter(); + + mFilters = request.getDeviceFilters(); + mWifiFilters = ArrayUtils.filter(mFilters, WifiDeviceFilter.class); + mBluetoothFilters = ArrayUtils.filter(mFilters, BluetoothDeviceFilter.class); + mBLEFilters = ArrayUtils.filter(mFilters, BluetoothLEDeviceFilter.class); + mBLEScanFilters = ArrayUtils.map(mBLEFilters, BluetoothLEDeviceFilter::getScanFilter); reset(); - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(BluetoothDevice.ACTION_FOUND); - intentFilter.addAction(BluetoothDevice.ACTION_DISAPPEARED); + if (shouldScan(mBluetoothFilters)) { + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(BluetoothDevice.ACTION_FOUND); + intentFilter.addAction(BluetoothDevice.ACTION_DISAPPEARED); - registerReceiver(mBluetoothDeviceFoundBroadcastReceiver, intentFilter); - mBluetoothAdapter.startDiscovery(); + registerReceiver(mBluetoothDeviceFoundBroadcastReceiver, intentFilter); + mBluetoothAdapter.startDiscovery(); + } - mBLEScanner.startScan( - Collections.singletonList(mScanFilter), mDefaultScanSettings, mBLEScanCallback); + if (shouldScan(mBLEFilters)) { + mBLEScanner.startScan(mBLEScanFilters, mDefaultScanSettings, mBLEScanCallback); + } + + if (shouldScan(mWifiFilters)) { + registerReceiver(mWifiDeviceFoundBroadcastReceiver, + new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); + mWifiManager.startScan(); + } + } + + private boolean shouldScan(List mediumSpecificFilters) { + return !isEmpty(mediumSpecificFilters) || isEmpty(mFilters); } private void reset() { @@ -178,25 +233,18 @@ public class DeviceDiscoveryService extends Service { mBluetoothAdapter.cancelDiscovery(); mBLEScanner.stopScan(mBLEScanCallback); unregisterReceiver(mBluetoothDeviceFoundBroadcastReceiver); + unregisterReceiver(mWifiDeviceFoundBroadcastReceiver); stopSelf(); } - private void onDeviceFound(BluetoothDevice device) { + private void onDeviceFound(@Nullable DeviceFilterPair device) { if (mDevicesFound.contains(device)) { return; } - if (DEBUG) { - Log.i(LOG_TAG, "Considering device " + getDeviceDisplayName(device)); - } + if (DEBUG) Log.i(LOG_TAG, "Found device " + device.getDisplayName() + " " + + getDeviceMacAddress(device.device)); - if (!mFilter.matches(device)) { - return; - } - - if (DEBUG) { - Log.i(LOG_TAG, "Found device " + getDeviceDisplayName(device)); - } if (mDevicesFound.isEmpty()) { onReadyToShowUI(); } @@ -217,12 +265,10 @@ public class DeviceDiscoveryService extends Service { } } - private void onDeviceLost(BluetoothDevice device) { + private void onDeviceLost(@Nullable DeviceFilterPair device) { mDevicesFound.remove(device); mDevicesAdapter.notifyDataSetChanged(); - if (DEBUG) { - Log.i(LOG_TAG, "Lost device " + getDeviceDisplayName(device)); - } + if (DEBUG) Log.i(LOG_TAG, "Lost device " + device.getDisplayName()); } void onDeviceSelected(String callingPackage, String deviceAddress) { @@ -236,7 +282,8 @@ public class DeviceDiscoveryService extends Service { } } - class DevicesAdapter extends ArrayAdapter { + class DevicesAdapter extends ArrayAdapter { + //TODO wifi icon private Drawable BLUETOOTH_ICON = icon(android.R.drawable.stat_sys_data_bluetooth); private Drawable icon(int drawableRes) { @@ -261,8 +308,8 @@ public class DeviceDiscoveryService extends Service { return view; } - private void bind(TextView textView, BluetoothDevice device) { - textView.setText(getDeviceDisplayName(device)); + private void bind(TextView textView, DeviceFilterPair device) { + textView.setText(device.getDisplayName()); textView.setBackgroundColor( device.equals(mSelectedDevice) ? Color.GRAY @@ -285,4 +332,62 @@ public class DeviceDiscoveryService extends Service { return textView; } } + + /** + * A pair of device and a filter that matched this device if any. + * + * @param device type + */ + static class DeviceFilterPair { + public final T device; + @Nullable + public final DeviceFilter filter; + + private DeviceFilterPair(T device, @Nullable DeviceFilter filter) { + this.device = device; + this.filter = filter; + } + + /** + * {@code (device, null)} if the filters list is empty or null + * {@code null} if none of the provided filters match the device + * {@code (device, filter)} where filter is among the list of filters and matches the device + */ + @Nullable + public static DeviceFilterPair findMatch( + T dev, @Nullable List> filters) { + if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null); + final DeviceFilter matchingFilter = ArrayUtils.find(filters, (f) -> f.matches(dev)); + return matchingFilter != null ? new DeviceFilterPair<>(dev, matchingFilter) : null; + } + + public String getDisplayName() { + if (filter == null) { + Preconditions.checkNotNull(device); + if (device instanceof BluetoothDevice) { + return getDeviceDisplayNameInternal((BluetoothDevice) device); + } else if (device instanceof android.net.wifi.ScanResult) { + return getDeviceDisplayNameInternal((android.net.wifi.ScanResult) device); + } else if (device instanceof ScanResult) { + return getDeviceDisplayNameInternal(((ScanResult) device).getDevice()); + } else { + throw new IllegalArgumentException("Unknown device type: " + device.getClass()); + } + } + return filter.getDeviceDisplayName(device); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DeviceFilterPair that = (DeviceFilterPair) o; + return Objects.equals(getDeviceMacAddress(device), getDeviceMacAddress(that.device)); + } + + @Override + public int hashCode() { + return Objects.hash(getDeviceMacAddress(device)); + } + } } diff --git a/services/print/java/com/android/server/print/CompanionDeviceManagerService.java b/services/print/java/com/android/server/print/CompanionDeviceManagerService.java index ad64e4e6e64da..e6e2cb3d99c9e 100644 --- a/services/print/java/com/android/server/print/CompanionDeviceManagerService.java +++ b/services/print/java/com/android/server/print/CompanionDeviceManagerService.java @@ -141,7 +141,7 @@ public class CompanionDeviceManagerService extends SystemService { } private ServiceConnection getServiceConnection( - final AssociationRequest request, + final AssociationRequest request, final IFindDeviceCallback findDeviceCallback, final String callingPackage) { return new ServiceConnection() {