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()); }