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