/* * 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.media.AudioManager.AUDIO_SESSION_ID_GENERATE; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.UserIdInt; import android.app.PendingIntent; import android.companion.AssociationInfo; import android.companion.virtual.audio.VirtualAudioDevice; import android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback; import android.companion.virtual.sensor.VirtualSensor; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Point; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.VirtualDisplayFlag; import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplayConfig; import android.hardware.input.VirtualDpad; import android.hardware.input.VirtualDpadConfig; import android.hardware.input.VirtualKeyboard; import android.hardware.input.VirtualKeyboardConfig; import android.hardware.input.VirtualMouse; import android.hardware.input.VirtualMouseConfig; import android.hardware.input.VirtualNavigationTouchpad; import android.hardware.input.VirtualNavigationTouchpadConfig; import android.hardware.input.VirtualTouchscreen; import android.hardware.input.VirtualTouchscreenConfig; import android.media.AudioManager; import android.os.Looper; import android.os.RemoteException; import android.util.Log; import android.view.Surface; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.IntConsumer; /** * System level service for creation and management of virtual devices. * *
VirtualDeviceManager enables interactive sharing of capabilities between the host Android * device and a remote device. * *
Not to be confused with the Android Studio's Virtual Device Manager, which allows * for device emulation. */ @SystemService(Context.VIRTUAL_DEVICE_SERVICE) public final class VirtualDeviceManager { private static final String TAG = "VirtualDeviceManager"; /** * Broadcast Action: A Virtual Device was removed. * *
This is a protected intent that can only be sent by the system.
* * @hide */ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_VIRTUAL_DEVICE_REMOVED = "android.companion.virtual.action.VIRTUAL_DEVICE_REMOVED"; /** * Int intent extra to be used with {@link #ACTION_VIRTUAL_DEVICE_REMOVED}. * Contains the identifier of the virtual device, which was removed. * * @hide */ public static final String EXTRA_VIRTUAL_DEVICE_ID = "android.companion.virtual.extra.VIRTUAL_DEVICE_ID"; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef( prefix = "LAUNCH_", value = { LAUNCH_SUCCESS, LAUNCH_FAILURE_PENDING_INTENT_CANCELED, LAUNCH_FAILURE_NO_ACTIVITY}) @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) public @interface PendingIntentLaunchStatus {} /** * Status for {@link VirtualDevice#launchPendingIntent}, indicating that the launch was * successful. * * @hide */ @SystemApi public static final int LAUNCH_SUCCESS = 0; /** * Status for {@link VirtualDevice#launchPendingIntent}, indicating that the launch failed * because the pending intent was canceled. * * @hide */ @SystemApi public static final int LAUNCH_FAILURE_PENDING_INTENT_CANCELED = 1; /** * Status for {@link VirtualDevice#launchPendingIntent}, indicating that the launch failed * because no activity starts were detected as a result of calling the pending intent. * * @hide */ @SystemApi public static final int LAUNCH_FAILURE_NO_ACTIVITY = 2; private final IVirtualDeviceManager mService; private final Context mContext; /** @hide */ public VirtualDeviceManager( @Nullable IVirtualDeviceManager service, @NonNull Context context) { mService = service; mContext = context; } /** * Creates a virtual device where applications can launch and receive input events injected by * the creator. * *The {@link android.Manifest.permission#CREATE_VIRTUAL_DEVICE} permission is required to * create virtual devices, which is only available to system apps holding specific roles. * * @param associationId The association ID as returned by {@link AssociationInfo#getId()} from * Companion Device Manager. Virtual devices must have a corresponding association with CDM in * order to be created. * @param params The parameters for creating virtual devices. See {@link VirtualDeviceParams} * for the available options. * @return The created virtual device. * * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) @NonNull public VirtualDevice createVirtualDevice( int associationId, @NonNull VirtualDeviceParams params) { Objects.requireNonNull(params, "params must not be null"); try { return new VirtualDevice(mService, mContext, associationId, params); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Returns the details of all available virtual devices. * *
The returned objects are read-only representations that expose the properties of all
* existing virtual devices.
*/
@NonNull
public List In case the virtual device identifier is not valid, or there's no explicitly specified
* policy for that device and policy type, then
* {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT} is returned.
*
* @hide
*/
public @VirtualDeviceParams.DevicePolicy int getDevicePolicy(
int deviceId, @VirtualDeviceParams.PolicyType int policyType) {
if (mService == null) {
Log.w(TAG, "Failed to retrieve device policy; no virtual device manager service.");
return VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
}
try {
return mService.getDevicePolicy(deviceId, policyType);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns the ID of the device which owns the display with the given ID.
*
* @hide
*/
public int getDeviceIdForDisplayId(int displayId) {
if (mService == null) {
Log.w(TAG, "Failed to retrieve virtual devices; no virtual device manager service.");
return Context.DEVICE_ID_DEFAULT;
}
try {
return mService.getDeviceIdForDisplayId(displayId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Checks whether the passed {@code deviceId} is a valid virtual device ID or not.
* {@link Context#DEVICE_ID_DEFAULT} is not valid as it is the ID of the default
* device which is not a virtual device. {@code deviceId} must correspond to a virtual device
* created by {@link VirtualDeviceManager#createVirtualDevice(int, VirtualDeviceParams)}.
*
* @hide
*/
public boolean isValidVirtualDeviceId(int deviceId) {
if (mService == null) {
Log.w(TAG, "Failed to retrieve virtual devices; no virtual device manager service.");
return false;
}
try {
return mService.isValidVirtualDeviceId(deviceId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns device-specific audio session id for audio playback.
*
* @param deviceId - id of the virtual audio device
* @return Device specific session id to be used for audio playback (see
* {@link AudioManager#generateAudioSessionId}) if virtual device has
* {@link VirtualDeviceParams#POLICY_TYPE_AUDIO} set to
* {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM} and Virtual Audio Device
* is configured in context-aware mode. Otherwise
* {@link AudioManager#AUDIO_SESSION_ID_GENERATE} constant is returned.
*
* @hide
*/
public int getAudioPlaybackSessionId(int deviceId) {
if (mService == null) {
return AUDIO_SESSION_ID_GENERATE;
}
try {
return mService.getAudioPlaybackSessionId(deviceId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns device-specific audio session id for audio recording.
*
* @param deviceId - id of the virtual audio device
* @return Device specific session id to be used for audio recording (see
* {@link AudioManager#generateAudioSessionId}) if virtual device has
* {@link VirtualDeviceParams#POLICY_TYPE_AUDIO} set to
* {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM} and Virtual Audio Device
* is configured in context-aware mode. Otherwise
* {@link AudioManager#AUDIO_SESSION_ID_GENERATE} constant is returned.
*
* @hide
*/
public int getAudioRecordingSessionId(int deviceId) {
if (mService == null) {
return AUDIO_SESSION_ID_GENERATE;
}
try {
return mService.getAudioRecordingSessionId(deviceId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Requests sound effect to be played on virtual device.
*
* @see AudioManager#playSoundEffect(int)
*
* @param deviceId - id of the virtual audio device
* @param effectType the type of sound effect
*
* @hide
*/
public void playSoundEffect(int deviceId, @AudioManager.SystemSoundEffect int effectType) {
if (mService == null) {
Log.w(TAG, "Failed to dispatch sound effect; no virtual device manager service.");
return;
}
try {
mService.playSoundEffect(deviceId, effectType);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* A representation of a virtual device.
*
* A virtual device can have its own virtual displays, audio input/output, sensors, etc.
* The creator of a virtual device can take the output from the virtual display and stream it
* over to another device, and inject input and sensor events that are received from the remote
* device.
*
* This object is only used by the virtual device creator and allows them to manage the
* device's behavior, peripherals, and the user interaction with that device.
*
* Not to be confused with {@link android.companion.virtual.VirtualDevice},
* which is a read-only representation exposing the properties of an existing virtual device.
*
* @hide
*/
@SystemApi
public static class VirtualDevice implements AutoCloseable {
private final VirtualDeviceInternal mVirtualDeviceInternal;
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
private VirtualDevice(
IVirtualDeviceManager service,
Context context,
int associationId,
VirtualDeviceParams params) throws RemoteException {
mVirtualDeviceInternal =
new VirtualDeviceInternal(service, context, associationId, params);
}
/**
* Returns the unique ID of this virtual device.
*/
public int getDeviceId() {
return mVirtualDeviceInternal.getDeviceId();
}
/**
* Returns a new context bound to this device.
*
* This is a convenience method equivalent to calling
* {@link Context#createDeviceContext(int)} with the id of this device.
*/
public @NonNull Context createContext() {
return mVirtualDeviceInternal.createContext();
}
/**
* Returns this device's sensors.
*
* @see VirtualDeviceParams.Builder#addVirtualSensorConfig
*
* @return A list of all sensors for this device, or an empty list if no sensors exist.
*/
@NonNull
public List A touchpad in navigation mode means that its events are interpreted as navigation
* events (up, down, etc) instead of using them to update a cursor's absolute position. If
* the events are not consumed they are converted to DPAD events and delivered to the target
* again.
*
* @param config the configurations of the virtual navigation touchpad.
* @see android.view.InputDevice#SOURCE_TOUCH_NAVIGATION
*/
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
public VirtualNavigationTouchpad createVirtualNavigationTouchpad(
@NonNull VirtualNavigationTouchpadConfig config) {
return mVirtualDeviceInternal.createVirtualNavigationTouchpad(config);
}
/**
* Creates a VirtualAudioDevice, capable of recording audio emanating from this device,
* or injecting audio from another device.
*
* Note: One {@link VirtualDevice} can only create one {@link VirtualAudioDevice}, so
* calling this method multiple times will return the same instance. When
* {@link VirtualDevice#close()} is called, the associated {@link VirtualAudioDevice} will
* also be closed automatically.
*
* @param display The target virtual display to capture from and inject into.
* @param executor The {@link Executor} object for the thread on which to execute
* the callback. If Note: When there are no activities running on the virtual display, the
* {@link #onDisplayEmpty(int)} will be called. If the value topActivity is cached, it
* should be cleared when {@link #onDisplayEmpty(int)} is called.
*
* @param displayId The display ID on which the activity change happened.
* @param topActivity The component name of the top activity.
* @deprecated Use {@link #onTopActivityChanged(int, ComponentName, int)} instead
*/
void onTopActivityChanged(int displayId, @NonNull ComponentName topActivity);
/**
* Called when the top activity is changed.
*
* Note: When there are no activities running on the virtual display, the
* {@link #onDisplayEmpty(int)} will be called. If the value topActivity is cached, it
* should be cleared when {@link #onDisplayEmpty(int)} is called.
*
* @param displayId The display ID on which the activity change happened.
* @param topActivity The component name of the top activity.
* @param userId The user ID associated with the top activity.
*/
default void onTopActivityChanged(int displayId, @NonNull ComponentName topActivity,
@UserIdInt int userId) {}
/**
* Called when the display becomes empty (e.g. if the user hits back on the last
* activity of the root task).
*
* @param displayId The display ID that became empty.
*/
void onDisplayEmpty(int displayId);
}
/**
* Interceptor interface to be called when an intent matches the IntentFilter passed into {@link
* VirtualDevice#registerIntentInterceptor}. When the interceptor is called after matching the
* IntentFilter, the intended activity launch will be aborted and alternatively replaced by
* the interceptor's receiver.
*
* @hide
*/
@SystemApi
public interface IntentInterceptorCallback {
/**
* Called when an intent that matches the IntentFilter registered in {@link
* VirtualDevice#registerIntentInterceptor} is intercepted for the virtual device to
* handle.
*
* @param intent The intent that has been intercepted by the interceptor.
*/
void onIntentIntercepted(@NonNull Intent intent);
}
/**
* Listener for system sound effect playback on virtual device.
*
* @hide
*/
@SystemApi
public interface SoundEffectListener {
/**
* Called when there's a system sound effect to be played on virtual device.
*
* @param effectType - system sound effect type
* @see android.media.AudioManager.SystemSoundEffect
*/
void onPlaySoundEffect(@AudioManager.SystemSoundEffect int effectType);
}
}
null
, the {@link Executor} associated with the main
* {@link Looper} will be used.
* @param callback Interface to be notified when playback or recording configuration of
* applications running on virtual display is changed.
* @return A {@link VirtualAudioDevice} instance.
*/
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
public VirtualAudioDevice createVirtualAudioDevice(
@NonNull VirtualDisplay display,
@Nullable Executor executor,
@Nullable AudioConfigurationChangeCallback callback) {
Objects.requireNonNull(display, "display must not be null");
return mVirtualDeviceInternal.createVirtualAudioDevice(display, executor, callback);
}
/**
* Sets the visibility of the pointer icon for this VirtualDevice's associated displays.
*
* @param showPointerIcon True if the pointer should be shown; false otherwise. The default
* visibility is true.
*/
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
public void setShowPointerIcon(boolean showPointerIcon) {
mVirtualDeviceInternal.setShowPointerIcon(showPointerIcon);
}
/**
* Adds an activity listener to listen for events such as top activity change or virtual
* display task stack became empty.
*
* @param executor The executor where the listener is executed on.
* @param listener The listener to add.
* @see #removeActivityListener(ActivityListener)
*/
public void addActivityListener(
@CallbackExecutor @NonNull Executor executor, @NonNull ActivityListener listener) {
mVirtualDeviceInternal.addActivityListener(executor, listener);
}
/**
* Removes an activity listener previously added with {@link #addActivityListener}.
*
* @param listener The listener to remove.
* @see #addActivityListener(Executor, ActivityListener)
*/
public void removeActivityListener(@NonNull ActivityListener listener) {
mVirtualDeviceInternal.removeActivityListener(listener);
}
/**
* Adds a sound effect listener.
*
* @param executor The executor where the listener is executed on.
* @param soundEffectListener The listener to add.
* @see #removeActivityListener(ActivityListener)
*/
public void addSoundEffectListener(@CallbackExecutor @NonNull Executor executor,
@NonNull SoundEffectListener soundEffectListener) {
mVirtualDeviceInternal.addSoundEffectListener(executor, soundEffectListener);
}
/**
* Removes a sound effect listener previously added with {@link #addSoundEffectListener}.
*
* @param soundEffectListener The listener to remove.
* @see #addSoundEffectListener(Executor, SoundEffectListener)
*/
public void removeSoundEffectListener(@NonNull SoundEffectListener soundEffectListener) {
mVirtualDeviceInternal.removeSoundEffectListener(soundEffectListener);
}
/**
* Registers an intent interceptor that will intercept an intent attempting to launch
* when matching the provided IntentFilter and calls the callback with the intercepted
* intent.
*
* @param interceptorFilter The filter to match intents intended for interception.
* @param executor The executor where the interceptor is executed on.
* @param interceptorCallback The callback called when an intent matching interceptorFilter
* is intercepted.
* @see #unregisterIntentInterceptor(IntentInterceptorCallback)
*/
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void registerIntentInterceptor(
@NonNull IntentFilter interceptorFilter,
@CallbackExecutor @NonNull Executor executor,
@NonNull IntentInterceptorCallback interceptorCallback) {
mVirtualDeviceInternal.registerIntentInterceptor(
interceptorFilter, executor, interceptorCallback);
}
/**
* Unregisters the intent interceptor previously registered with
* {@link #registerIntentInterceptor}.
*/
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void unregisterIntentInterceptor(
@NonNull IntentInterceptorCallback interceptorCallback) {
mVirtualDeviceInternal.unregisterIntentInterceptor(interceptorCallback);
}
}
/**
* Listener for activity changes in this virtual device.
*
* @hide
*/
@SystemApi
public interface ActivityListener {
/**
* Called when the top activity is changed.
*
*