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 <app name> 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
This commit is contained in:
Maurice Lam
2022-01-12 16:30:31 -08:00
parent 30a0a6dec1
commit 7f512ebc8a
7 changed files with 251 additions and 17 deletions

View File

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

View File

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

View File

@@ -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<ActivityListener, ActivityListenerDelegate> 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));
}
}
}

View File

@@ -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<Integer> 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<UserHandle> allowedUsers,
@Nullable Set<ComponentName> allowedActivities,
@Nullable Set<ComponentName> blockedActivities) {
@Nullable Set<ComponentName> 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<Integer> 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));
}
}
/**

View File

@@ -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;
}

View File

@@ -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;
}

View File

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