/* * 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 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.SystemApi; import android.annotation.SystemService; import android.app.PendingIntent; import android.companion.AssociationInfo; import android.companion.virtual.audio.VirtualAudioDevice; import android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback; import android.content.ComponentName; import android.content.Context; import android.graphics.Point; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.VirtualDisplayFlag; import android.hardware.display.DisplayManagerGlobal; import android.hardware.display.IVirtualDisplayCallback; import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplayConfig; import android.hardware.input.VirtualKeyboard; import android.hardware.input.VirtualMouse; import android.hardware.input.VirtualTouchscreen; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.os.ResultReceiver; import android.util.ArrayMap; 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.concurrent.Executor; import java.util.function.IntConsumer; /** * System level service for managing virtual devices. * * @hide */ @SystemApi @SystemService(Context.VIRTUAL_DEVICE_SERVICE) public final class VirtualDeviceManager { private static final boolean DEBUG = false; private static final String TAG = "VirtualDeviceManager"; private static final int DEFAULT_VIRTUAL_DISPLAY_FLAGS = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC | DisplayManager.VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | DisplayManager.VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL | DisplayManager.VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP; /** @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. */ public static final int LAUNCH_SUCCESS = 0; /** * Status for {@link VirtualDevice#launchPendingIntent}, indicating that the launch failed * because the pending intent was canceled. */ 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. */ 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.
*/
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
public VirtualDevice createVirtualDevice(
int associationId,
@NonNull VirtualDeviceParams params) {
try {
return new VirtualDevice(mService, mContext, associationId, params);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* A virtual device has its own virtual display, audio output, microphone, and camera 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 events that are received from the remote device.
*
* TODO(b/204081582): Consider using a builder pattern for the input APIs.
*/
public static class VirtualDevice implements AutoCloseable {
private final Context mContext;
private final IVirtualDeviceManager mService;
private final IVirtualDevice mVirtualDevice;
private final ArrayMap 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.
*/
void onTopActivityChanged(int displayId, @NonNull ComponentName topActivity);
/**
* 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);
}
/**
* A wrapper for {@link ActivityListener} that executes callbacks on the given executor.
*/
private static class ActivityListenerDelegate {
@NonNull private final ActivityListener mActivityListener;
@NonNull private final Executor mExecutor;
ActivityListenerDelegate(@NonNull ActivityListener listener, @NonNull Executor executor) {
mActivityListener = listener;
mExecutor = executor;
}
public void onTopActivityChanged(int displayId, ComponentName topActivity) {
mExecutor.execute(() -> mActivityListener.onTopActivityChanged(displayId, topActivity));
}
public void onDisplayEmpty(int displayId) {
mExecutor.execute(() -> mActivityListener.onDisplayEmpty(displayId));
}
}
}
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) {
if (mVirtualAudioDevice == null) {
mVirtualAudioDevice = new VirtualAudioDevice(
mContext, mVirtualDevice, display, executor, callback);
}
return mVirtualAudioDevice;
}
/**
* 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) {
try {
mVirtualDevice.setShowPointerIcon(showPointerIcon);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Returns the display flags that should be added to a particular virtual display.
* Additional device-level flags from {@link
* com.android.server.companion.virtual.VirtualDeviceImpl#getBaseVirtualDisplayFlags()} will
* be added by DisplayManagerService.
*/
private int getVirtualDisplayFlags(int flags) {
return DEFAULT_VIRTUAL_DISPLAY_FLAGS | flags;
}
private String getVirtualDisplayName() {
try {
// Currently this just use the association ID, which means all of the virtual
// displays created using the same virtual device will have the same name. The name
// should only be used for informational purposes, and not for identifying the
// display in code.
return "VirtualDevice_" + mVirtualDevice.getAssociationId();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* 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) {
mActivityListeners.put(listener, new ActivityListenerDelegate(listener, executor));
}
/**
* 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) {
mActivityListeners.remove(listener);
}
}
/**
* Listener for activity changes in this virtual device.
*/
public interface ActivityListener {
/**
* Called when the top activity is changed.
*
*