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:
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user