/*
* 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.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED;
import static com.android.internal.util.CollectionUtils.emptyIfNull;
import static java.util.Objects.requireNonNull;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.StringDef;
import android.annotation.UserIdInt;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.OneTimeUseBuilder;
import com.android.internal.util.ArrayUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
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 {
/**
* Device profile: watch.
*
* If specified, the current request may have a modified UI to highlight that the device being
* set up is a specific kind of device, and some extra permissions may be granted to the app
* as a result.
*
* Using it requires declaring uses-permission
* {@link android.Manifest.permission#REQUEST_COMPANION_PROFILE_WATCH} in the manifest.
*
* Learn more
* about device profiles.
*
* @see AssociationRequest.Builder#setDeviceProfile
*/
public static final String DEVICE_PROFILE_WATCH = "android.app.role.COMPANION_DEVICE_WATCH";
/**
* Device profile: glasses.
*
* If specified, the current request may have a modified UI to highlight that the device being
* set up is a glasses device, and some extra permissions may be granted to the app
* as a result.
*
* Using it requires declaring uses-permission
* {@link android.Manifest.permission#REQUEST_COMPANION_PROFILE_GLASSES} in the manifest.
*
* @see AssociationRequest.Builder#setDeviceProfile
*/
@RequiresPermission(Manifest.permission.REQUEST_COMPANION_PROFILE_GLASSES)
public static final String DEVICE_PROFILE_GLASSES = "android.app.role.COMPANION_DEVICE_GLASSES";
/**
* Device profile: a virtual display capable of rendering Android applications, and sending back
* input events.
*
* Only applications that have been granted
* {@link android.Manifest.permission#REQUEST_COMPANION_PROFILE_APP_STREAMING} are allowed to
* request to be associated with such devices.
*
* @see AssociationRequest.Builder#setDeviceProfile
*/
@RequiresPermission(Manifest.permission.REQUEST_COMPANION_PROFILE_APP_STREAMING)
public static final String DEVICE_PROFILE_APP_STREAMING =
"android.app.role.COMPANION_DEVICE_APP_STREAMING";
/**
* Device profile: a virtual device capable of rendering content from an Android host to a
* nearby device.
*
* Only applications that have been granted
* {@link android.Manifest.permission#REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING}
* are allowed to request to be associated with such devices.
*
* @see AssociationRequest.Builder#setDeviceProfile
*/
@RequiresPermission(Manifest.permission.REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING)
public static final String DEVICE_PROFILE_NEARBY_DEVICE_STREAMING =
"android.app.role.COMPANION_DEVICE_NEARBY_DEVICE_STREAMING";
/**
* Device profile: Android Automotive Projection
*
* Only applications that have been granted
* {@link android.Manifest.permission#REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION} are
* allowed to request to be associated with such devices.
*
* @see AssociationRequest.Builder#setDeviceProfile
*/
@RequiresPermission(Manifest.permission.REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION)
public static final String DEVICE_PROFILE_AUTOMOTIVE_PROJECTION =
"android.app.role.SYSTEM_AUTOMOTIVE_PROJECTION";
/**
* Device profile: Allows the companion app to access notification, recent photos and media for
* computer cross-device features.
*
* Only applications that have been granted
* {@link android.Manifest.permission#REQUEST_COMPANION_PROFILE_COMPUTER} are allowed to
* request to be associated with such devices.
*
* @see AssociationRequest.Builder#setDeviceProfile
*/
@RequiresPermission(Manifest.permission.REQUEST_COMPANION_PROFILE_COMPUTER)
public static final String DEVICE_PROFILE_COMPUTER =
"android.app.role.COMPANION_DEVICE_COMPUTER";
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@StringDef(value = { DEVICE_PROFILE_WATCH, DEVICE_PROFILE_COMPUTER,
DEVICE_PROFILE_AUTOMOTIVE_PROJECTION, DEVICE_PROFILE_APP_STREAMING,
DEVICE_PROFILE_GLASSES, DEVICE_PROFILE_NEARBY_DEVICE_STREAMING })
public @interface DeviceProfile {}
/**
* Whether only a single device should match the provided filter.
*
* When scanning for a single device with a specific {@link BluetoothDeviceFilter} mac
* address, bonded devices are also searched among. This allows to obtain the necessary app
* privileges even if the device is already paired.
*/
private final boolean mSingleDevice;
/**
* If set, only devices matching either of the given filters will be shown to the user
*/
@NonNull
private final List> mDeviceFilters;
/**
* Profile of the device.
*/
@Nullable
@DeviceProfile
private final String mDeviceProfile;
/**
* The Display name of the device to be shown in the CDM confirmation UI. Must be non-null for
* "self-managed" association.
*/
@Nullable
private CharSequence mDisplayName;
/**
* The device that was associated. Will be null for "self-managed" association.
*/
@Nullable
private AssociatedDevice mAssociatedDevice;
/**
* Whether the association is to be managed by the companion application.
*/
private final boolean mSelfManaged;
/**
* Indicates that the application would prefer the CompanionDeviceManager to collect an explicit
* confirmation from the user before creating an association, even if such confirmation is not
* required.
*/
private final boolean mForceConfirmation;
/**
* The app package name of the application the association will belong to.
* Populated by the system.
* @hide
*/
@Nullable
private String mPackageName;
/**
* The UserId of the user the association will belong to.
* Populated by the system.
* @hide
*/
@UserIdInt
private int mUserId;
/**
* The user-readable description of the device profile's privileges.
* Populated by the system.
* @hide
*/
@Nullable
private String mDeviceProfilePrivilegesDescription;
/**
* The time at which his request was created
* @hide
*/
private final long mCreationTime;
/**
* Whether the user-prompt may be skipped once the device is found.
* Populated by the system.
* @hide
*/
private boolean mSkipPrompt;
/**
* Creates a new AssociationRequest.
*
* @param singleDevice
* Whether only a single device should match the provided filter.
*
* When scanning for a single device with a specific {@link BluetoothDeviceFilter} mac
* address, bonded devices are also searched among. This allows to obtain the necessary app
* privileges even if the device is already paired.
* @param deviceFilters
* If set, only devices matching either of the given filters will be shown to the user
* @param deviceProfile
* Profile of the device.
* @param displayName
* The Display name of the device to be shown in the CDM confirmation UI. Must be non-null for
* "self-managed" association.
* @param selfManaged
* Whether the association is to be managed by the companion application.
*/
private AssociationRequest(
boolean singleDevice,
@NonNull List> deviceFilters,
@Nullable @DeviceProfile String deviceProfile,
@Nullable CharSequence displayName,
boolean selfManaged,
boolean forceConfirmation) {
mSingleDevice = singleDevice;
mDeviceFilters = requireNonNull(deviceFilters);
mDeviceProfile = deviceProfile;
mDisplayName = displayName;
mSelfManaged = selfManaged;
mForceConfirmation = forceConfirmation;
mCreationTime = System.currentTimeMillis();
}
/**
* @return profile of the companion device.
*/
@Nullable
@DeviceProfile
public String getDeviceProfile() {
return mDeviceProfile;
}
/**
* The Display name of the device to be shown in the CDM confirmation UI. Must be non-null for
* "self-managed" association.
*/
@Nullable
public CharSequence getDisplayName() {
return mDisplayName;
}
/**
* Whether the association is to be managed by the companion application.
*
* @see Builder#setSelfManaged(boolean)
*/
public boolean isSelfManaged() {
return mSelfManaged;
}
/**
* Indicates whether the application requires the {@link CompanionDeviceManager} service to
* collect an explicit confirmation from the user before creating an association, even if
* such confirmation is not required from the service's perspective.
*
* @see Builder#setForceConfirmation(boolean)
*/
public boolean isForceConfirmation() {
return mForceConfirmation;
}
/**
* Whether only a single device should match the provided filter.
*
* When scanning for a single device with a specific {@link BluetoothDeviceFilter} mac
* address, bonded devices are also searched among. This allows to obtain the necessary app
* privileges even if the device is already paired.
*/
public boolean isSingleDevice() {
return mSingleDevice;
}
/** @hide */
public void setPackageName(@NonNull String packageName) {
mPackageName = packageName;
}
/** @hide */
public void setUserId(@UserIdInt int userId) {
mUserId = userId;
}
/** @hide */
public void setDeviceProfilePrivilegesDescription(@NonNull String desc) {
mDeviceProfilePrivilegesDescription = desc;
}
/** @hide */
public void setSkipPrompt(boolean value) {
mSkipPrompt = value;
}
/** @hide */
public void setDisplayName(CharSequence displayName) {
mDisplayName = displayName;
}
/** @hide */
public void setAssociatedDevice(AssociatedDevice associatedDevice) {
mAssociatedDevice = associatedDevice;
}
/** @hide */
@NonNull
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public List> getDeviceFilters() {
return mDeviceFilters;
}
/**
* A builder for {@link AssociationRequest}
*/
public static final class Builder extends OneTimeUseBuilder {
private boolean mSingleDevice = false;
private ArrayList> mDeviceFilters = null;
private String mDeviceProfile;
private CharSequence mDisplayName;
private boolean mSelfManaged = false;
private boolean mForceConfirmation = false;
public Builder() {}
/**
* Whether only a single device should match the provided filter.
*
* When scanning for a single device with a specific {@link BluetoothDeviceFilter} mac
* address, bonded devices are also searched among. This allows to obtain the necessary app
* privileges even if the device is already paired.
*
* @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;
}
/**
* If set, association will be requested as a corresponding kind of device
*/
@NonNull
public Builder setDeviceProfile(@NonNull @DeviceProfile String deviceProfile) {
checkNotUsed();
mDeviceProfile = deviceProfile;
return this;
}
/**
* Adds a display name.
* Generally {@link AssociationRequest}s are not required to provide a display name, except
* for request for creating "self-managed" associations, which MUST provide a display name.
*
* @param displayName the display name of the device.
*/
@NonNull
public Builder setDisplayName(@NonNull CharSequence displayName) {
checkNotUsed();
mDisplayName = requireNonNull(displayName);
return this;
}
/**
* Indicate whether the association would be managed by the companion application.
*
* Requests for creating "self-managed" association MUST provide a Display name.
*
* @see #setDisplayName(CharSequence)
*/
@RequiresPermission(REQUEST_COMPANION_SELF_MANAGED)
@NonNull
public Builder setSelfManaged(boolean selfManaged) {
checkNotUsed();
mSelfManaged = selfManaged;
return this;
}
/**
* Indicates whether the application requires the {@link CompanionDeviceManager} service to
* collect an explicit confirmation from the user before creating an association, even if
* such confirmation is not required from the service's perspective.
*/
@RequiresPermission(REQUEST_COMPANION_SELF_MANAGED)
@NonNull
public Builder setForceConfirmation(boolean forceConfirmation) {
checkNotUsed();
mForceConfirmation = forceConfirmation;
return this;
}
/** @inheritDoc */
@NonNull
@Override
public AssociationRequest build() {
markUsed();
if (mSelfManaged && mDisplayName == null) {
throw new IllegalStateException("Request for a self-managed association MUST "
+ "provide the display name of the device");
}
return new AssociationRequest(mSingleDevice, emptyIfNull(mDeviceFilters),
mDeviceProfile, mDisplayName, mSelfManaged, mForceConfirmation);
}
}
/**
* The device that was associated. Will be null for "self-managed" association.
*
* @hide
*/
@Nullable
public AssociatedDevice getAssociatedDevice() {
return mAssociatedDevice;
}
/**
* The app package name of the application the association will belong to.
* Populated by the system.
*
* @hide
*/
@Nullable
public String getPackageName() {
return mPackageName;
}
/**
* The UserId of the user the association will belong to.
* Populated by the system.
*
* @hide
*/
@UserIdInt
public int getUserId() {
return mUserId;
}
/**
* The user-readable description of the device profile's privileges.
* Populated by the system.
*
* @hide
*/
@Nullable
public String getDeviceProfilePrivilegesDescription() {
return mDeviceProfilePrivilegesDescription;
}
/**
* The time at which his request was created
*
* @hide
*/
public long getCreationTime() {
return mCreationTime;
}
/**
* Whether the user-prompt may be skipped once the device is found.
* Populated by the system.
*
* @hide
*/
public boolean isSkipPrompt() {
return mSkipPrompt;
}
@Override
public String toString() {
return "AssociationRequest { "
+ "singleDevice = " + mSingleDevice
+ ", deviceFilters = " + mDeviceFilters
+ ", deviceProfile = " + mDeviceProfile
+ ", displayName = " + mDisplayName
+ ", associatedDevice = " + mAssociatedDevice
+ ", selfManaged = " + mSelfManaged
+ ", forceConfirmation = " + mForceConfirmation
+ ", packageName = " + mPackageName
+ ", userId = " + mUserId
+ ", deviceProfilePrivilegesDescription = " + mDeviceProfilePrivilegesDescription
+ ", creationTime = " + mCreationTime
+ ", skipPrompt = " + mSkipPrompt
+ " }";
}
@Override
public boolean equals(@Nullable 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)
&& Objects.equals(mDeviceProfile, that.mDeviceProfile)
&& Objects.equals(mDisplayName, that.mDisplayName)
&& Objects.equals(mAssociatedDevice, that.mAssociatedDevice)
&& mSelfManaged == that.mSelfManaged
&& mForceConfirmation == that.mForceConfirmation
&& Objects.equals(mPackageName, that.mPackageName)
&& mUserId == that.mUserId
&& Objects.equals(mDeviceProfilePrivilegesDescription,
that.mDeviceProfilePrivilegesDescription)
&& mCreationTime == that.mCreationTime
&& mSkipPrompt == that.mSkipPrompt;
}
@Override
public int hashCode() {
int _hash = 1;
_hash = 31 * _hash + Boolean.hashCode(mSingleDevice);
_hash = 31 * _hash + Objects.hashCode(mDeviceFilters);
_hash = 31 * _hash + Objects.hashCode(mDeviceProfile);
_hash = 31 * _hash + Objects.hashCode(mDisplayName);
_hash = 31 * _hash + Objects.hashCode(mAssociatedDevice);
_hash = 31 * _hash + Boolean.hashCode(mSelfManaged);
_hash = 31 * _hash + Boolean.hashCode(mForceConfirmation);
_hash = 31 * _hash + Objects.hashCode(mPackageName);
_hash = 31 * _hash + mUserId;
_hash = 31 * _hash + Objects.hashCode(mDeviceProfilePrivilegesDescription);
_hash = 31 * _hash + Long.hashCode(mCreationTime);
_hash = 31 * _hash + Boolean.hashCode(mSkipPrompt);
return _hash;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
int flg = 0;
if (mSingleDevice) flg |= 0x1;
if (mSelfManaged) flg |= 0x2;
if (mForceConfirmation) flg |= 0x4;
if (mSkipPrompt) flg |= 0x8;
if (mDeviceProfile != null) flg |= 0x10;
if (mDisplayName != null) flg |= 0x20;
if (mAssociatedDevice != null) flg |= 0x40;
if (mPackageName != null) flg |= 0x80;
if (mDeviceProfilePrivilegesDescription != null) flg |= 0x100;
dest.writeInt(flg);
dest.writeParcelableList(mDeviceFilters, flags);
if (mDeviceProfile != null) dest.writeString(mDeviceProfile);
if (mDisplayName != null) dest.writeCharSequence(mDisplayName);
if (mAssociatedDevice != null) dest.writeTypedObject(mAssociatedDevice, flags);
if (mPackageName != null) dest.writeString(mPackageName);
dest.writeInt(mUserId);
if (mDeviceProfilePrivilegesDescription != null) {
dest.writeString8(mDeviceProfilePrivilegesDescription);
}
dest.writeLong(mCreationTime);
}
@Override
public int describeContents() {
return 0;
}
/** @hide */
@SuppressWarnings("unchecked")
/* package-private */ AssociationRequest(@NonNull Parcel in) {
int flg = in.readInt();
boolean singleDevice = (flg & 0x1) != 0;
boolean selfManaged = (flg & 0x2) != 0;
boolean forceConfirmation = (flg & 0x4) != 0;
boolean skipPrompt = (flg & 0x8) != 0;
List> deviceFilters = new ArrayList<>();
in.readParcelableList(deviceFilters, DeviceFilter.class.getClassLoader(),
(Class>) (Class>)
android.companion.DeviceFilter.class);
String deviceProfile = (flg & 0x10) == 0 ? null : in.readString();
CharSequence displayName = (flg & 0x20) == 0 ? null : in.readCharSequence();
AssociatedDevice associatedDevice = (flg & 0x40) == 0 ? null
: in.readTypedObject(AssociatedDevice.CREATOR);
String packageName = (flg & 0x80) == 0 ? null : in.readString();
int userId = in.readInt();
String deviceProfilePrivilegesDescription = (flg & 0x100) == 0 ? null : in.readString8();
long creationTime = in.readLong();
this.mSingleDevice = singleDevice;
this.mDeviceFilters = deviceFilters;
com.android.internal.util.AnnotationValidations.validate(
NonNull.class, null, mDeviceFilters);
this.mDeviceProfile = deviceProfile;
this.mDisplayName = displayName;
this.mAssociatedDevice = associatedDevice;
this.mSelfManaged = selfManaged;
this.mForceConfirmation = forceConfirmation;
this.mPackageName = packageName;
this.mUserId = userId;
com.android.internal.util.AnnotationValidations.validate(
UserIdInt.class, null, mUserId);
this.mDeviceProfilePrivilegesDescription = deviceProfilePrivilegesDescription;
this.mCreationTime = creationTime;
this.mSkipPrompt = skipPrompt;
}
@NonNull
public static final Parcelable.Creator CREATOR =
new Parcelable.Creator() {
@Override
public AssociationRequest[] newArray(int size) {
return new AssociationRequest[size];
}
@Override
public AssociationRequest createFromParcel(@NonNull Parcel in) {
return new AssociationRequest(in);
}
};
}