From f03d832c434cc327f7dbc317324c26e4cd1b1b68 Mon Sep 17 00:00:00 2001 From: Maurice Lam Date: Tue, 30 Nov 2021 15:05:03 -0800 Subject: [PATCH] Add VirtualDevice.launchPendingIntent Add launchPendingIntent method on VirtualDevice, which allows specifying a display ID to launch the pending intent on. For activity intents, this is equivalent to adding the ActivityOptions with setLaunchDisplayId on the pending intent. For service and broadcast pending intents, VirtualDeviceManager will try to catch any trampolines activities launched by the pending intent creator and start those on the virtual display instead. Bug: 194949534 Test: Manual, CTS to be added in ag/15781910 Change-Id: Ia5d6c92167f0ee6c9f246fd4335daa824aceaef9 --- .../companion/virtual/IVirtualDevice.aidl | 9 ++ .../virtual/VirtualDeviceManager.java | 67 +++++++++++ .../companion/virtual/VirtualDeviceImpl.java | 112 +++++++++++++++++- .../virtual/VirtualDeviceManagerService.java | 108 +++++++++++++++-- .../wm/ActivityInterceptorCallback.java | 9 +- .../VirtualDeviceManagerServiceTest.java | 5 +- 6 files changed, 298 insertions(+), 12 deletions(-) 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