/* * Copyright (C) 2021 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.virtual; import static android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY; import static android.media.AudioManager.AUDIO_SESSION_ID_GENERATE; import static java.util.concurrent.TimeUnit.MICROSECONDS; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.companion.virtual.flags.Flags; import android.companion.virtual.sensor.IVirtualSensorCallback; import android.companion.virtual.sensor.VirtualSensor; import android.companion.virtual.sensor.VirtualSensorCallback; import android.companion.virtual.sensor.VirtualSensorConfig; import android.companion.virtual.sensor.VirtualSensorDirectChannelCallback; import android.content.ComponentName; import android.content.Context; import android.hardware.display.VirtualDisplayConfig; import android.os.Parcel; import android.os.Parcelable; import android.os.SharedMemory; import android.os.UserHandle; import android.util.ArraySet; import android.util.SparseArray; import android.util.SparseIntArray; import java.io.PrintWriter; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; /** * Params that can be configured when creating virtual devices. * * @hide */ @SystemApi public final class VirtualDeviceParams implements Parcelable { /** @hide */ @IntDef(prefix = "LOCK_STATE_", value = {LOCK_STATE_DEFAULT, LOCK_STATE_ALWAYS_UNLOCKED}) @Retention(RetentionPolicy.SOURCE) @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) public @interface LockState {} /** * Indicates that the lock state of the virtual device will be the same as the default physical * display. */ public static final int LOCK_STATE_DEFAULT = 0; /** * Indicates that the lock state of the virtual device should be always unlocked. */ public static final int LOCK_STATE_ALWAYS_UNLOCKED = 1; /** @hide */ @IntDef(prefix = "ACTIVITY_POLICY_", value = {ACTIVITY_POLICY_DEFAULT_ALLOWED, ACTIVITY_POLICY_DEFAULT_BLOCKED}) @Retention(RetentionPolicy.SOURCE) @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) public @interface ActivityPolicy {} /** * Indicates that activities are allowed by default on this virtual device, unless they are * explicitly blocked by {@link Builder#setBlockedActivities}. * * @deprecated Use {@link #POLICY_TYPE_ACTIVITY} and {@link #DEVICE_POLICY_DEFAULT} */ @Deprecated public static final int ACTIVITY_POLICY_DEFAULT_ALLOWED = 0; /** * Indicates that activities are blocked by default on this virtual device, unless they are * allowed by {@link Builder#setAllowedActivities}. * * @deprecated Use {@link #POLICY_TYPE_ACTIVITY} and {@link #DEVICE_POLICY_CUSTOM} */ @Deprecated public static final int ACTIVITY_POLICY_DEFAULT_BLOCKED = 1; /** @hide */ @IntDef(prefix = "NAVIGATION_POLICY_", value = {NAVIGATION_POLICY_DEFAULT_ALLOWED, NAVIGATION_POLICY_DEFAULT_BLOCKED}) @Retention(RetentionPolicy.SOURCE) @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) public @interface NavigationPolicy {} /** * Indicates that tasks are allowed to navigate to other tasks on this virtual device, * unless they are explicitly blocked by {@link Builder#setBlockedCrossTaskNavigations}. * * @deprecated Use {@link #POLICY_TYPE_ACTIVITY} and {@link #DEVICE_POLICY_DEFAULT} */ @Deprecated public static final int NAVIGATION_POLICY_DEFAULT_ALLOWED = 0; /** * Indicates that tasks are blocked from navigating to other tasks by default on this virtual * device, unless allowed by {@link Builder#setAllowedCrossTaskNavigations}. * * @deprecated Use {@link #POLICY_TYPE_ACTIVITY} and {@link #DEVICE_POLICY_CUSTOM} */ @Deprecated public static final int NAVIGATION_POLICY_DEFAULT_BLOCKED = 1; /** @hide */ @IntDef(prefix = "DEVICE_POLICY_", value = {DEVICE_POLICY_DEFAULT, DEVICE_POLICY_CUSTOM}) @Retention(RetentionPolicy.SOURCE) @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) public @interface DevicePolicy {} /** * Indicates that there is no special logic for this virtual device and it should be treated * the same way as the default device, keeping the default behavior unchanged. */ public static final int DEVICE_POLICY_DEFAULT = 0; /** * Indicates that there is custom logic, specific to this virtual device, which should be * triggered instead of the default behavior. */ public static final int DEVICE_POLICY_CUSTOM = 1; /** * Any relevant component must be able to interpret the correct meaning of a custom policy for * a given policy type. * @hide */ @IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_SENSORS, POLICY_TYPE_AUDIO, POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CAMERA}) @Retention(RetentionPolicy.SOURCE) @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) public @interface PolicyType {} /** * Policy types that can be dynamically changed during the virtual device's lifetime. * * @see VirtualDeviceManager.VirtualDevice#setDevicePolicy * @hide */ @IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CLIPBOARD}) @Retention(RetentionPolicy.SOURCE) @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) public @interface DynamicPolicyType {} /** * Tells the sensor framework how to handle sensor requests from contexts associated with this * virtual device, namely the sensors returned by * {@link android.hardware.SensorManager#getSensorList}: * *
Note: Only relevant for virtual displays that support home activities.
* * @param homeComponent The component name to be used as home. If unset, then the system- * default secondary home activity will be used. * * @see VirtualDisplayConfig#isHomeSupported() */ @FlaggedApi(Flags.FLAG_VDM_CUSTOM_HOME) @NonNull public Builder setHomeComponent(@Nullable ComponentName homeComponent) { mHomeComponent = homeComponent; return this; } /** * Specifies a component to be used as input method on all displays owned by this virtual * device. * * @param inputMethodComponent The component name to be used as input method. Must comply to * all general input method requirements described in the guide to * * Creating an Input Method. If the given component is not available for any user that * may interact with the virtual device, then there will effectively be no IME on this * device's displays for that user. * * @see android.inputmethodservice.InputMethodService * @attr ref android.R.styleable#InputMethod_isVirtualDeviceOnly * @attr ref android.R.styleable#InputMethod_showInInputMethodPicker */ @FlaggedApi(Flags.FLAG_VDM_CUSTOM_IME) @NonNull public Builder setInputMethodComponent(@Nullable ComponentName inputMethodComponent) { mInputMethodComponent = inputMethodComponent; return this; } /** * Sets the user handles with matching managed accounts on the remote device to which * this virtual device is streaming. The caller is responsible for verifying the presence * and legitimacy of a matching managed account on the remote device. * *If the app streaming policy is * {@link android.app.admin.DevicePolicyManager#NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY * NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY}, activities not in * {@code usersWithMatchingAccounts} will be blocked from starting. * *
If {@code usersWithMatchingAccounts} is empty (the default), streaming is allowed
* only if there is no device policy, or if the nearby streaming policy is
* {@link android.app.admin.DevicePolicyManager#NEARBY_STREAMING_ENABLED
* NEARBY_STREAMING_ENABLED}.
*
* @param usersWithMatchingAccounts A set of user handles with matching managed
* accounts on the remote device this is streaming to.
*
* @see android.app.admin.DevicePolicyManager#NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY
*/
@NonNull
public Builder setUsersWithMatchingAccounts(
@NonNull Set This method must not be called if {@link #setBlockedCrossTaskNavigations(Set)} has
* been called.
*
* @throws IllegalArgumentException if {@link #setBlockedCrossTaskNavigations(Set)} has been
* called.
*
* @param allowedCrossTaskNavigations A set of tasks {@link ComponentName} allowed to
* navigate to new tasks in the virtual device.
*
* @deprecated Use {@link #POLICY_TYPE_ACTIVITY} and
* {@link VirtualDeviceManager.VirtualDevice#addActivityPolicyExemption}
*/
@Deprecated
@NonNull
public Builder setAllowedCrossTaskNavigations(
@NonNull Set This method must not be called if {@link #setAllowedCrossTaskNavigations(Set)} has
* been called.
*
* @throws IllegalArgumentException if {@link #setAllowedCrossTaskNavigations(Set)} has
* been called.
*
* @param blockedCrossTaskNavigations A set of tasks {@link ComponentName} to be
* blocked from navigating to new tasks in the virtual device.
*
* @deprecated Use {@link #POLICY_TYPE_ACTIVITY} and
* {@link VirtualDeviceManager.VirtualDevice#addActivityPolicyExemption}
*/
@Deprecated
@NonNull
public Builder setBlockedCrossTaskNavigations(
@NonNull Set This method must not be called if {@link #setBlockedActivities(Set)} has been called.
*
* @throws IllegalArgumentException if {@link #setBlockedActivities(Set)} has been called.
*
* @param allowedActivities A set of activity {@link ComponentName} allowed to be launched
* in the virtual device.
*
* @deprecated Use {@link #POLICY_TYPE_ACTIVITY} and
* {@link VirtualDeviceManager.VirtualDevice#addActivityPolicyExemption}
*/
@Deprecated
@NonNull
public Builder setAllowedActivities(@NonNull Set This method must not be called if {@link #setAllowedActivities(Set)} has been called.
*
* @throws IllegalArgumentException if {@link #setAllowedActivities(Set)} has been called.
*
* @param blockedActivities A set of {@link ComponentName} to be blocked launching from
* virtual device.
*
* @deprecated Use {@link #POLICY_TYPE_ACTIVITY} and
* {@link VirtualDeviceManager.VirtualDevice#addActivityPolicyExemption}
*/
@Deprecated
@NonNull
public Builder setBlockedActivities(@NonNull Set This string is not typically intended to be displayed to end users, but rather for
* debugging and other developer-facing purposes.
*
* 3rd party applications may be able to see the name (i.e. it's not private to the
* device owner)
*/
@NonNull
public Builder setName(@NonNull String name) {
mName = name;
return this;
}
/**
* Specifies a policy for this virtual device.
*
* Policies define the system behavior that may be specific for this virtual device. A
* policy can be defined for each {@code PolicyType}, but they are all optional.
*
* @param policyType the type of policy, i.e. which behavior to specify a policy for.
* @param devicePolicy the value of the policy, i.e. how to interpret the device behavior.
*/
@NonNull
public Builder setDevicePolicy(@PolicyType int policyType, @DevicePolicy int devicePolicy) {
mDevicePolicies.put(policyType, devicePolicy);
return this;
}
/**
* Adds a configuration for a sensor that should be created for this virtual device.
*
* Device sensors must remain valid for the entire lifetime of the device, hence they are
* created together with the device itself, and removed when the device is removed.
*
* Requires {@link #DEVICE_POLICY_CUSTOM} to be set for {@link #POLICY_TYPE_SENSORS}.
*
* @see android.companion.virtual.sensor.VirtualSensor
* @see #setDevicePolicy
*/
@NonNull
public Builder addVirtualSensorConfig(@NonNull VirtualSensorConfig virtualSensorConfig) {
mVirtualSensorConfigs.add(Objects.requireNonNull(virtualSensorConfig));
return this;
}
/**
* Sets the callback to get notified about changes in the sensor configuration.
*
* @param executor The executor where the callback is executed on.
* @param callback The callback to get notified when the state of the sensor
* configuration has changed, see {@link VirtualSensorCallback}
*/
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder setVirtualSensorCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull VirtualSensorCallback callback) {
mVirtualSensorCallbackExecutor = Objects.requireNonNull(executor);
mVirtualSensorCallback = Objects.requireNonNull(callback);
return this;
}
/**
* Sets the callback to get notified about changes in
* {@link android.hardware.SensorDirectChannel} configuration.
*
* @param executor The executor where the callback is executed on.
* @param callback The callback to get notified when the state of the sensor
* configuration has changed, see {@link VirtualSensorDirectChannelCallback}
*/
@SuppressLint("MissingGetterMatchingBuilder")
@NonNull
public Builder setVirtualSensorDirectChannelCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull VirtualSensorDirectChannelCallback callback) {
mVirtualSensorDirectChannelCallbackExecutor = Objects.requireNonNull(executor);
mVirtualSensorDirectChannelCallback = Objects.requireNonNull(callback);
return this;
}
/**
* Sets audio playback session id specific for this virtual device.
*
* Audio players constructed within context associated with this virtual device
* will be automatically assigned provided session id.
*
* Requires {@link #DEVICE_POLICY_CUSTOM} to be set for {@link #POLICY_TYPE_AUDIO},
* otherwise {@link #build()} method will throw {@link IllegalArgumentException} if
* the playback session id is set to value other than
* {@link android.media.AudioManager#AUDIO_SESSION_ID_GENERATE}.
*
* @param playbackSessionId requested device-specific audio session id for playback
* @see android.media.AudioManager#generateAudioSessionId()
* @see android.media.AudioTrack.Builder#setContext(Context)
*/
@NonNull
public Builder setAudioPlaybackSessionId(int playbackSessionId) {
if (playbackSessionId < 0) {
throw new IllegalArgumentException("Invalid playback audio session id");
}
mAudioPlaybackSessionId = playbackSessionId;
return this;
}
/**
* Sets audio recording session id specific for this virtual device.
*
* {@link android.media.AudioRecord} constructed within context associated with this
* virtual device will be automatically assigned provided session id.
*
* Requires {@link #DEVICE_POLICY_CUSTOM} to be set for {@link #POLICY_TYPE_AUDIO},
* otherwise {@link #build()} method will throw {@link IllegalArgumentException} if
* the recording session id is set to value other than
* {@link android.media.AudioManager#AUDIO_SESSION_ID_GENERATE}.
*
* @param recordingSessionId requested device-specific audio session id for playback
* @see android.media.AudioManager#generateAudioSessionId()
* @see android.media.AudioRecord.Builder#setContext(Context)
*/
@NonNull
public Builder setAudioRecordingSessionId(int recordingSessionId) {
if (recordingSessionId < 0) {
throw new IllegalArgumentException("Invalid recording audio session id");
}
mAudioRecordingSessionId = recordingSessionId;
return this;
}
/**
* Builds the {@link VirtualDeviceParams} instance.
*
* @throws IllegalArgumentException if there's mismatch between policy definition and
* the passed parameters or if there are sensor configs with the same type and name.
*
*/
@NonNull
public VirtualDeviceParams build() {
VirtualSensorCallbackDelegate virtualSensorCallbackDelegate = null;
if (!mVirtualSensorConfigs.isEmpty()) {
if (mDevicePolicies.get(POLICY_TYPE_SENSORS, DEVICE_POLICY_DEFAULT)
!= DEVICE_POLICY_CUSTOM) {
throw new IllegalArgumentException(
"DEVICE_POLICY_CUSTOM for POLICY_TYPE_SENSORS is required for creating "
+ "virtual sensors.");
}
if (mVirtualSensorCallback == null) {
throw new IllegalArgumentException(
"VirtualSensorCallback is required for creating virtual sensors.");
}
for (int i = 0; i < mVirtualSensorConfigs.size(); ++i) {
if (mVirtualSensorConfigs.get(i).getDirectChannelTypesSupported() > 0) {
if (mVirtualSensorDirectChannelCallback == null) {
throw new IllegalArgumentException(
"VirtualSensorDirectChannelCallback is required for creating "
+ "virtual sensors that support direct channel.");
}
break;
}
}
virtualSensorCallbackDelegate = new VirtualSensorCallbackDelegate(
mVirtualSensorCallbackExecutor,
mVirtualSensorCallback,
mVirtualSensorDirectChannelCallbackExecutor,
mVirtualSensorDirectChannelCallback);
}
if (Flags.dynamicPolicy()) {
switch (mDevicePolicies.get(POLICY_TYPE_ACTIVITY, -1)) {
case DEVICE_POLICY_DEFAULT:
if (mDefaultActivityPolicyConfigured
&& mDefaultActivityPolicy == ACTIVITY_POLICY_DEFAULT_BLOCKED) {
throw new IllegalArgumentException(
"DEVICE_POLICY_DEFAULT is explicitly configured for "
+ "POLICY_TYPE_ACTIVITY, which is exclusive with "
+ "setAllowedActivities.");
}
break;
case DEVICE_POLICY_CUSTOM:
if (mDefaultActivityPolicyConfigured
&& mDefaultActivityPolicy == ACTIVITY_POLICY_DEFAULT_ALLOWED) {
throw new IllegalArgumentException(
"DEVICE_POLICY_CUSTOM is explicitly configured for "
+ "POLICY_TYPE_ACTIVITY, which is exclusive with "
+ "setBlockedActivities.");
}
break;
default:
if (mDefaultActivityPolicyConfigured
&& mDefaultActivityPolicy == ACTIVITY_POLICY_DEFAULT_BLOCKED) {
mDevicePolicies.put(POLICY_TYPE_ACTIVITY, DEVICE_POLICY_CUSTOM);
}
break;
}
}
if (!Flags.crossDeviceClipboard()) {
mDevicePolicies.delete(POLICY_TYPE_CLIPBOARD);
}
if (!Flags.virtualCamera()) {
mDevicePolicies.delete(POLICY_TYPE_CAMERA);
}
if ((mAudioPlaybackSessionId != AUDIO_SESSION_ID_GENERATE
|| mAudioRecordingSessionId != AUDIO_SESSION_ID_GENERATE)
&& mDevicePolicies.get(POLICY_TYPE_AUDIO, DEVICE_POLICY_DEFAULT)
!= DEVICE_POLICY_CUSTOM) {
throw new IllegalArgumentException("DEVICE_POLICY_CUSTOM for POLICY_TYPE_AUDIO is "
+ "required for configuration of device-specific audio session ids.");
}
SparseArray