From 7f512ebc8a035f5683c71b3a151fa9259bf5d0e9 Mon Sep 17 00:00:00 2001 From: Maurice Lam Date: Wed, 12 Jan 2022 16:30:31 -0800 Subject: [PATCH] Add ActivityListener to VirtualDeviceManager The API allows virtual device owners to listen to when the top activity of a virtual device's display changes, and when a display becomes empty. The former is used to display a notification to the user that can say "Syncing to your Chromebook". The latter is used so that the streamed window on ChromeOS side can be closed when the display becomes empty. Bug: 194949534 Test: To be added in ag/15781910 Change-Id: I5172e55a3a79b5fadc1719e99160b7675612e97b --- .../IVirtualDeviceActivityListener.aidl | 43 ++++++ .../virtual/IVirtualDeviceManager.aidl | 3 +- .../virtual/VirtualDeviceManager.java | 138 +++++++++++++++++- .../GenericWindowPolicyController.java | 30 +++- .../companion/virtual/VirtualDeviceImpl.java | 43 +++++- .../virtual/VirtualDeviceManagerService.java | 6 +- .../VirtualDeviceManagerServiceTest.java | 5 +- 7 files changed, 251 insertions(+), 17 deletions(-) create mode 100644 core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl diff --git a/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl b/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl new file mode 100644 index 0000000000000..53af4c5da761d --- /dev/null +++ b/core/java/android/companion/virtual/IVirtualDeviceActivityListener.aidl @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 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.content.ComponentName; + +/** + * Interface to listen for activity changes in a virtual device. + * + * @hide + */ +interface IVirtualDeviceActivityListener { + + /** + * Called when the top activity is changed. + * + * @param displayId The display ID on which the activity change happened. + * @param topActivity The component name of the top activity. + */ + void onTopActivityChanged(int displayId, in 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); +} diff --git a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl index d80bee668f182..b7f826a940a84 100644 --- a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl +++ b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl @@ -17,6 +17,7 @@ package android.companion.virtual; import android.companion.virtual.IVirtualDevice; +import android.companion.virtual.IVirtualDeviceActivityListener; import android.companion.virtual.VirtualDeviceParams; /** @@ -39,5 +40,5 @@ interface IVirtualDeviceManager { */ IVirtualDevice createVirtualDevice( in IBinder token, String packageName, int associationId, - in VirtualDeviceParams params); + in VirtualDeviceParams params, in IVirtualDeviceActivityListener activityListener); } diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index c72346837e297..64f16ac3e1b3a 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -25,6 +25,7 @@ import android.annotation.SystemService; import android.app.Activity; import android.app.PendingIntent; import android.companion.AssociationInfo; +import android.content.ComponentName; import android.content.Context; import android.graphics.Point; import android.hardware.display.DisplayManager; @@ -40,6 +41,7 @@ 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.util.concurrent.Executor; @@ -87,9 +89,7 @@ public final class VirtualDeviceManager { int associationId, @NonNull VirtualDeviceParams params) { try { - IVirtualDevice virtualDevice = mService.createVirtualDevice( - new Binder(), mContext.getPackageName(), associationId, params); - return new VirtualDevice(mContext, virtualDevice); + return new VirtualDevice(mService, mContext, associationId, params); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -106,10 +106,49 @@ public final class VirtualDeviceManager { private final Context mContext; private final IVirtualDevice mVirtualDevice; + private final ArrayMap mActivityListeners = + new ArrayMap<>(); + private final IVirtualDeviceActivityListener mActivityListenerBinder = + new IVirtualDeviceActivityListener.Stub() { - private VirtualDevice(Context context, IVirtualDevice virtualDevice) { + @Override + public void onTopActivityChanged(int displayId, ComponentName topActivity) { + final long token = Binder.clearCallingIdentity(); + try { + for (int i = 0; i < mActivityListeners.size(); i++) { + mActivityListeners.valueAt(i) + .onTopActivityChanged(displayId, topActivity); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public void onDisplayEmpty(int displayId) { + final long token = Binder.clearCallingIdentity(); + try { + for (int i = 0; i < mActivityListeners.size(); i++) { + mActivityListeners.valueAt(i).onDisplayEmpty(displayId); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + }; + + private VirtualDevice( + IVirtualDeviceManager service, + Context context, + int associationId, + VirtualDeviceParams params) throws RemoteException { mContext = context.getApplicationContext(); - mVirtualDevice = virtualDevice; + mVirtualDevice = service.createVirtualDevice( + new Binder(), + mContext.getPackageName(), + associationId, + params, + mActivityListenerBinder); } /** @@ -137,7 +176,7 @@ public final class VirtualDeviceManager { mVirtualDevice.launchPendingIntent( displayId, pendingIntent, - new ResultReceiver(new Handler(Looper.myLooper())) { + new ResultReceiver(new Handler(Looper.getMainLooper())) { @Override protected void onReceiveResult(int resultCode, Bundle resultData) { super.onReceiveResult(resultCode, resultData); @@ -323,6 +362,47 @@ public final class VirtualDeviceManager { throw e.rethrowFromSystemServer(); } } + + /** + * Adds an activity listener to listen for events such as top activity change or virtual + * display task stack became empty. + * + * @param listener The listener to add. + * @see #removeActivityListener(ActivityListener) + * @hide + */ + // TODO(b/194949534): Unhide this API + public void addActivityListener(@NonNull ActivityListener listener) { + addActivityListener(listener, mContext.getMainExecutor()); + } + + /** + * Adds an activity listener to listen for events such as top activity change or virtual + * display task stack became empty. + * + * @param listener The listener to add. + * @param executor The executor where the callback is executed on. + * @see #removeActivityListener(ActivityListener) + * @hide + */ + // TODO(b/194949534): Unhide this API + public void addActivityListener( + @NonNull ActivityListener listener, @NonNull Executor executor) { + mActivityListeners.put(listener, new ActivityListenerDelegate(listener, executor)); + } + + /** + * Removes an activity listener previously added with + * {@link #addActivityListener}. + * + * @param listener The listener to remove. + * @see #addActivityListener(ActivityListener, Executor) + * @hide + */ + // TODO(b/194949534): Unhide this API + public void removeActivityListener(@NonNull ActivityListener listener) { + mActivityListeners.remove(listener); + } } /** @@ -342,4 +422,50 @@ public final class VirtualDeviceManager { */ void onLaunchFailed(); } + + /** + * Listener for activity changes in this virtual device. + * + * @hide + */ + // TODO(b/194949534): Unhide this API + public interface ActivityListener { + + /** + * Called when the top activity is changed. + * + * @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)); + } + } } diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java index 0fd29675d4694..75acf81a4a3cc 100644 --- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java +++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java @@ -23,14 +23,18 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE import android.annotation.NonNull; import android.annotation.Nullable; import android.app.compat.CompatChanges; +import android.companion.virtual.VirtualDeviceManager.ActivityListener; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.ComponentName; import android.content.pm.ActivityInfo; import android.os.Build; +import android.os.Handler; +import android.os.Looper; import android.os.UserHandle; import android.util.ArraySet; import android.util.Slog; +import android.view.Display; import android.window.DisplayWindowPolicyController; import java.util.List; @@ -60,15 +64,29 @@ class GenericWindowPolicyController extends DisplayWindowPolicyController { @NonNull final ArraySet mRunningUids = new ArraySet<>(); + @Nullable private final ActivityListener mActivityListener; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + /** + * Creates a window policy controller that is generic to the different use cases of virtual + * device. + * + * @param windowFlags The window flags that this controller is interested in. + * @param systemWindowFlags The system window flags that this controller is interested in. + * @param allowedUsers The set of users that are allowed to stream in this display. + * @param activityListener Activity listener to listen for activity changes. The display ID + * is not populated in this callback and is always {@link Display#INVALID_DISPLAY}. + */ GenericWindowPolicyController(int windowFlags, int systemWindowFlags, @NonNull ArraySet allowedUsers, @Nullable Set allowedActivities, - @Nullable Set blockedActivities) { + @Nullable Set blockedActivities, + @NonNull ActivityListener activityListener) { mAllowedUsers = allowedUsers; mAllowedActivities = allowedActivities == null ? null : new ArraySet<>(allowedActivities); mBlockedActivities = blockedActivities == null ? null : new ArraySet<>(blockedActivities); setInterestedWindowFlags(windowFlags, systemWindowFlags); + mActivityListener = activityListener; } @Override @@ -92,13 +110,21 @@ class GenericWindowPolicyController extends DisplayWindowPolicyController { @Override public void onTopActivityChanged(ComponentName topActivity, int uid) { - + if (mActivityListener != null) { + // Post callback on the main thread so it doesn't block activity launching + mHandler.post(() -> + mActivityListener.onTopActivityChanged(Display.INVALID_DISPLAY, topActivity)); + } } @Override public void onRunningAppsChanged(ArraySet runningUids) { mRunningUids.clear(); mRunningUids.addAll(runningUids); + if (mActivityListener != null && mRunningUids.isEmpty()) { + // Post callback on the main thread so it doesn't block activity launching + mHandler.post(() -> mActivityListener.onDisplayEmpty(Display.INVALID_DISPLAY)); + } } /** 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 6ab8e754e2b31..5c8fb2ecec0fe 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -29,7 +29,10 @@ import android.app.PendingIntent; import android.app.admin.DevicePolicyManager; import android.companion.AssociationInfo; import android.companion.virtual.IVirtualDevice; +import android.companion.virtual.IVirtualDeviceActivityListener; +import android.companion.virtual.VirtualDeviceManager.ActivityListener; import android.companion.virtual.VirtualDeviceParams; +import android.content.ComponentName; import android.content.Context; import android.graphics.Point; import android.graphics.PointF; @@ -75,6 +78,30 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub private final OnDeviceCloseListener mListener; private final IBinder mAppToken; private final VirtualDeviceParams mParams; + private final IVirtualDeviceActivityListener mActivityListener; + + private ActivityListener createListenerAdapter(int displayId) { + return new ActivityListener() { + + @Override + public void onTopActivityChanged(int unusedDisplayId, ComponentName topActivity) { + try { + mActivityListener.onTopActivityChanged(displayId, topActivity); + } catch (RemoteException e) { + Slog.w(TAG, "Unable to call mActivityListener", e); + } + } + + @Override + public void onDisplayEmpty(int unusedDisplayId) { + try { + mActivityListener.onDisplayEmpty(displayId); + } catch (RemoteException e) { + Slog.w(TAG, "Unable to call mActivityListener", e); + } + } + }; + } /** * A mapping from the virtual display ID to its corresponding @@ -85,18 +112,22 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub VirtualDeviceImpl(Context context, AssociationInfo associationInfo, IBinder token, int ownerUid, OnDeviceCloseListener listener, - PendingTrampolineCallback pendingTrampolineCallback, VirtualDeviceParams params) { + PendingTrampolineCallback pendingTrampolineCallback, + IVirtualDeviceActivityListener activityListener, + VirtualDeviceParams params) { this(context, associationInfo, token, ownerUid, /* inputController= */ null, listener, - pendingTrampolineCallback, params); + pendingTrampolineCallback, activityListener, params); } @VisibleForTesting VirtualDeviceImpl(Context context, AssociationInfo associationInfo, IBinder token, int ownerUid, InputController inputController, OnDeviceCloseListener listener, - PendingTrampolineCallback pendingTrampolineCallback, VirtualDeviceParams params) { + PendingTrampolineCallback pendingTrampolineCallback, + IVirtualDeviceActivityListener activityListener, VirtualDeviceParams params) { mContext = context; mAssociationInfo = associationInfo; mPendingTrampolineCallback = pendingTrampolineCallback; + mActivityListener = activityListener; mOwnerUid = ownerUid; mAppToken = token; mParams = params; @@ -361,9 +392,11 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub displayId, false); final GenericWindowPolicyController dwpc = new GenericWindowPolicyController(FLAG_SECURE, - SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, getAllowedUserHandles(), + SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, + getAllowedUserHandles(), mParams.getAllowedActivities(), - mParams.getBlockedActivities()); + mParams.getBlockedActivities(), + createListenerAdapter(displayId)); mWindowPolicyControllers.put(displayId, dwpc); return dwpc; } 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 7e0c2fc37da6f..b507110d54730 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -27,6 +27,7 @@ import android.companion.AssociationInfo; import android.companion.CompanionDeviceManager; import android.companion.CompanionDeviceManager.OnAssociationsChangedListener; import android.companion.virtual.IVirtualDevice; +import android.companion.virtual.IVirtualDeviceActivityListener; import android.companion.virtual.IVirtualDeviceManager; import android.companion.virtual.VirtualDeviceParams; import android.content.Context; @@ -176,7 +177,8 @@ public class VirtualDeviceManagerService extends SystemService { IBinder token, String packageName, int associationId, - @NonNull VirtualDeviceParams params) { + @NonNull VirtualDeviceParams params, + @NonNull IVirtualDeviceActivityListener activityListener) { getContext().enforceCallingOrSelfPermission( android.Manifest.permission.CREATE_VIRTUAL_DEVICE, "createVirtualDevice"); @@ -206,7 +208,7 @@ public class VirtualDeviceManagerService extends SystemService { } } }, - this, params); + this, activityListener, params); mVirtualDevices.put(associationInfo.getId(), virtualDevice); return virtualDevice; } 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 fe7d34a55852b..72100e44b3ebc 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 @@ -29,6 +29,7 @@ import static org.testng.Assert.assertThrows; import android.Manifest; import android.app.admin.DevicePolicyManager; +import android.companion.virtual.IVirtualDeviceActivityListener; import android.companion.virtual.VirtualDeviceParams; import android.content.Context; import android.content.ContextWrapper; @@ -81,6 +82,8 @@ public class VirtualDeviceManagerServiceTest { private DevicePolicyManager mDevicePolicyManagerMock; @Mock private InputManagerInternal mInputManagerInternalMock; + @Mock + private IVirtualDeviceActivityListener mActivityListener; @Before public void setUp() { @@ -102,7 +105,7 @@ public class VirtualDeviceManagerServiceTest { mInputController = new InputController(new Object(), mNativeWrapperMock); mDeviceImpl = new VirtualDeviceImpl(mContext, /* association info */ null, new Binder(), /* uid */ 0, mInputController, - (int associationId) -> {}, mPendingTrampolineCallback, + (int associationId) -> {}, mPendingTrampolineCallback, mActivityListener, new VirtualDeviceParams.Builder().build()); }