diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl index 82ad15057fe3d..85855bedfbeb5 100644 --- a/core/java/android/companion/virtual/IVirtualDevice.aidl +++ b/core/java/android/companion/virtual/IVirtualDevice.aidl @@ -16,12 +16,14 @@ package android.companion.virtual; +import android.app.PendingIntent; import android.graphics.Point; import android.hardware.input.VirtualKeyEvent; import android.hardware.input.VirtualMouseButtonEvent; import android.hardware.input.VirtualMouseRelativeEvent; import android.hardware.input.VirtualMouseScrollEvent; import android.hardware.input.VirtualTouchEvent; +import android.os.ResultReceiver; /** * Interface for a virtual device. @@ -41,6 +43,7 @@ interface IVirtualDevice { * Closes the virtual device and frees all associated resources. */ void close(); + void createVirtualKeyboard( int displayId, String inputDeviceName, @@ -66,4 +69,10 @@ interface IVirtualDevice { boolean sendRelativeEvent(IBinder token, in VirtualMouseRelativeEvent event); boolean sendScrollEvent(IBinder token, in VirtualMouseScrollEvent event); boolean sendTouchEvent(IBinder token, in VirtualTouchEvent event); + + /** + * Launches a pending intent on the given display that is owned by this virtual device. + */ + void launchPendingIntent( + int displayId, in PendingIntent pendingIntent, in ResultReceiver resultReceiver); } diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index 858e4daa136f0..8ab668873f334 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -23,6 +23,8 @@ import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; +import android.app.Activity; +import android.app.PendingIntent; import android.companion.AssociationInfo; import android.content.Context; import android.graphics.Point; @@ -33,15 +35,19 @@ 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.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; /** * System level service for managing virtual devices. @@ -128,6 +134,49 @@ public final class VirtualDeviceManager { mVirtualDevice = virtualDevice; } + /** + * Launches a given pending intent on the give display ID. + * + * @param displayId The display to launch the pending intent on. This display must be + * created from this virtual device. + * @param pendingIntent The pending intent to be launched. If the intent is an activity + * intent, the activity will be started on the virtual display using + * {@link android.app.ActivityOptions#setLaunchDisplayId}. If the intent is a service or + * broadcast intent, an attempt will be made to catch activities started as a result of + * sending the pending intent and move them to the given display. + * @param executor The executor to run {@code launchCallback} on. + * @param launchCallback Callback that is called when the pending intent launching is + * complete. + * + * @hide + */ + public void launchPendingIntent( + int displayId, + @NonNull PendingIntent pendingIntent, + @NonNull Executor executor, + @NonNull LaunchCallback launchCallback) { + try { + mVirtualDevice.launchPendingIntent( + displayId, + pendingIntent, + new ResultReceiver(new Handler(Looper.myLooper())) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + super.onReceiveResult(resultCode, resultData); + executor.execute(() -> { + if (resultCode == Activity.RESULT_OK) { + launchCallback.onLaunchSuccess(); + } else { + launchCallback.onLaunchFailed(); + } + }); + } + }); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + /** * Creates a virtual display for this virtual device. All displays created on the same * device belongs to the same display group. @@ -299,4 +348,22 @@ public final class VirtualDeviceManager { } } } + + /** + * Callback for launching pending intents on the virtual device. + * + * @hide + */ + // TODO(b/194949534): Unhide this API + public interface LaunchCallback { + /** + * Called when the pending intent launched successfully. + */ + void onLaunchSuccess(); + + /** + * Called when the pending intent failed to launch. + */ + void onLaunchFailed(); + } } diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 1bb95f8db337f..0c0ee521af0f6 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -23,6 +23,9 @@ import static android.view.WindowManager.LayoutParams.FLAG_SECURE; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; import android.annotation.NonNull; +import android.app.Activity; +import android.app.ActivityOptions; +import android.app.PendingIntent; import android.app.admin.DevicePolicyManager; import android.companion.AssociationInfo; import android.companion.virtual.IVirtualDevice; @@ -38,9 +41,11 @@ import android.hardware.input.VirtualTouchEvent; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; +import android.os.ResultReceiver; import android.os.UserHandle; import android.os.UserManager; import android.util.ArraySet; +import android.util.Slog; import android.util.SparseArray; import android.window.DisplayWindowPolicyController; @@ -55,10 +60,12 @@ import java.util.List; final class VirtualDeviceImpl extends IVirtualDevice.Stub implements IBinder.DeathRecipient { + private static final String TAG = "VirtualDeviceImpl"; private final Object mVirtualDeviceLock = new Object(); private final Context mContext; private final AssociationInfo mAssociationInfo; + private final PendingTrampolineCallback mPendingTrampolineCallback; private final int mOwnerUid; private final InputController mInputController; @VisibleForTesting @@ -76,17 +83,18 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub VirtualDeviceImpl(Context context, AssociationInfo associationInfo, IBinder token, int ownerUid, OnDeviceCloseListener listener, - VirtualDeviceParams params) { + PendingTrampolineCallback pendingTrampolineCallback, VirtualDeviceParams params) { this(context, associationInfo, token, ownerUid, /* inputController= */ null, listener, - params); + pendingTrampolineCallback, params); } @VisibleForTesting VirtualDeviceImpl(Context context, AssociationInfo associationInfo, IBinder token, int ownerUid, InputController inputController, OnDeviceCloseListener listener, - VirtualDeviceParams params) { + PendingTrampolineCallback pendingTrampolineCallback, VirtualDeviceParams params) { mContext = context; mAssociationInfo = associationInfo; + mPendingTrampolineCallback = pendingTrampolineCallback; mOwnerUid = ownerUid; mAppToken = token; mParams = params; @@ -120,6 +128,49 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub return mAssociationInfo.getId(); } + @Override // Binder call + public void launchPendingIntent(int displayId, PendingIntent pendingIntent, + ResultReceiver resultReceiver) { + if (!mVirtualDisplayIds.contains(displayId)) { + throw new SecurityException("Display ID " + displayId + + " not found for this virtual device"); + } + if (pendingIntent.isActivity()) { + try { + sendPendingIntent(displayId, pendingIntent); + resultReceiver.send(Activity.RESULT_OK, null); + } catch (PendingIntent.CanceledException e) { + Slog.w(TAG, "Pending intent canceled", e); + resultReceiver.send(Activity.RESULT_CANCELED, null); + } + } else { + PendingTrampoline pendingTrampoline = new PendingTrampoline(pendingIntent, + resultReceiver, displayId); + mPendingTrampolineCallback.startWaitingForPendingTrampoline(pendingTrampoline); + try { + sendPendingIntent(displayId, pendingIntent); + } catch (PendingIntent.CanceledException e) { + Slog.w(TAG, "Pending intent canceled", e); + resultReceiver.send(Activity.RESULT_CANCELED, null); + mPendingTrampolineCallback.stopWaitingForPendingTrampoline(pendingTrampoline); + } + } + } + + private void sendPendingIntent(int displayId, PendingIntent pendingIntent) + throws PendingIntent.CanceledException { + pendingIntent.send( + mContext, + /* code= */ 0, + /* intent= */ null, + /* onFinished= */ null, + /* handler= */ null, + /* requiredPermission= */ null, + ActivityOptions.makeBasic() + .setLaunchDisplayId(displayId) + .toBundle()); + } + @Override // Binder call public void close() { mListener.onClose(mAssociationInfo.getId()); @@ -276,6 +327,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { fout.println(" VirtualDevice: "); + fout.println(" mAssociationId: " + mAssociationInfo.getId()); fout.println(" mVirtualDisplayIds: "); synchronized (mVirtualDeviceLock) { for (int id : mVirtualDisplayIds) { @@ -346,4 +398,58 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub interface OnDeviceCloseListener { void onClose(int associationId); } + + interface PendingTrampolineCallback { + /** + * Called when the callback should start waiting for the given pending trampoline. + * Implementations should try to listen for activity starts associated with the given + * {@code pendingTrampoline}, and launch the activity on the display with + * {@link PendingTrampoline#mDisplayId}. + */ + void startWaitingForPendingTrampoline(PendingTrampoline pendingTrampoline); + + /** + * Called when the callback should stop waiting for the given pending trampoline. This can + * happen, for example, when the pending intent failed to send. + */ + void stopWaitingForPendingTrampoline(PendingTrampoline pendingTrampoline); + } + + /** + * A data class storing a pending trampoline this device is expecting. + */ + static class PendingTrampoline { + + /** + * The original pending intent sent, for which a trampoline activity launch is expected. + */ + final PendingIntent mPendingIntent; + + /** + * The result receiver associated with this pending call. {@link Activity#RESULT_OK} will + * be sent to the receiver if the trampoline activity was captured successfully. + * {@link Activity#RESULT_CANCELED} is sent otherwise. + */ + final ResultReceiver mResultReceiver; + + /** + * The display ID to send the captured trampoline activity launch to. + */ + final int mDisplayId; + + private PendingTrampoline(PendingIntent pendingIntent, ResultReceiver resultReceiver, + int displayId) { + mPendingIntent = pendingIntent; + mResultReceiver = resultReceiver; + mDisplayId = displayId; + } + + @Override + public String toString() { + return "PendingTrampoline{" + + "pendingIntent=" + mPendingIntent + + ", resultReceiver=" + mResultReceiver + + ", displayId=" + mDisplayId + "}"; + } + } } diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index 12df79d2ddc90..7e0c2fc37da6f 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -16,9 +16,13 @@ package com.android.server.companion.virtual; +import static com.android.server.wm.ActivityInterceptorCallback.VIRTUAL_DEVICE_SERVICE_ORDERED_ID; + import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.ActivityOptions; import android.companion.AssociationInfo; import android.companion.CompanionDeviceManager; import android.companion.CompanionDeviceManager.OnAssociationsChangedListener; @@ -26,7 +30,9 @@ import android.companion.virtual.IVirtualDevice; import android.companion.virtual.IVirtualDeviceManager; import android.companion.virtual.VirtualDeviceParams; import android.content.Context; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.os.Parcel; import android.os.RemoteException; import android.util.ExceptionUtils; @@ -37,6 +43,9 @@ import android.window.DisplayWindowPolicyController; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.DumpUtils; import com.android.server.SystemService; +import com.android.server.companion.virtual.VirtualDeviceImpl.PendingTrampoline; +import com.android.server.wm.ActivityInterceptorCallback; +import com.android.server.wm.ActivityTaskManagerInternal; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -48,10 +57,12 @@ import java.util.concurrent.ConcurrentHashMap; public class VirtualDeviceManagerService extends SystemService { private static final boolean DEBUG = false; - private static final String LOG_TAG = "VirtualDeviceManagerService"; + private static final String TAG = "VirtualDeviceManagerService"; private final Object mVirtualDeviceManagerLock = new Object(); private final VirtualDeviceManagerImpl mImpl; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final PendingTrampolineMap mPendingTrampolines = new PendingTrampolineMap(mHandler); /** * Mapping from CDM association IDs to virtual devices. Only one virtual device is allowed for @@ -80,10 +91,39 @@ public class VirtualDeviceManagerService extends SystemService { mImpl = new VirtualDeviceManagerImpl(); } + private final ActivityInterceptorCallback mActivityInterceptorCallback = + new ActivityInterceptorCallback() { + + @Nullable + @Override + public ActivityInterceptResult intercept(ActivityInterceptorInfo info) { + if (info.callingPackage == null) { + return null; + } + PendingTrampoline pt = mPendingTrampolines.remove(info.callingPackage); + if (pt == null) { + return null; + } + pt.mResultReceiver.send(Activity.RESULT_OK, null); + ActivityOptions options = info.checkedOptions; + if (options == null) { + options = ActivityOptions.makeBasic(); + } + return new ActivityInterceptResult( + info.intent, options.setLaunchDisplayId(pt.mDisplayId)); + } + }; + @Override public void onStart() { publishBinderService(Context.VIRTUAL_DEVICE_SERVICE, mImpl); publishLocalService(VirtualDeviceManagerInternal.class, new LocalService()); + + ActivityTaskManagerInternal activityTaskManagerInternal = getLocalService( + ActivityTaskManagerInternal.class); + activityTaskManagerInternal.registerActivityStartInterceptor( + VIRTUAL_DEVICE_SERVICE_ORDERED_ID, + mActivityInterceptorCallback); } @GuardedBy("mVirtualDeviceManagerLock") @@ -128,7 +168,8 @@ public class VirtualDeviceManagerService extends SystemService { } } - class VirtualDeviceManagerImpl extends IVirtualDeviceManager.Stub { + class VirtualDeviceManagerImpl extends IVirtualDeviceManager.Stub implements + VirtualDeviceImpl.PendingTrampolineCallback { @Override // Binder call public IVirtualDevice createVirtualDevice( @@ -164,7 +205,8 @@ public class VirtualDeviceManagerService extends SystemService { mVirtualDevices.remove(associationId); } } - }, params); + }, + this, params); mVirtualDevices.put(associationInfo.getId(), virtualDevice); return virtualDevice; } @@ -185,7 +227,7 @@ public class VirtualDeviceManagerService extends SystemService { } } } else { - Slog.w(LOG_TAG, "No associations for user " + callingUserId); + Slog.w(TAG, "No associations for user " + callingUserId); } return null; } @@ -196,7 +238,7 @@ public class VirtualDeviceManagerService extends SystemService { try { return super.onTransact(code, data, reply, flags); } catch (Throwable e) { - Slog.e(LOG_TAG, "Error during IPC", e); + Slog.e(TAG, "Error during IPC", e); throw ExceptionUtils.propagate(e, RemoteException.class); } } @@ -205,7 +247,7 @@ public class VirtualDeviceManagerService extends SystemService { public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout, @Nullable String[] args) { - if (!DumpUtils.checkDumpAndUsageStatsPermission(getContext(), LOG_TAG, fout)) { + if (!DumpUtils.checkDumpAndUsageStatsPermission(getContext(), TAG, fout)) { return; } fout.println("Created virtual devices: "); @@ -215,10 +257,24 @@ public class VirtualDeviceManagerService extends SystemService { } } } + + @Override + public void startWaitingForPendingTrampoline(PendingTrampoline pendingTrampoline) { + PendingTrampoline existing = mPendingTrampolines.put( + pendingTrampoline.mPendingIntent.getCreatorPackage(), + pendingTrampoline); + if (existing != null) { + existing.mResultReceiver.send(Activity.RESULT_CANCELED, null); + } + } + + @Override + public void stopWaitingForPendingTrampoline(PendingTrampoline pendingTrampoline) { + mPendingTrampolines.remove(pendingTrampoline.mPendingIntent.getCreatorPackage()); + } } private final class LocalService extends VirtualDeviceManagerInternal { - @Override public boolean isValidVirtualDevice(IVirtualDevice virtualDevice) { synchronized (mVirtualDeviceManagerLock) { @@ -272,4 +328,42 @@ public class VirtualDeviceManagerService extends SystemService { return false; } } + + private static final class PendingTrampolineMap { + /** + * The maximum duration, in milliseconds, to wait for a trampoline activity launch after + * invoking a pending intent. + */ + private static final int TRAMPOLINE_WAIT_MS = 5000; + + private final ConcurrentHashMap mMap = new ConcurrentHashMap<>(); + private final Handler mHandler; + + PendingTrampolineMap(Handler handler) { + mHandler = handler; + } + + PendingTrampoline put( + @NonNull String packageName, @NonNull PendingTrampoline pendingTrampoline) { + PendingTrampoline existing = mMap.put(packageName, pendingTrampoline); + mHandler.removeCallbacksAndMessages(existing); + mHandler.postDelayed( + () -> { + final String creatorPackage = + pendingTrampoline.mPendingIntent.getCreatorPackage(); + if (creatorPackage != null) { + remove(creatorPackage); + } + }, + pendingTrampoline, + TRAMPOLINE_WAIT_MS); + return existing; + } + + PendingTrampoline remove(@NonNull String packageName) { + PendingTrampoline pendingTrampoline = mMap.remove(packageName); + mHandler.removeCallbacksAndMessages(pendingTrampoline); + return pendingTrampoline; + } + } } diff --git a/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java b/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java index 1bb9ca770c45f..48dd2f4238c62 100644 --- a/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java +++ b/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java @@ -55,6 +55,7 @@ public abstract class ActivityInterceptorCallback { FIRST_ORDERED_ID, COMMUNAL_MODE_ORDERED_ID, PERMISSION_POLICY_ORDERED_ID, + VIRTUAL_DEVICE_SERVICE_ORDERED_ID, LAST_ORDERED_ID // Update this when adding new ids }) @Retention(RetentionPolicy.SOURCE) @@ -75,11 +76,17 @@ public abstract class ActivityInterceptorCallback { */ public static final int PERMISSION_POLICY_ORDERED_ID = 2; + /** + * The identifier for {@link com.android.server.companion.virtual.VirtualDeviceManagerService} + * interceptor. + */ + public static final int VIRTUAL_DEVICE_SERVICE_ORDERED_ID = 3; + /** * The final id, used by the framework to determine the valid range of ids. Update this when * adding new ids. */ - static final int LAST_ORDERED_ID = PERMISSION_POLICY_ORDERED_ID; + static final int LAST_ORDERED_ID = VIRTUAL_DEVICE_SERVICE_ORDERED_ID; /** * Data class for storing the various arguments needed for activity interception. diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index d79a833d70460..0a0f7d7a0ae0b 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -71,6 +71,8 @@ public class VirtualDeviceManagerServiceTest { private InputController.NativeWrapper mNativeWrapperMock; @Mock private DisplayManagerInternal mDisplayManagerInternalMock; + @Mock + private VirtualDeviceImpl.PendingTrampolineCallback mPendingTrampolineCallback; @Before public void setUp() { @@ -85,7 +87,8 @@ public class VirtualDeviceManagerServiceTest { mInputController = new InputController(new Object(), mNativeWrapperMock); mDeviceImpl = new VirtualDeviceImpl(mContext, /* association info */ null, new Binder(), /* uid */ 0, mInputController, - (int associationId) -> {}, new VirtualDeviceParams.Builder().build()); + (int associationId) -> {}, mPendingTrampolineCallback, + new VirtualDeviceParams.Builder().build()); } @Test