| /* |
| * 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. |
| * |
| * <a href="{@docRoot}about/versions/12/features#cdm-profiles">Learn more</a> |
| * 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<DeviceFilter<?>> 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<DeviceFilter<?>> 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<DeviceFilter<?>> getDeviceFilters() { |
| return mDeviceFilters; |
| } |
| |
| /** |
| * A builder for {@link AssociationRequest} |
| */ |
| public static final class Builder extends OneTimeUseBuilder<AssociationRequest> { |
| private boolean mSingleDevice = false; |
| private ArrayList<DeviceFilter<?>> 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<DeviceFilter<?>> deviceFilters = new ArrayList<>(); |
| in.readParcelableList(deviceFilters, DeviceFilter.class.getClassLoader(), |
| (Class<android.companion.DeviceFilter<?>>) (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<AssociationRequest> CREATOR = |
| new Parcelable.Creator<AssociationRequest>() { |
| @Override |
| public AssociationRequest[] newArray(int size) { |
| return new AssociationRequest[size]; |
| } |
| |
| @Override |
| public AssociationRequest createFromParcel(@NonNull Parcel in) { |
| return new AssociationRequest(in); |
| } |
| }; |
| } |