blob: 8c728f1eeffde800adfa91f895d290790c1fb649 [file] [log] [blame]
/*
* Copyright (C) 2021 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 com.android.server.companion.virtual;
import static android.content.pm.ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES;
import static android.view.Display.DEFAULT_DISPLAY;
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.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.WindowConfiguration;
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.Intent;
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 com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.BlockedAppStreamingActivity;
import java.util.Set;
/**
* A controller to control the policies of the windows that can be displayed on the virtual display.
*/
public class GenericWindowPolicyController extends DisplayWindowPolicyController {
private static final String TAG = "GenericWindowPolicyController";
/** Interface to listen running applications change on virtual display. */
public interface RunningAppsChangedListener {
/**
* Notifies the running applications change.
*/
void onRunningAppsChanged(ArraySet<Integer> runningUids);
}
/**
* For communicating when activities are blocked from running on the display by this policy
* controller.
*/
public interface ActivityBlockedCallback {
/** Called when an activity is blocked.*/
void onActivityBlocked(int displayId, ActivityInfo activityInfo);
}
private static final ComponentName BLOCKED_APP_STREAMING_COMPONENT =
new ComponentName("android", BlockedAppStreamingActivity.class.getName());
/**
* For communicating when a secure window shows on the virtual display.
*/
public interface SecureWindowCallback {
/** Called when a secure window shows on the virtual display. */
void onSecureWindowShown(int displayId, int uid);
}
/**
* For communicating when activities are blocked from entering PIP on the display by this
* policy controller.
*/
public interface PipBlockedCallback {
/** Called when an activity is blocked from entering PIP. */
void onEnteringPipBlocked(int uid);
}
/** Interface to listen for interception of intents. */
public interface IntentListenerCallback {
/** Returns true when an intent should be intercepted */
boolean shouldInterceptIntent(Intent intent);
}
/**
* If required, allow the secure activity to display on remote device since
* {@link android.os.Build.VERSION_CODES#TIRAMISU}.
*/
@ChangeId
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
public static final long ALLOW_SECURE_ACTIVITY_DISPLAY_ON_REMOTE_DEVICE = 201712607L;
@NonNull
private final ArraySet<UserHandle> mAllowedUsers;
@GuardedBy("mGenericWindowPolicyControllerLock")
private boolean mActivityLaunchAllowedByDefault;
@NonNull
@GuardedBy("mGenericWindowPolicyControllerLock")
private final Set<ComponentName> mActivityPolicyExemptions;
private final boolean mCrossTaskNavigationAllowedByDefault;
@NonNull
private final ArraySet<ComponentName> mCrossTaskNavigationExemptions;
@Nullable
private final ComponentName mPermissionDialogComponent;
private final Object mGenericWindowPolicyControllerLock = new Object();
@Nullable private final ActivityBlockedCallback mActivityBlockedCallback;
private int mDisplayId = Display.INVALID_DISPLAY;
private boolean mIsMirrorDisplay = false;
@NonNull
@GuardedBy("mGenericWindowPolicyControllerLock")
private final ArraySet<Integer> mRunningUids = new ArraySet<>();
@Nullable private final ActivityListener mActivityListener;
@Nullable private final PipBlockedCallback mPipBlockedCallback;
@Nullable private final IntentListenerCallback mIntentListenerCallback;
private final Handler mHandler = new Handler(Looper.getMainLooper());
@NonNull
@GuardedBy("mGenericWindowPolicyControllerLock")
private final ArraySet<RunningAppsChangedListener> mRunningAppsChangedListeners =
new ArraySet<>();
@Nullable private final SecureWindowCallback mSecureWindowCallback;
@NonNull private final Set<String> mDisplayCategories;
@GuardedBy("mGenericWindowPolicyControllerLock")
private boolean mShowTasksInHostDeviceRecents;
@Nullable private final ComponentName mCustomHomeComponent;
/**
* 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 activityLaunchAllowedByDefault Whether activities are default allowed to be launched
* or blocked.
* @param activityPolicyExemptions The set of activities explicitly exempt from the default
* activity policy.
* @param crossTaskNavigationAllowedByDefault Whether cross task navigations are allowed by
* default or not.
* @param crossTaskNavigationExemptions The set of components explicitly exempt from the default
* navigation policy.
* @param activityListener Activity listener to listen for activity changes.
* @param activityBlockedCallback Callback that is called when an activity is blocked from
* launching.
* @param secureWindowCallback Callback that is called when a secure window shows on the
* virtual display.
* @param intentListenerCallback Callback that is called to intercept intents when matching
* passed in filters.
* @param showTasksInHostDeviceRecents whether to show activities in recents on the host device.
* @param customHomeComponent The component acting as a home activity on the virtual display. If
* {@code null}, then the system-default secondary home activity will be used. This is only
* applicable to displays that support home activities, i.e. they're created with the relevant
* virtual display flag.
*/
public GenericWindowPolicyController(
int windowFlags,
int systemWindowFlags,
@NonNull ArraySet<UserHandle> allowedUsers,
boolean activityLaunchAllowedByDefault,
@NonNull Set<ComponentName> activityPolicyExemptions,
boolean crossTaskNavigationAllowedByDefault,
@NonNull Set<ComponentName> crossTaskNavigationExemptions,
@Nullable ComponentName permissionDialogComponent,
@Nullable ActivityListener activityListener,
@Nullable PipBlockedCallback pipBlockedCallback,
@Nullable ActivityBlockedCallback activityBlockedCallback,
@Nullable SecureWindowCallback secureWindowCallback,
@Nullable IntentListenerCallback intentListenerCallback,
@NonNull Set<String> displayCategories,
boolean showTasksInHostDeviceRecents,
@Nullable ComponentName customHomeComponent) {
super();
mAllowedUsers = allowedUsers;
mActivityLaunchAllowedByDefault = activityLaunchAllowedByDefault;
mActivityPolicyExemptions = activityPolicyExemptions;
mCrossTaskNavigationAllowedByDefault = crossTaskNavigationAllowedByDefault;
mCrossTaskNavigationExemptions = new ArraySet<>(crossTaskNavigationExemptions);
mPermissionDialogComponent = permissionDialogComponent;
mActivityBlockedCallback = activityBlockedCallback;
setInterestedWindowFlags(windowFlags, systemWindowFlags);
mActivityListener = activityListener;
mPipBlockedCallback = pipBlockedCallback;
mSecureWindowCallback = secureWindowCallback;
mIntentListenerCallback = intentListenerCallback;
mDisplayCategories = displayCategories;
mShowTasksInHostDeviceRecents = showTasksInHostDeviceRecents;
mCustomHomeComponent = customHomeComponent;
}
/**
* Expected to be called once this object is associated with a newly created display.
*/
void setDisplayId(int displayId, boolean isMirrorDisplay) {
mDisplayId = displayId;
mIsMirrorDisplay = isMirrorDisplay;
}
/**
* Set whether to show activities in recents on the host device.
*/
public void setShowInHostDeviceRecents(boolean showInHostDeviceRecents) {
synchronized (mGenericWindowPolicyControllerLock) {
mShowTasksInHostDeviceRecents = showInHostDeviceRecents;
}
}
void setActivityLaunchDefaultAllowed(boolean activityLaunchDefaultAllowed) {
synchronized (mGenericWindowPolicyControllerLock) {
if (mActivityLaunchAllowedByDefault != activityLaunchDefaultAllowed) {
mActivityPolicyExemptions.clear();
}
mActivityLaunchAllowedByDefault = activityLaunchDefaultAllowed;
}
}
void addActivityPolicyExemption(@NonNull ComponentName componentName) {
synchronized (mGenericWindowPolicyControllerLock) {
mActivityPolicyExemptions.add(componentName);
}
}
void removeActivityPolicyExemption(@NonNull ComponentName componentName) {
synchronized (mGenericWindowPolicyControllerLock) {
mActivityPolicyExemptions.remove(componentName);
}
}
/** Register a listener for running applications changes. */
public void registerRunningAppsChangedListener(@NonNull RunningAppsChangedListener listener) {
synchronized (mGenericWindowPolicyControllerLock) {
mRunningAppsChangedListeners.add(listener);
}
}
/** Unregister a listener for running applications changes. */
public void unregisterRunningAppsChangedListener(@NonNull RunningAppsChangedListener listener) {
synchronized (mGenericWindowPolicyControllerLock) {
mRunningAppsChangedListeners.remove(listener);
}
}
@Override
public boolean canActivityBeLaunched(@NonNull ActivityInfo activityInfo,
@Nullable Intent intent, @WindowConfiguration.WindowingMode int windowingMode,
int launchingFromDisplayId, boolean isNewTask) {
if (!canContainActivity(activityInfo, windowingMode, launchingFromDisplayId, isNewTask)) {
notifyActivityBlocked(activityInfo);
return false;
}
if (mIntentListenerCallback != null && intent != null
&& mIntentListenerCallback.shouldInterceptIntent(intent)) {
Slog.d(TAG, "Virtual device intercepting intent");
return false;
}
return true;
}
@Override
public boolean canContainActivity(@NonNull ActivityInfo activityInfo,
@WindowConfiguration.WindowingMode int windowingMode, int launchingFromDisplayId,
boolean isNewTask) {
// Mirror displays cannot contain activities.
if (mIsMirrorDisplay) {
Slog.d(TAG, "Mirror virtual displays cannot contain activities.");
return false;
}
if (!isWindowingModeSupported(windowingMode)) {
Slog.d(TAG, "Virtual device doesn't support windowing mode " + windowingMode);
return false;
}
if ((activityInfo.flags & FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES) == 0) {
Slog.d(TAG, "Virtual device requires android:canDisplayOnRemoteDevices=true");
return false;
}
final UserHandle activityUser =
UserHandle.getUserHandleForUid(activityInfo.applicationInfo.uid);
if (!mAllowedUsers.contains(activityUser)) {
Slog.d(TAG, "Virtual device launch disallowed from user " + activityUser);
return false;
}
final ComponentName activityComponent = activityInfo.getComponentName();
if (BLOCKED_APP_STREAMING_COMPONENT.equals(activityComponent)) {
// The error dialog alerting users that streaming is blocked is always allowed.
return true;
}
if (!activityMatchesDisplayCategory(activityInfo)) {
Slog.d(TAG, "The activity's required display category '"
+ activityInfo.requiredDisplayCategory
+ "' not found on virtual display with the following categories: "
+ mDisplayCategories);
return false;
}
synchronized (mGenericWindowPolicyControllerLock) {
if (!isAllowedByPolicy(mActivityLaunchAllowedByDefault, mActivityPolicyExemptions,
activityComponent)) {
Slog.d(TAG, "Virtual device launch disallowed by policy: "
+ activityComponent);
return false;
}
}
if (isNewTask && launchingFromDisplayId != DEFAULT_DISPLAY
&& !isAllowedByPolicy(mCrossTaskNavigationAllowedByDefault,
mCrossTaskNavigationExemptions, activityComponent)) {
Slog.d(TAG, "Virtual device cross task navigation disallowed by policy: "
+ activityComponent);
return false;
}
// mPermissionDialogComponent being null means we don't want to block permission Dialogs
// based on FLAG_STREAM_PERMISSIONS
if (mPermissionDialogComponent != null
&& mPermissionDialogComponent.equals(activityComponent)) {
return false;
}
return true;
}
@Override
@SuppressWarnings("AndroidFrameworkRequiresPermission")
public boolean keepActivityOnWindowFlagsChanged(ActivityInfo activityInfo, int windowFlags,
int systemWindowFlags) {
// The callback is fired only when windowFlags are changed. To let VirtualDevice owner
// aware that the virtual display has a secure window on top.
if ((windowFlags & FLAG_SECURE) != 0 && mSecureWindowCallback != null) {
// Post callback on the main thread, so it doesn't block activity launching.
mHandler.post(() -> mSecureWindowCallback.onSecureWindowShown(mDisplayId,
activityInfo.applicationInfo.uid));
}
if (!CompatChanges.isChangeEnabled(ALLOW_SECURE_ACTIVITY_DISPLAY_ON_REMOTE_DEVICE,
activityInfo.packageName,
UserHandle.getUserHandleForUid(activityInfo.applicationInfo.uid))) {
// TODO(b/201712607): Add checks for the apps that use SurfaceView#setSecure.
if ((windowFlags & FLAG_SECURE) != 0
|| (systemWindowFlags & SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS) != 0) {
notifyActivityBlocked(activityInfo);
return false;
}
}
return true;
}
@Override
public void onTopActivityChanged(ComponentName topActivity, int uid, @UserIdInt int userId) {
// Don't send onTopActivityChanged() callback when topActivity is null because it's defined
// as @NonNull in ActivityListener interface. Sends onDisplayEmpty() callback instead when
// there is no activity running on virtual display.
if (mActivityListener != null && topActivity != null) {
// Post callback on the main thread so it doesn't block activity launching
mHandler.post(() ->
mActivityListener.onTopActivityChanged(mDisplayId, topActivity, userId));
}
}
@Override
public void onRunningAppsChanged(ArraySet<Integer> runningUids) {
synchronized (mGenericWindowPolicyControllerLock) {
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(mDisplayId));
}
if (!mRunningAppsChangedListeners.isEmpty()) {
final ArraySet<RunningAppsChangedListener> listeners =
new ArraySet<>(mRunningAppsChangedListeners);
mHandler.post(() -> {
for (RunningAppsChangedListener listener : listeners) {
listener.onRunningAppsChanged(runningUids);
}
});
}
}
}
@Override
public boolean canShowTasksInHostDeviceRecents() {
synchronized (mGenericWindowPolicyControllerLock) {
return mShowTasksInHostDeviceRecents;
}
}
@Override
public boolean isEnteringPipAllowed(int uid) {
if (super.isEnteringPipAllowed(uid)) {
return true;
}
if (mPipBlockedCallback != null) {
mHandler.post(() -> mPipBlockedCallback.onEnteringPipBlocked(uid));
}
return false;
}
@Override
public @Nullable ComponentName getCustomHomeComponent() {
return mCustomHomeComponent;
}
/**
* Returns true if an app with the given UID has an activity running on the virtual display for
* this controller.
*/
boolean containsUid(int uid) {
synchronized (mGenericWindowPolicyControllerLock) {
return mRunningUids.contains(uid);
}
}
private boolean activityMatchesDisplayCategory(ActivityInfo activityInfo) {
if (mDisplayCategories.isEmpty()) {
return activityInfo.requiredDisplayCategory == null;
}
return activityInfo.requiredDisplayCategory != null
&& mDisplayCategories.contains(activityInfo.requiredDisplayCategory);
}
private void notifyActivityBlocked(ActivityInfo activityInfo) {
// Don't trigger activity blocked callback for mirror displays, because we can't show
// any activity or presentation on it anyway.
if (!mIsMirrorDisplay && mActivityBlockedCallback != null) {
mActivityBlockedCallback.onActivityBlocked(mDisplayId, activityInfo);
}
}
private static boolean isAllowedByPolicy(boolean allowedByDefault,
Set<ComponentName> exemptions, ComponentName component) {
// Either allowed and the exemptions do not contain the component,
// or disallowed and the exemptions contain the component.
return allowedByDefault != exemptions.contains(component);
}
@VisibleForTesting
int getRunningAppsChangedListenersSizeForTesting() {
synchronized (mGenericWindowPolicyControllerLock) {
return mRunningAppsChangedListeners.size();
}
}
}