diff options
19 files changed, 730 insertions, 214 deletions
diff --git a/api/current.txt b/api/current.txt index 1a5ea853b365..0d38bc6e3d56 100644 --- a/api/current.txt +++ b/api/current.txt @@ -8183,18 +8183,17 @@ package android.bluetooth.le { package android.companion { - public final class AssociationRequest<F extends android.companion.DeviceFilter> 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<android.companion.AssociationRequest> CREATOR; } - public static final class AssociationRequest.Builder<F extends android.companion.DeviceFilter> { - method public android.companion.AssociationRequest<F> build(); - method public static android.companion.AssociationRequest.Builder<android.companion.BluetoothDeviceFilter> createForBluetoothDevice(); - method public static android.companion.AssociationRequest.Builder<android.companion.BluetoothLEDeviceFilter> createForBluetoothLEDevice(); - method public android.companion.AssociationRequest.Builder<F> setDeviceFilter(F); - method public android.companion.AssociationRequest.Builder<F> 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 { @@ -8215,17 +8214,20 @@ package android.companion { method public int describeContents(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.companion.BluetoothLEDeviceFilter> CREATOR; + field public static final int RENAME_PREFIX_LENGTH_LIMIT = 10; // 0xa } public static final class BluetoothLEDeviceFilter.Builder { 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<java.lang.String> 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<D extends android.os.Parcelable> 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<android.companion.WifiDeviceFilter> 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 b5c20e52e913..16de51039ff4 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<F extends android.companion.DeviceFilter> 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<android.companion.AssociationRequest> CREATOR; } - public static final class AssociationRequest.Builder<F extends android.companion.DeviceFilter> { - method public android.companion.AssociationRequest<F> build(); - method public static android.companion.AssociationRequest.Builder<android.companion.BluetoothDeviceFilter> createForBluetoothDevice(); - method public static android.companion.AssociationRequest.Builder<android.companion.BluetoothLEDeviceFilter> createForBluetoothLEDevice(); - method public android.companion.AssociationRequest.Builder<F> setDeviceFilter(F); - method public android.companion.AssociationRequest.Builder<F> 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 { @@ -8715,17 +8714,20 @@ package android.companion { method public int describeContents(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.companion.BluetoothLEDeviceFilter> CREATOR; + field public static final int RENAME_PREFIX_LENGTH_LIMIT = 10; // 0xa } public static final class BluetoothLEDeviceFilter.Builder { 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<java.lang.String> 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<D extends android.os.Parcelable> 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<android.companion.WifiDeviceFilter> 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 57cc998ffb5d..62f76bde349d 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<F extends android.companion.DeviceFilter> 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<android.companion.AssociationRequest> CREATOR; } - public static final class AssociationRequest.Builder<F extends android.companion.DeviceFilter> { - method public android.companion.AssociationRequest<F> build(); - method public static android.companion.AssociationRequest.Builder<android.companion.BluetoothDeviceFilter> createForBluetoothDevice(); - method public static android.companion.AssociationRequest.Builder<android.companion.BluetoothLEDeviceFilter> createForBluetoothLEDevice(); - method public android.companion.AssociationRequest.Builder<F> setDeviceFilter(F); - method public android.companion.AssociationRequest.Builder<F> 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 { @@ -8242,17 +8241,20 @@ package android.companion { method public int describeContents(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.companion.BluetoothLEDeviceFilter> CREATOR; + field public static final int RENAME_PREFIX_LENGTH_LIMIT = 10; // 0xa } public static final class BluetoothLEDeviceFilter.Builder { 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<java.lang.String> 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<D extends android.os.Parcelable> 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<android.companion.WifiDeviceFilter> 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 b89c64a8cac6..457096bf843e 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 d477f43ac8c2..56f5d4483270 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 <F> Device filter type */ -public final class AssociationRequest<F extends DeviceFilter> 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<DeviceFilter<?>> mDeviceFilters; - private AssociationRequest(boolean singleDevice, int mMediumType, F deviceFilter) { + private AssociationRequest( + boolean singleDevice, @Nullable List<DeviceFilter<?>> 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<F extends DeviceFilter> implements Parcela } /** @hide */ - @MediumType - public int getMediumType() { - return mMediumType; - } - - /** @hide */ - @Nullable - public F getDeviceFilter() { - return mDeviceFilter; + @NonNull + public List<DeviceFilter<?>> 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<F extends DeviceFilter> implements Parcela /** * A builder for {@link AssociationRequest} - * - * @param <F> the type of filter for the request. */ - public static final class Builder<F extends DeviceFilter> - extends OneTimeUseBuilder<AssociationRequest<F>> { + public static final class Builder extends OneTimeUseBuilder<AssociationRequest> { private boolean mSingleDevice = false; - @MediumType private int mMediumType; - @Nullable private F mDeviceFilter = null; - - private Builder() {} - - /** - * Create a new builder for an association request with a Bluetooth LE device - */ - @NonNull - public static Builder<BluetoothLEDeviceFilter> createForBluetoothLEDevice() { - return new Builder<BluetoothLEDeviceFilter>() - .setMediumType(MEDIUM_TYPE_BLUETOOTH_LE); - } - - /** - * Create a new builder for an association request with a Bluetooth(non-LE) device - */ - @NonNull - public static Builder<BluetoothDeviceFilter> createForBluetoothDevice() { - return new Builder<BluetoothDeviceFilter>() - .setMediumType(MEDIUM_TYPE_BLUETOOTH); - } + @Nullable private ArrayList<DeviceFilter<?>> mDeviceFilters = null; - //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<F> setSingleDevice(boolean singleDevice) { + public Builder setSingleDevice(boolean singleDevice) { checkNotUsed(); this.mSingleDevice = singleDevice; return this; @@ -163,29 +115,20 @@ public final class AssociationRequest<F extends DeviceFilter> implements Parcela * user */ @NonNull - public Builder<F> 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<F> setMediumType(@MediumType int deviceType) { - mMediumType = deviceType; + if (deviceFilter != null) { + mDeviceFilters = ArrayUtils.add(mDeviceFilters, deviceFilter); + } return this; } /** @inheritDoc */ @NonNull @Override - public AssociationRequest<F> 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 5a69955429aa..0f16b7b90165 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<BluetoothDevice> { - private static BluetoothDeviceFilter NO_OP; - private final Pattern mNamePattern; private final String mAddress; private final List<ParcelUuid> mServiceUuids; @@ -67,30 +66,27 @@ public final class BluetoothDeviceFilter implements DeviceFilter<BluetoothDevice } private static List<ParcelUuid> readUuids(Parcel in) { - final ArrayList<ParcelUuid> list = new ArrayList<>(); - in.readParcelableList(list, ParcelUuid.class.getClassLoader()); - return list; + return in.readParcelableList(new ArrayList<>(), ParcelUuid.class.getClassLoader()); } /** @hide */ - @NonNull - public static BluetoothDeviceFilter nullsafe(@Nullable BluetoothDeviceFilter nullable) { - return nullable != null ? nullable : noOp(); + @Override + public boolean matches(BluetoothDevice device) { + return matchesAddress(mAddress, device) + && matchesServiceUuids(mServiceUuids, mServiceUuidMasks, device) + && matchesName(getNamePattern(), device); } /** @hide */ - @NonNull - public static BluetoothDeviceFilter noOp() { - if (NO_OP == null) NO_OP = new Builder().build(); - return NO_OP; + @Override + public String getDeviceDisplayName(BluetoothDevice device) { + return getDeviceDisplayNameInternal(device); } /** @hide */ @Override - public boolean matches(BluetoothDevice device) { - return matchesAddress(mAddress, device) - && matchesServiceUuids(mServiceUuids, mServiceUuidMasks, device) - && matchesName(getNamePattern(), device); + public int getMediumType() { + return DeviceFilter.MEDIUM_TYPE_BLUETOOTH; } /** @hide */ diff --git a/core/java/android/companion/BluetoothDeviceFilterUtils.java b/core/java/android/companion/BluetoothDeviceFilterUtils.java index 289f9953a068..8a316f19af8e 100644 --- a/core/java/android/companion/BluetoothDeviceFilterUtils.java +++ b/core/java/android/companion/BluetoothDeviceFilterUtils.java @@ -23,7 +23,9 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.bluetooth.BluetoothDevice; import android.bluetooth.le.ScanFilter; +import android.net.wifi.ScanResult; import android.os.ParcelUuid; +import android.os.Parcelable; import android.util.Log; import java.util.Arrays; @@ -96,12 +98,47 @@ public class BluetoothDeviceFilterUtils { return result; } + static boolean matchesName(@Nullable Pattern namePattern, ScanResult device) { + boolean result; + if (namePattern == null) { + result = true; + } else if (device == null) { + result = false; + } else { + final String name = device.SSID; + result = name != null && namePattern.matcher(name).find(); + } + if (DEBUG) debugLogMatchResult(result, device, namePattern); + return result; + } + private static void debugLogMatchResult( boolean result, BluetoothDevice device, Object criteria) { - Log.i(LOG_TAG, getDeviceDisplayName(device) + (result ? " ~ " : " !~ ") + criteria); + Log.i(LOG_TAG, getDeviceDisplayNameInternal(device) + (result ? " ~ " : " !~ ") + criteria); } - public static String getDeviceDisplayName(@NonNull BluetoothDevice device) { + private static void debugLogMatchResult( + boolean result, ScanResult device, Object criteria) { + Log.i(LOG_TAG, getDeviceDisplayNameInternal(device) + (result ? " ~ " : " !~ ") + criteria); + } + + public static String getDeviceDisplayNameInternal(@NonNull BluetoothDevice device) { return firstNotEmpty(device.getAliasName(), device.getAddress()); } + + public static String getDeviceDisplayNameInternal(@NonNull ScanResult device) { + return firstNotEmpty(device.SSID, device.BSSID); + } + + public static String getDeviceMacAddress(@NonNull Parcelable device) { + if (device instanceof BluetoothDevice) { + return ((BluetoothDevice) device).getAddress(); + } else if (device instanceof ScanResult) { + return ((ScanResult) device).BSSID; + } else if (device instanceof android.bluetooth.le.ScanResult) { + return getDeviceMacAddress(((android.bluetooth.le.ScanResult) device).getDevice()); + } else { + throw new IllegalArgumentException("Unknown device type: " + device); + } + } } diff --git a/core/java/android/companion/BluetoothLEDeviceFilter.java b/core/java/android/companion/BluetoothLEDeviceFilter.java index 4a481ca80045..e057fbcc901a 100644 --- a/core/java/android/companion/BluetoothLEDeviceFilter.java +++ b/core/java/android/companion/BluetoothLEDeviceFilter.java @@ -16,18 +16,25 @@ 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 android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.SuppressLint; 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 com.android.internal.util.BitUtils; import com.android.internal.util.ObjectUtils; +import com.android.internal.util.Preconditions; import java.util.regex.Pattern; @@ -36,57 +43,122 @@ import java.util.regex.Pattern; * * @see ScanFilter */ -public final class BluetoothLEDeviceFilter implements DeviceFilter<BluetoothDevice> { +public final class BluetoothLEDeviceFilter implements DeviceFilter<ScanResult> { - 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); + mRawDataFilter = rawDataFilter; + mRawDataFilterMask = rawDataFilterMask; + mRenamePrefix = renamePrefix; + mRenameSuffix = renameSuffix; + mRenameBytesFrom = renameBytesFrom; + mRenameBytesTo = renameBytesTo; + mRenameBytesReverseOrder = renameBytesReverseOrder; } - @SuppressLint("ParcelClassLoader") - private BluetoothLEDeviceFilter(Parcel in) { - this( - patternFromString(in.readString()), - in.readParcelable(null)); + /** @hide */ + @Nullable + public Pattern getNamePattern() { + return mNamePattern; } /** @hide */ @NonNull - public static BluetoothLEDeviceFilter nullsafe(@Nullable BluetoothLEDeviceFilter nullable) { - return nullable != null ? nullable : noOp(); + public ScanFilter getScanFilter() { + return mScanFilter; } /** @hide */ - @NonNull - public static BluetoothLEDeviceFilter noOp() { - if (NO_OP == null) NO_OP = new Builder().build(); - return NO_OP; + @Nullable + public byte[] getRawDataFilter() { + return mRawDataFilter; } /** @hide */ @Nullable - public Pattern getNamePattern() { - return mNamePattern; + public byte[] getRawDataFilterMask() { + return mRawDataFilterMask; } /** @hide */ - @NonNull - public ScanFilter getScanFilter() { - return mScanFilter; + @Nullable + public String getRenamePrefix() { + return mRenamePrefix; + } + + /** @hide */ + @Nullable + public String getRenameSuffix() { + return mRenameSuffix; + } + + /** @hide */ + public int getRenameBytesFrom() { + return mRenameBytesFrom; + } + + /** @hide */ + public int getRenameBytesTo() { + return mRenameBytesTo; + } + + /** @hide */ + public boolean isRenameBytesReverseOrder() { + return mRenameBytesReverseOrder; + } + + /** @hide */ + @Override + @Nullable + public String getDeviceDisplayName(ScanResult sr) { + if (mRenameBytesFrom < 0) return getDeviceDisplayNameInternal(sr.getDevice()); + final byte[] bytes = sr.getScanRecord().getBytes(); + final StringBuilder sb = new StringBuilder(TextUtils.emptyIfNull(mRenamePrefix)); + int startInclusive = mRenameBytesFrom; + int endInclusive = mRenameBytesTo - 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)); + } + return sb.append(TextUtils.emptyIfNull(mRenameSuffix)).toString(); } /** @hide */ @Override - public boolean matches(BluetoothDevice device) { + public boolean matches(ScanResult device) { + return matches(device.getDevice()) + && BitUtils.maskedEquals(device.getScanRecord().getBytes(), + mRawDataFilter, mRawDataFilterMask); + } + + 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 void writeToParcel(Parcel dest, int flags) { dest.writeString(patternToString(getNamePattern())); @@ -102,7 +174,13 @@ public final class BluetoothLEDeviceFilter implements DeviceFilter<BluetoothDevi = new Creator<BluetoothLEDeviceFilter>() { @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<BluetoothDevi } }; + 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 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<BluetoothDevi /** * @param scanFilter a {@link ScanFilter} to filter devices by * + * @return self for chaining * @see ScanFilter for specific details on its various fields */ @NonNull @@ -140,12 +231,66 @@ public final class BluetoothLEDeviceFilter implements DeviceFilter<BluetoothDevi 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, + @NonNull byte[] rawDataFilterMask) { + checkNotUsed(); + checkArgument(rawDataFilter.length == rawDataFilterMask.length, + "Mask and filter should be the same length"); + mRawDataFilter = Preconditions.checkNotNull(rawDataFilter); + mRawDataFilterMask = Preconditions.checkNotNull(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 bytesTo the end byte index to be displayed (exclusive) + * @param bytesReverseOrder if true, the byte order of the provided range will be flipped + * when displaying + * @return self for chaining + */ + @NonNull + public Builder setRename(@NonNull String prefix, @NonNull String suffix, + int bytesFrom, int bytesTo, boolean bytesReverseOrder) { + checkNotUsed(); + checkArgument(TextUtils.length(prefix) >= 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 6fa32b4b6944..5710ad1318d7 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 8362b2dab8fd..9b4fdfdf5108 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 <D> 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<D extends Parcelable> 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<D extends Parcelable> 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<D extends Parcelable> extends Parcelable { static <D extends Parcelable> boolean matches(@Nullable DeviceFilter<D> 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 000000000000..1ab9ce11cb0f --- /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<ScanResult> { + + 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<WifiDeviceFilter> CREATOR + = new Creator<WifiDeviceFilter>() { + @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<WifiDeviceFilter> { + 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 f94e89a2785d..7a39d239f84b 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 <T extends Parcelable> void readParcelableList(List<T> list, ClassLoader cl) { + public final <T extends Parcelable> List<T> readParcelableList(List<T> 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<M; i++) { list.remove(N); } + return list; } /** diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index ac9c0d782c53..ee2b38e4f390 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -474,6 +474,11 @@ public class TextUtils { return !isEmpty(a) ? a : Preconditions.checkStringNotEmpty(b); } + /** {@hide} */ + public static int length(@Nullable String s) { + return isEmpty(s) ? 0 : s.length(); + } + /** * Returns the length that the specified CharSequence would have if * spaces and ASCII control characters were trimmed from the start and end, diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java index be69d9f808e2..d0fbe7c8a666 100644 --- a/core/java/com/android/internal/util/ArrayUtils.java +++ b/core/java/com/android/internal/util/ArrayUtils.java @@ -237,6 +237,35 @@ public class ArrayUtils { return false; } + @NonNull + public static <T> List<T> filter(@Nullable List<?> list, Class<T> c) { + if (isEmpty(list)) return Collections.emptyList(); + ArrayList<T> 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 <T> boolean any(@Nullable List<T> items, + java.util.function.Predicate<T> predicate) { + return find(items, predicate) != null; + } + + @Nullable + public static <T> T find(@Nullable List<T> items, + java.util.function.Predicate<T> 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 000000000000..a208ccb8f35f --- /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 65cac09a09b3..34bc4ebcd0aa 100644 --- a/packages/CompanionDeviceManager/AndroidManifest.xml +++ b/packages/CompanionDeviceManager/AndroidManifest.xml @@ -26,6 +26,8 @@ <uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> + <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/> <application android:allowClearUserData="true" diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceChooserActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceChooserActivity.java index 12bab18c88c9..14b9de59bf5a 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceChooserActivity.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceChooserActivity.java @@ -16,10 +16,9 @@ package com.android.companiondevicemanager; -import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayName; +import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress; import android.app.Activity; -import android.bluetooth.BluetoothDevice; import android.companion.CompanionDeviceManager; import android.content.Intent; import android.content.pm.PackageManager; @@ -34,6 +33,8 @@ import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; +import com.android.companiondevicemanager.DeviceDiscoveryService.DeviceFilterPair; + public class DeviceChooserActivity extends Activity { private static final boolean DEBUG = false; @@ -55,11 +56,11 @@ public class DeviceChooserActivity extends Activity { if (getService().mRequest.isSingleDevice()) { setContentView(R.layout.device_confirmation); - final BluetoothDevice selectedDevice = getService().mDevicesFound.get(0); + final DeviceFilterPair selectedDevice = getService().mDevicesFound.get(0); setTitle(Html.fromHtml(getString( R.string.confirmation_title, getCallingAppName(), - getDeviceDisplayName(selectedDevice)), 0)); + selectedDevice.getDisplayName()), 0)); getService().mSelectedDevice = selectedDevice; } else { setContentView(R.layout.device_chooser); @@ -127,10 +128,11 @@ public class DeviceChooserActivity extends Activity { return DeviceDiscoveryService.sInstance; } - protected void onPairTapped(BluetoothDevice selectedDevice) { - getService().onDeviceSelected(getCallingPackage(), selectedDevice.getAddress()); + protected void onPairTapped(DeviceFilterPair selectedDevice) { + getService().onDeviceSelected( + getCallingPackage(), getDeviceMacAddress(selectedDevice.device)); setResult(RESULT_OK, - new Intent().putExtra(CompanionDeviceManager.EXTRA_DEVICE, selectedDevice)); + new Intent().putExtra(CompanionDeviceManager.EXTRA_DEVICE, selectedDevice.device)); finish(); } }
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java index f0f910848943..e1e60bb99374 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceDiscoveryService.java @@ -16,8 +16,10 @@ package com.android.companiondevicemanager; -import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayName; -import static android.companion.BluetoothLEDeviceFilter.nullsafe; +import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal; +import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress; + +import static com.android.internal.util.ArrayUtils.isEmpty; import android.annotation.NonNull; import android.annotation.Nullable; @@ -32,28 +34,38 @@ import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; import android.companion.AssociationRequest; +import android.companion.BluetoothDeviceFilter; +import android.companion.BluetoothDeviceFilterUtils; import android.companion.BluetoothLEDeviceFilter; import android.companion.CompanionDeviceManager; +import android.companion.DeviceFilter; import android.companion.ICompanionDeviceDiscoveryService; import android.companion.ICompanionDeviceDiscoveryServiceCallback; import android.companion.IFindDeviceCallback; +import android.companion.WifiDeviceFilter; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Color; import android.graphics.drawable.Drawable; +import android.net.wifi.WifiManager; import android.os.IBinder; +import android.os.Parcelable; import android.os.RemoteException; +import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.Preconditions; + import java.util.ArrayList; -import java.util.Collections; import java.util.List; +import java.util.Objects; public class DeviceDiscoveryService extends Service { @@ -63,12 +75,16 @@ public class DeviceDiscoveryService extends Service { static DeviceDiscoveryService sInstance; private BluetoothAdapter mBluetoothAdapter; - private BluetoothLEDeviceFilter mFilter; - private ScanFilter mScanFilter; + private WifiManager mWifiManager; private ScanSettings mDefaultScanSettings = new ScanSettings.Builder().build(); - AssociationRequest<?> mRequest; - List<BluetoothDevice> mDevicesFound; - BluetoothDevice mSelectedDevice; + private List<DeviceFilter<?>> mFilters; + private List<BluetoothLEDeviceFilter> mBLEFilters; + private List<BluetoothDeviceFilter> mBluetoothFilters; + private List<WifiDeviceFilter> mWifiFilters; + private List<ScanFilter> mBLEScanFilters; + AssociationRequest mRequest; + List<DeviceFilterPair> 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<ScanResult> 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,15 +127,35 @@ 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<BluetoothDevice> 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<android.net.wifi.ScanResult> 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<android.net.wifi.ScanResult> deviceFilterPair = + DeviceFilterPair.findMatch(scanResults.get(i), mWifiFilters); + if (deviceFilterPair != null) onDeviceFound(deviceFilterPair); + } } + } }; @@ -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<? extends DeviceFilter> 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<BluetoothDevice> { + class DevicesAdapter extends ArrayAdapter<DeviceFilterPair> { + //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 <T> device type + */ + static class DeviceFilterPair<T extends Parcelable> { + public final T device; + @Nullable + public final DeviceFilter<T> filter; + + private DeviceFilterPair(T device, @Nullable DeviceFilter<T> 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 <T extends Parcelable> DeviceFilterPair<T> findMatch( + T dev, @Nullable List<? extends DeviceFilter<T>> filters) { + if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null); + final DeviceFilter<T> 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 ad64e4e6e64d..e6e2cb3d99c9 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() { |