diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
commit | 10d07c88d69cc64f73a069163e7ea5ba2519a099 (patch) | |
tree | 8dbd149eb350320a29c3d10e7ad3201de1c5cbee /android/companion | |
parent | 677516fb6b6f207d373984757d3d9450474b6b00 (diff) | |
download | android-28-10d07c88d69cc64f73a069163e7ea5ba2519a099.tar.gz |
Import Android SDK Platform PI [4335822]
/google/data/ro/projects/android/fetch_artifact \
--bid 4335822 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4335822.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
Diffstat (limited to 'android/companion')
-rw-r--r-- | android/companion/AssociationRequest.java | 158 | ||||
-rw-r--r-- | android/companion/BluetoothDeviceFilter.java | 216 | ||||
-rw-r--r-- | android/companion/BluetoothDeviceFilterUtils.java | 146 | ||||
-rw-r--r-- | android/companion/BluetoothLeDeviceFilter.java | 434 | ||||
-rw-r--r-- | android/companion/CompanionDeviceManager.java | 333 | ||||
-rw-r--r-- | android/companion/DeviceFilter.java | 68 | ||||
-rw-r--r-- | android/companion/WifiDeviceFilter.java | 139 |
7 files changed, 1494 insertions, 0 deletions
diff --git a/android/companion/AssociationRequest.java b/android/companion/AssociationRequest.java new file mode 100644 index 00000000..922224a5 --- /dev/null +++ b/android/companion/AssociationRequest.java @@ -0,0 +1,158 @@ +/* + * 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 android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; +import android.provider.OneTimeUseBuilder; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * A request for the user to select a companion device to associate with. + * + * 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 + * supported. + * + * 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 + */ +public final class AssociationRequest implements Parcelable { + + private final boolean mSingleDevice; + private final List<DeviceFilter<?>> mDeviceFilters; + + private AssociationRequest( + boolean singleDevice, @Nullable List<DeviceFilter<?>> deviceFilters) { + this.mSingleDevice = singleDevice; + this.mDeviceFilters = CollectionUtils.emptyIfNull(deviceFilters); + } + + private AssociationRequest(Parcel in) { + this( + in.readByte() != 0, + in.readParcelableList(new ArrayList<>(), AssociationRequest.class.getClassLoader())); + } + + /** @hide */ + public boolean isSingleDevice() { + return mSingleDevice; + } + + /** @hide */ + @NonNull + public List<DeviceFilter<?>> getDeviceFilters() { + return mDeviceFilters; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AssociationRequest that = (AssociationRequest) o; + return mSingleDevice == that.mSingleDevice && + Objects.equals(mDeviceFilters, that.mDeviceFilters); + } + + @Override + public int hashCode() { + return Objects.hash(mSingleDevice, mDeviceFilters); + } + + @Override + public String toString() { + return "AssociationRequest{" + + "mSingleDevice=" + mSingleDevice + + ", mDeviceFilters=" + mDeviceFilters + + '}'; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeByte((byte) (mSingleDevice ? 1 : 0)); + dest.writeParcelableList(mDeviceFilters, flags); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<AssociationRequest> CREATOR = new Creator<AssociationRequest>() { + @Override + public AssociationRequest createFromParcel(Parcel in) { + return new AssociationRequest(in); + } + + @Override + public AssociationRequest[] newArray(int size) { + return new AssociationRequest[size]; + } + }; + + /** + * A builder for {@link AssociationRequest} + */ + public static final class Builder extends OneTimeUseBuilder<AssociationRequest> { + private boolean mSingleDevice = false; + @Nullable private ArrayList<DeviceFilter<?>> mDeviceFilters = null; + + 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) { + checkNotUsed(); + this.mSingleDevice = singleDevice; + return this; + } + + /** + * @param deviceFilter if set, only devices matching the given filter will be shown to the + * user + */ + @NonNull + public Builder addDeviceFilter(@Nullable DeviceFilter<?> deviceFilter) { + checkNotUsed(); + if (deviceFilter != null) { + mDeviceFilters = ArrayUtils.add(mDeviceFilters, deviceFilter); + } + return this; + } + + /** @inheritDoc */ + @NonNull + @Override + public AssociationRequest build() { + markUsed(); + return new AssociationRequest(mSingleDevice, mDeviceFilters); + } + } +} diff --git a/android/companion/BluetoothDeviceFilter.java b/android/companion/BluetoothDeviceFilter.java new file mode 100644 index 00000000..84e15364 --- /dev/null +++ b/android/companion/BluetoothDeviceFilter.java @@ -0,0 +1,216 @@ +/* + * 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.matchesAddress; +import static android.companion.BluetoothDeviceFilterUtils.matchesName; +import static android.companion.BluetoothDeviceFilterUtils.matchesServiceUuids; +import static android.companion.BluetoothDeviceFilterUtils.patternFromString; +import static android.companion.BluetoothDeviceFilterUtils.patternToString; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.bluetooth.BluetoothDevice; +import android.os.Parcel; +import android.os.ParcelUuid; +import android.provider.OneTimeUseBuilder; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * A filter for Bluetooth(non-LE) devices + */ +public final class BluetoothDeviceFilter implements DeviceFilter<BluetoothDevice> { + + private final Pattern mNamePattern; + private final String mAddress; + private final List<ParcelUuid> mServiceUuids; + private final List<ParcelUuid> mServiceUuidMasks; + + private BluetoothDeviceFilter( + Pattern namePattern, + String address, + List<ParcelUuid> serviceUuids, + List<ParcelUuid> serviceUuidMasks) { + mNamePattern = namePattern; + mAddress = address; + mServiceUuids = CollectionUtils.emptyIfNull(serviceUuids); + mServiceUuidMasks = CollectionUtils.emptyIfNull(serviceUuidMasks); + } + + private BluetoothDeviceFilter(Parcel in) { + this( + patternFromString(in.readString()), + in.readString(), + readUuids(in), + readUuids(in)); + } + + private static List<ParcelUuid> readUuids(Parcel in) { + return in.readParcelableList(new ArrayList<>(), ParcelUuid.class.getClassLoader()); + } + + /** @hide */ + @Override + public boolean matches(BluetoothDevice device) { + return matchesAddress(mAddress, device) + && matchesServiceUuids(mServiceUuids, mServiceUuidMasks, device) + && matchesName(getNamePattern(), device); + } + + /** @hide */ + @Override + public String getDeviceDisplayName(BluetoothDevice device) { + return getDeviceDisplayNameInternal(device); + } + + /** @hide */ + @Override + public int getMediumType() { + return DeviceFilter.MEDIUM_TYPE_BLUETOOTH; + } + + /** @hide */ + @Nullable + public Pattern getNamePattern() { + return mNamePattern; + } + + /** @hide */ + @Nullable + public String getAddress() { + return mAddress; + } + + /** @hide */ + @NonNull + public List<ParcelUuid> getServiceUuids() { + return mServiceUuids; + } + + /** @hide */ + @NonNull + public List<ParcelUuid> getServiceUuidMasks() { + return mServiceUuidMasks; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(patternToString(getNamePattern())); + dest.writeString(mAddress); + dest.writeParcelableList(mServiceUuids, flags); + dest.writeParcelableList(mServiceUuidMasks, flags); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BluetoothDeviceFilter that = (BluetoothDeviceFilter) o; + return Objects.equals(mNamePattern, that.mNamePattern) && + Objects.equals(mAddress, that.mAddress) && + Objects.equals(mServiceUuids, that.mServiceUuids) && + Objects.equals(mServiceUuidMasks, that.mServiceUuidMasks); + } + + @Override + public int hashCode() { + return Objects.hash(mNamePattern, mAddress, mServiceUuids, mServiceUuidMasks); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<BluetoothDeviceFilter> CREATOR + = new Creator<BluetoothDeviceFilter>() { + @Override + public BluetoothDeviceFilter createFromParcel(Parcel in) { + return new BluetoothDeviceFilter(in); + } + + @Override + public BluetoothDeviceFilter[] newArray(int size) { + return new BluetoothDeviceFilter[size]; + } + }; + + /** + * A builder for {@link BluetoothDeviceFilter} + */ + public static final class Builder extends OneTimeUseBuilder<BluetoothDeviceFilter> { + private Pattern mNamePattern; + private String mAddress; + private ArrayList<ParcelUuid> mServiceUuid; + private ArrayList<ParcelUuid> mServiceUuidMask; + + /** + * @param regex if set, only devices with {@link BluetoothDevice#getName name} matching the + * given regular expression will be shown + */ + public Builder setNamePattern(@Nullable Pattern regex) { + checkNotUsed(); + mNamePattern = regex; + return this; + } + + /** + * @param address if set, only devices with MAC address exactly matching the given one will + * pass the filter + */ + @NonNull + public Builder setAddress(@Nullable String address) { + checkNotUsed(); + mAddress = address; + return this; + } + + /** + * Add filtering by certain bits of {@link BluetoothDevice#getUuids()} + * + * A device with any uuid matching the given bits is considered passing + * + * @param serviceUuid the values for the bits to match + * @param serviceUuidMask if provided, only those bits would have to match. + */ + @NonNull + public Builder addServiceUuid( + @Nullable ParcelUuid serviceUuid, @Nullable ParcelUuid serviceUuidMask) { + checkNotUsed(); + mServiceUuid = ArrayUtils.add(mServiceUuid, serviceUuid); + mServiceUuidMask = ArrayUtils.add(mServiceUuidMask, serviceUuidMask); + return this; + } + + /** @inheritDoc */ + @Override + @NonNull + public BluetoothDeviceFilter build() { + markUsed(); + return new BluetoothDeviceFilter( + mNamePattern, mAddress, mServiceUuid, mServiceUuidMask); + } + } +} diff --git a/android/companion/BluetoothDeviceFilterUtils.java b/android/companion/BluetoothDeviceFilterUtils.java new file mode 100644 index 00000000..4ee38fe4 --- /dev/null +++ b/android/companion/BluetoothDeviceFilterUtils.java @@ -0,0 +1,146 @@ +/* + * 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.text.TextUtils.firstNotEmpty; + +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; +import java.util.List; +import java.util.regex.Pattern; + +/** @hide */ +public class BluetoothDeviceFilterUtils { + private BluetoothDeviceFilterUtils() {} + + private static final boolean DEBUG = false; + private static final String LOG_TAG = "BluetoothDeviceFilterUtils"; + + @Nullable + static String patternToString(@Nullable Pattern p) { + return p == null ? null : p.pattern(); + } + + @Nullable + static Pattern patternFromString(@Nullable String s) { + return s == null ? null : Pattern.compile(s); + } + + static boolean matches(ScanFilter filter, BluetoothDevice device) { + boolean result = matchesAddress(filter.getDeviceAddress(), device) + && matchesServiceUuid(filter.getServiceUuid(), filter.getServiceUuidMask(), device); + if (DEBUG) debugLogMatchResult(result, device, filter); + return result; + } + + static boolean matchesAddress(String deviceAddress, BluetoothDevice device) { + final boolean result = deviceAddress == null + || (device != null && deviceAddress.equals(device.getAddress())); + if (DEBUG) debugLogMatchResult(result, device, deviceAddress); + return result; + } + + static boolean matchesServiceUuids(List<ParcelUuid> serviceUuids, + List<ParcelUuid> serviceUuidMasks, BluetoothDevice device) { + for (int i = 0; i < serviceUuids.size(); i++) { + ParcelUuid uuid = serviceUuids.get(i); + ParcelUuid uuidMask = serviceUuidMasks.get(i); + if (!matchesServiceUuid(uuid, uuidMask, device)) { + return false; + } + } + return true; + } + + static boolean matchesServiceUuid(ParcelUuid serviceUuid, ParcelUuid serviceUuidMask, + BluetoothDevice device) { + final boolean result = serviceUuid == null || + ScanFilter.matchesServiceUuids( + serviceUuid, + serviceUuidMask, + Arrays.asList(device.getUuids())); + if (DEBUG) debugLogMatchResult(result, device, serviceUuid); + return result; + } + + static boolean matchesName(@Nullable Pattern namePattern, BluetoothDevice device) { + boolean result; + if (namePattern == null) { + result = true; + } else if (device == null) { + result = false; + } else { + final String name = device.getName(); + result = name != null && namePattern.matcher(name).find(); + } + if (DEBUG) debugLogMatchResult(result, device, namePattern); + 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, getDeviceDisplayNameInternal(device) + (result ? " ~ " : " !~ ") + criteria); + } + + 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/android/companion/BluetoothLeDeviceFilter.java b/android/companion/BluetoothLeDeviceFilter.java new file mode 100644 index 00000000..7fb768c6 --- /dev/null +++ b/android/companion/BluetoothLeDeviceFilter.java @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.companion; + +import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal; +import static android.companion.BluetoothDeviceFilterUtils.patternFromString; +import static android.companion.BluetoothDeviceFilterUtils.patternToString; + +import static com.android.internal.util.Preconditions.checkArgument; +import static com.android.internal.util.Preconditions.checkState; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanRecord; +import android.bluetooth.le.ScanResult; +import android.os.Parcel; +import android.provider.OneTimeUseBuilder; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.util.BitUtils; +import com.android.internal.util.ObjectUtils; +import com.android.internal.util.Preconditions; + +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * A filter for Bluetooth LE devices + * + * @see ScanFilter + */ +public final class BluetoothLeDeviceFilter implements DeviceFilter<ScanResult> { + + private static final boolean DEBUG = false; + private static final String LOG_TAG = "BluetoothLeDeviceFilter"; + + private static final int RENAME_PREFIX_LENGTH_LIMIT = 10; + + private final Pattern mNamePattern; + private final ScanFilter mScanFilter; + private final byte[] mRawDataFilter; + private final byte[] mRawDataFilterMask; + private final String mRenamePrefix; + private final String mRenameSuffix; + private final int mRenameBytesFrom; + private final int mRenameBytesLength; + private final int mRenameNameFrom; + private final int mRenameNameLength; + private final boolean mRenameBytesReverseOrder; + + private BluetoothLeDeviceFilter(Pattern namePattern, ScanFilter scanFilter, + byte[] rawDataFilter, byte[] rawDataFilterMask, String renamePrefix, + String renameSuffix, int renameBytesFrom, int renameBytesLength, + int renameNameFrom, int renameNameLength, boolean renameBytesReverseOrder) { + mNamePattern = namePattern; + mScanFilter = ObjectUtils.firstNotNull(scanFilter, ScanFilter.EMPTY); + mRawDataFilter = rawDataFilter; + mRawDataFilterMask = rawDataFilterMask; + mRenamePrefix = renamePrefix; + mRenameSuffix = renameSuffix; + mRenameBytesFrom = renameBytesFrom; + mRenameBytesLength = renameBytesLength; + mRenameNameFrom = renameNameFrom; + mRenameNameLength = renameNameLength; + mRenameBytesReverseOrder = renameBytesReverseOrder; + } + + /** @hide */ + @Nullable + public Pattern getNamePattern() { + return mNamePattern; + } + + /** @hide */ + @NonNull + public ScanFilter getScanFilter() { + return mScanFilter; + } + + /** @hide */ + @Nullable + public byte[] getRawDataFilter() { + return mRawDataFilter; + } + + /** @hide */ + @Nullable + public byte[] getRawDataFilterMask() { + return mRawDataFilterMask; + } + + /** @hide */ + @Nullable + public String getRenamePrefix() { + return mRenamePrefix; + } + + /** @hide */ + @Nullable + public String getRenameSuffix() { + return mRenameSuffix; + } + + /** @hide */ + public int getRenameBytesFrom() { + return mRenameBytesFrom; + } + + /** @hide */ + public int getRenameBytesLength() { + return mRenameBytesLength; + } + + /** @hide */ + public boolean isRenameBytesReverseOrder() { + return mRenameBytesReverseOrder; + } + + /** @hide */ + @Override + @Nullable + public String getDeviceDisplayName(ScanResult sr) { + if (mRenameBytesFrom < 0 && mRenameNameFrom < 0) { + return getDeviceDisplayNameInternal(sr.getDevice()); + } + final StringBuilder sb = new StringBuilder(TextUtils.emptyIfNull(mRenamePrefix)); + if (mRenameBytesFrom >= 0) { + final byte[] bytes = sr.getScanRecord().getBytes(); + int startInclusive = mRenameBytesFrom; + int endInclusive = mRenameBytesFrom + mRenameBytesLength -1; + int initial = mRenameBytesReverseOrder ? endInclusive : startInclusive; + int step = mRenameBytesReverseOrder ? -1 : 1; + for (int i = initial; startInclusive <= i && i <= endInclusive; i += step) { + sb.append(Byte.toHexString(bytes[i], true)); + } + } else { + sb.append( + getDeviceDisplayNameInternal(sr.getDevice()) + .substring(mRenameNameFrom, mRenameNameFrom + mRenameNameLength)); + } + return sb.append(TextUtils.emptyIfNull(mRenameSuffix)).toString(); + } + + /** @hide */ + @Override + public boolean matches(ScanResult device) { + boolean result = matches(device.getDevice()) + && (mRawDataFilter == null + || BitUtils.maskedEquals(device.getScanRecord().getBytes(), + mRawDataFilter, mRawDataFilterMask)); + if (DEBUG) Log.i(LOG_TAG, "matches(this = " + this + ", device = " + device + + ") -> " + result); + return result; + } + + private boolean matches(BluetoothDevice device) { + return BluetoothDeviceFilterUtils.matches(getScanFilter(), device) + && BluetoothDeviceFilterUtils.matchesName(getNamePattern(), device); + } + + /** @hide */ + @Override + public int getMediumType() { + return DeviceFilter.MEDIUM_TYPE_BLUETOOTH_LE; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BluetoothLeDeviceFilter that = (BluetoothLeDeviceFilter) o; + return mRenameBytesFrom == that.mRenameBytesFrom && + mRenameBytesLength == that.mRenameBytesLength && + mRenameNameFrom == that.mRenameNameFrom && + mRenameNameLength == that.mRenameNameLength && + mRenameBytesReverseOrder == that.mRenameBytesReverseOrder && + Objects.equals(mNamePattern, that.mNamePattern) && + Objects.equals(mScanFilter, that.mScanFilter) && + Arrays.equals(mRawDataFilter, that.mRawDataFilter) && + Arrays.equals(mRawDataFilterMask, that.mRawDataFilterMask) && + Objects.equals(mRenamePrefix, that.mRenamePrefix) && + Objects.equals(mRenameSuffix, that.mRenameSuffix); + } + + @Override + public int hashCode() { + return Objects.hash(mNamePattern, mScanFilter, mRawDataFilter, mRawDataFilterMask, + mRenamePrefix, mRenameSuffix, mRenameBytesFrom, mRenameBytesLength, + mRenameNameFrom, mRenameNameLength, mRenameBytesReverseOrder); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(patternToString(getNamePattern())); + dest.writeParcelable(mScanFilter, flags); + dest.writeByteArray(mRawDataFilter); + dest.writeByteArray(mRawDataFilterMask); + dest.writeString(mRenamePrefix); + dest.writeString(mRenameSuffix); + dest.writeInt(mRenameBytesFrom); + dest.writeInt(mRenameBytesLength); + dest.writeInt(mRenameNameFrom); + dest.writeInt(mRenameNameLength); + dest.writeBoolean(mRenameBytesReverseOrder); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "BluetoothLEDeviceFilter{" + + "mNamePattern=" + mNamePattern + + ", mScanFilter=" + mScanFilter + + ", mRawDataFilter=" + Arrays.toString(mRawDataFilter) + + ", mRawDataFilterMask=" + Arrays.toString(mRawDataFilterMask) + + ", mRenamePrefix='" + mRenamePrefix + '\'' + + ", mRenameSuffix='" + mRenameSuffix + '\'' + + ", mRenameBytesFrom=" + mRenameBytesFrom + + ", mRenameBytesLength=" + mRenameBytesLength + + ", mRenameNameFrom=" + mRenameNameFrom + + ", mRenameNameLength=" + mRenameNameLength + + ", mRenameBytesReverseOrder=" + mRenameBytesReverseOrder + + '}'; + } + + public static final Creator<BluetoothLeDeviceFilter> CREATOR + = new Creator<BluetoothLeDeviceFilter>() { + @Override + public BluetoothLeDeviceFilter createFromParcel(Parcel in) { + Builder builder = new Builder() + .setNamePattern(patternFromString(in.readString())) + .setScanFilter(in.readParcelable(null)); + byte[] rawDataFilter = in.createByteArray(); + byte[] rawDataFilterMask = in.createByteArray(); + if (rawDataFilter != null) { + builder.setRawDataFilter(rawDataFilter, rawDataFilterMask); + } + String renamePrefix = in.readString(); + String suffix = in.readString(); + int bytesFrom = in.readInt(); + int bytesTo = in.readInt(); + int nameFrom = in.readInt(); + int nameTo = in.readInt(); + boolean bytesReverseOrder = in.readBoolean(); + if (renamePrefix != null) { + if (bytesFrom >= 0) { + builder.setRenameFromBytes(renamePrefix, suffix, bytesFrom, bytesTo, + bytesReverseOrder ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN); + } else { + builder.setRenameFromName(renamePrefix, suffix, nameFrom, nameTo); + } + } + return builder.build(); + } + + @Override + public BluetoothLeDeviceFilter[] newArray(int size) { + return new BluetoothLeDeviceFilter[size]; + } + }; + + public static int getRenamePrefixLengthLimit() { + return RENAME_PREFIX_LENGTH_LIMIT; + } + + /** + * Builder for {@link BluetoothLeDeviceFilter} + */ + public static final class Builder extends OneTimeUseBuilder<BluetoothLeDeviceFilter> { + private ScanFilter mScanFilter; + private Pattern mNamePattern; + private byte[] mRawDataFilter; + private byte[] mRawDataFilterMask; + private String mRenamePrefix; + private String mRenameSuffix; + private int mRenameBytesFrom = -1; + private int mRenameBytesLength; + private int mRenameNameFrom = -1; + private int mRenameNameLength; + private boolean mRenameBytesReverseOrder = false; + + /** + * @param regex if set, only devices with {@link BluetoothDevice#getName name} matching the + * given regular expression will be shown + * @return self for chaining + */ + public Builder setNamePattern(@Nullable Pattern regex) { + checkNotUsed(); + mNamePattern = regex; + return this; + } + + /** + * @param scanFilter a {@link ScanFilter} to filter devices by + * + * @return self for chaining + * @see ScanFilter for specific details on its various fields + */ + @NonNull + public Builder setScanFilter(@Nullable ScanFilter scanFilter) { + checkNotUsed(); + mScanFilter = scanFilter; + return this; + } + + /** + * Filter devices by raw advertisement data, as obtained by {@link ScanRecord#getBytes} + * + * @param rawDataFilter bit values that have to match against advertized data + * @param rawDataFilterMask bits that have to be matched + * @return self for chaining + */ + @NonNull + public Builder setRawDataFilter(@NonNull byte[] rawDataFilter, + @Nullable byte[] rawDataFilterMask) { + checkNotUsed(); + Preconditions.checkNotNull(rawDataFilter); + checkArgument(rawDataFilterMask == null || + rawDataFilter.length == rawDataFilterMask.length, + "Mask and filter should be the same length"); + mRawDataFilter = rawDataFilter; + mRawDataFilterMask = rawDataFilterMask; + return this; + } + + /** + * Rename the devices shown in the list, using specific bytes from the raw advertisement + * data ({@link ScanRecord#getBytes}) in hexadecimal format, as well as a custom + * prefix/suffix around them + * + * Note that the prefix length is limited to {@link #getRenamePrefixLengthLimit} characters + * to ensure that there's enough space to display the byte data + * + * The range of bytes to be displayed cannot be empty + * + * @param prefix to be displayed before the byte data + * @param suffix to be displayed after the byte data + * @param bytesFrom the start byte index to be displayed (inclusive) + * @param bytesLength the number of bytes to be displayed from the given index + * @param byteOrder whether the given range of bytes is big endian (will be displayed + * in same order) or little endian (will be flipped before displaying) + * @return self for chaining + */ + @NonNull + public Builder setRenameFromBytes(@NonNull String prefix, @NonNull String suffix, + int bytesFrom, int bytesLength, ByteOrder byteOrder) { + checkRenameNotSet(); + checkRangeNotEmpty(bytesLength); + mRenameBytesFrom = bytesFrom; + mRenameBytesLength = bytesLength; + mRenameBytesReverseOrder = byteOrder == ByteOrder.LITTLE_ENDIAN; + return setRename(prefix, suffix); + } + + /** + * Rename the devices shown in the list, using specific characters from the advertised name, + * as well as a custom prefix/suffix around them + * + * Note that the prefix length is limited to {@link #getRenamePrefixLengthLimit} characters + * to ensure that there's enough space to display the byte data + * + * The range of name characters to be displayed cannot be empty + * + * @param prefix to be displayed before the byte data + * @param suffix to be displayed after the byte data + * @param nameFrom the start name character index to be displayed (inclusive) + * @param nameLength the number of characters to be displayed from the given index + * @return self for chaining + */ + @NonNull + public Builder setRenameFromName(@NonNull String prefix, @NonNull String suffix, + int nameFrom, int nameLength) { + checkRenameNotSet(); + checkRangeNotEmpty(nameLength); + mRenameNameFrom = nameFrom; + mRenameNameLength = nameLength; + mRenameBytesReverseOrder = false; + return setRename(prefix, suffix); + } + + private void checkRenameNotSet() { + checkState(mRenamePrefix == null, "Renaming rule can only be set once"); + } + + private void checkRangeNotEmpty(int length) { + checkArgument(length > 0, "Range must be non-empty"); + } + + @NonNull + private Builder setRename(@NonNull String prefix, @NonNull String suffix) { + checkNotUsed(); + checkArgument(TextUtils.length(prefix) <= getRenamePrefixLengthLimit(), + "Prefix is too long"); + mRenamePrefix = prefix; + mRenameSuffix = suffix; + return this; + } + + /** @inheritDoc */ + @Override + @NonNull + public BluetoothLeDeviceFilter build() { + markUsed(); + return new BluetoothLeDeviceFilter(mNamePattern, mScanFilter, + mRawDataFilter, mRawDataFilterMask, + mRenamePrefix, mRenameSuffix, + mRenameBytesFrom, mRenameBytesLength, + mRenameNameFrom, mRenameNameLength, + mRenameBytesReverseOrder); + } + } +} diff --git a/android/companion/CompanionDeviceManager.java b/android/companion/CompanionDeviceManager.java new file mode 100644 index 00000000..1a5de569 --- /dev/null +++ b/android/companion/CompanionDeviceManager.java @@ -0,0 +1,333 @@ +/* + * 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 com.android.internal.util.Preconditions.checkNotNull; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemService; +import android.app.Activity; +import android.app.Application; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.IntentSender; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteException; +import android.service.notification.NotificationListenerService; +import android.util.Log; + +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; + +/** + * System level service for managing companion devices + * + * <p>To obtain an instance call {@link Context#getSystemService}({@link + * Context#COMPANION_DEVICE_SERVICE}) Then, call {@link #associate(AssociationRequest, + * Callback, Handler)} to initiate the flow of associating current package with a + * device selected by user.</p> + * + * @see AssociationRequest + */ +@SystemService(Context.COMPANION_DEVICE_SERVICE) +public final class CompanionDeviceManager { + + private static final boolean DEBUG = false; + private static final String LOG_TAG = "CompanionDeviceManager"; + + /** + * A device, returned in the activity result of the {@link IntentSender} received in + * {@link Callback#onDeviceFound} + */ + public static final String EXTRA_DEVICE = "android.companion.extra.DEVICE"; + + /** + * The package name of the companion device discovery component. + * + * @hide + */ + public static final String COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME = + "com.android.companiondevicemanager"; + + /** + * A callback to receive once at least one suitable device is found, or the search failed + * (e.g. timed out) + */ + public abstract static class Callback { + + /** + * Called once at least one suitable device is found + * + * @param chooserLauncher a {@link IntentSender} to launch the UI for user to select a + * device + */ + public abstract void onDeviceFound(IntentSender chooserLauncher); + + /** + * Called if there was an error looking for device(s), e.g. timeout + * + * @param error the cause of the error + */ + public abstract void onFailure(CharSequence error); + } + + private final ICompanionDeviceManager mService; + private final Context mContext; + + /** @hide */ + public CompanionDeviceManager( + @Nullable ICompanionDeviceManager service, @NonNull Context context) { + mService = service; + mContext = context; + } + + /** + * Associate this app with a companion device, selected by user + * + * <p>Once at least one appropriate device is found, {@code callback} will be called with a + * {@link PendingIntent} that can be used to show the list of available devices for the user + * to select. + * It should be started for result (i.e. using + * {@link android.app.Activity#startIntentSenderForResult}), as the resulting + * {@link android.content.Intent} will contain extra {@link #EXTRA_DEVICE}, with the selected + * device. (e.g. {@link android.bluetooth.BluetoothDevice})</p> + * + * <p>If your app needs to be excluded from battery optimizations (run in the background) + * or to have unrestricted data access (use data in the background) you can declare that + * you use the {@link android.Manifest.permission#REQUEST_COMPANION_RUN_IN_BACKGROUND} and {@link + * android.Manifest.permission#REQUEST_COMPANION_USE_DATA_IN_BACKGROUND} respectively. Note that these + * special capabilities have a negative effect on the device's battery and user's data + * usage, therefore you should requested them when absolutely necessary.</p> + * + * <p>You can call {@link #getAssociations} to get the list of currently associated + * devices, and {@link #disassociate} to remove an association. Consider doing so when the + * association is no longer relevant to avoid unnecessary battery and/or data drain resulting + * from special privileges that the association provides</p> + * + * <p>Calling this API requires a uses-feature + * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> + * + * @param request specific details about this request + * @param callback will be called once there's at least one device found for user to choose from + * @param handler A handler to control which thread the callback will be delivered on, or null, + * to deliver it on main thread + * + * @see AssociationRequest + */ + public void associate( + @NonNull AssociationRequest request, + @NonNull Callback callback, + @Nullable Handler handler) { + if (!checkFeaturePresent()) { + return; + } + checkNotNull(request, "Request cannot be null"); + checkNotNull(callback, "Callback cannot be null"); + try { + mService.associate( + request, + new CallbackProxy(request, callback, Handler.mainIfNull(handler)), + getCallingPackage()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * <p>Calling this API requires a uses-feature + * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> + * + * @return a list of MAC addresses of devices that have been previously associated with the + * current app. You can use these with {@link #disassociate} + */ + @NonNull + public List<String> getAssociations() { + if (!checkFeaturePresent()) { + return Collections.emptyList(); + } + try { + return mService.getAssociations(getCallingPackage(), mContext.getUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Remove the association between this app and the device with the given mac address. + * + * <p>Any privileges provided via being associated with a given device will be revoked</p> + * + * <p>Consider doing so when the + * association is no longer relevant to avoid unnecessary battery and/or data drain resulting + * from special privileges that the association provides</p> + * + * <p>Calling this API requires a uses-feature + * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> + * + * @param deviceMacAddress the MAC address of device to disassociate from this app + */ + public void disassociate(@NonNull String deviceMacAddress) { + if (!checkFeaturePresent()) { + return; + } + try { + mService.disassociate(deviceMacAddress, getCallingPackage()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Request notification access for the given component. + * + * The given component must follow the protocol specified in {@link NotificationListenerService} + * + * Only components from the same {@link ComponentName#getPackageName package} as the calling app + * are allowed. + * + * Your app must have an association with a device before calling this API + * + * <p>Calling this API requires a uses-feature + * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> + */ + public void requestNotificationAccess(ComponentName component) { + if (!checkFeaturePresent()) { + return; + } + try { + IntentSender intentSender = mService.requestNotificationAccess(component) + .getIntentSender(); + mContext.startIntentSender(intentSender, null, 0, 0, 0); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (IntentSender.SendIntentException e) { + throw new RuntimeException(e); + } + } + + /** + * Check whether the given component can access the notifications via a + * {@link NotificationListenerService} + * + * Your app must have an association with a device before calling this API + * + * <p>Calling this API requires a uses-feature + * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} declaration in the manifest</p> + * + * @param component the name of the component + * @return whether the given component has the notification listener permission + */ + public boolean hasNotificationAccess(ComponentName component) { + if (!checkFeaturePresent()) { + return false; + } + try { + return mService.hasNotificationAccess(component); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private boolean checkFeaturePresent() { + boolean featurePresent = mService != null; + if (!featurePresent && DEBUG) { + Log.d(LOG_TAG, "Feature " + PackageManager.FEATURE_COMPANION_DEVICE_SETUP + + " not available"); + } + return featurePresent; + } + + private Activity getActivity() { + return (Activity) mContext; + } + + private String getCallingPackage() { + return mContext.getPackageName(); + } + + private class CallbackProxy extends IFindDeviceCallback.Stub + implements Application.ActivityLifecycleCallbacks { + + private Callback mCallback; + private Handler mHandler; + private AssociationRequest mRequest; + + final Object mLock = new Object(); + + private CallbackProxy(AssociationRequest request, Callback callback, Handler handler) { + mCallback = callback; + mHandler = handler; + mRequest = request; + getActivity().getApplication().registerActivityLifecycleCallbacks(this); + } + + @Override + public void onSuccess(PendingIntent launcher) { + lockAndPost(Callback::onDeviceFound, launcher.getIntentSender()); + } + + @Override + public void onFailure(CharSequence reason) { + lockAndPost(Callback::onFailure, reason); + } + + <T> void lockAndPost(BiConsumer<Callback, T> action, T payload) { + synchronized (mLock) { + if (mHandler != null) { + mHandler.post(() -> { + Callback callback = null; + synchronized (mLock) { + callback = mCallback; + } + if (callback != null) { + action.accept(callback, payload); + } + }); + } + } + } + + @Override + public void onActivityDestroyed(Activity activity) { + synchronized (mLock) { + if (activity != getActivity()) return; + try { + mService.stopScan(mRequest, this, getCallingPackage()); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + getActivity().getApplication().unregisterActivityLifecycleCallbacks(this); + mCallback = null; + mHandler = null; + mRequest = null; + } + } + + @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {} + @Override public void onActivityStarted(Activity activity) {} + @Override public void onActivityResumed(Activity activity) {} + @Override public void onActivityPaused(Activity activity) {} + @Override public void onActivityStopped(Activity activity) {} + @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} + } +} diff --git a/android/companion/DeviceFilter.java b/android/companion/DeviceFilter.java new file mode 100644 index 00000000..9b4fdfdf --- /dev/null +++ b/android/companion/DeviceFilter.java @@ -0,0 +1,68 @@ +/* + * 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 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.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 + * + * @hide + */ + 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 + * + * @hide + */ + 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/android/companion/WifiDeviceFilter.java b/android/companion/WifiDeviceFilter.java new file mode 100644 index 00000000..b6e704c3 --- /dev/null +++ b/android/companion/WifiDeviceFilter.java @@ -0,0 +1,139 @@ +/* + * 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.Objects; +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 boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + WifiDeviceFilter that = (WifiDeviceFilter) o; + return Objects.equals(mNamePattern, that.mNamePattern); + } + + @Override + public int hashCode() { + return Objects.hash(mNamePattern); + } + + @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); + } + } +} |