blob: b9d2be280efbcf522aaf13311d901c07fa8b0e0c [file] [log] [blame]
/*
* 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 com.android.wm.shell.desktopmode;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_NONE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.WindowConfiguration;
import android.content.Context;
import android.database.ContentObserver;
import android.graphics.Region;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.ArraySet;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.DisplayAreaInfo;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;
import androidx.annotation.BinderThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.annotations.ExternalThread;
import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
* Handles windowing changes when desktop mode system setting changes
*/
public class DesktopModeController implements RemoteCallable<DesktopModeController>,
Transitions.TransitionHandler {
private final Context mContext;
private final ShellController mShellController;
private final ShellTaskOrganizer mShellTaskOrganizer;
private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
private final Transitions mTransitions;
private final DesktopModeTaskRepository mDesktopModeTaskRepository;
private final ShellExecutor mMainExecutor;
private final DesktopModeImpl mDesktopModeImpl = new DesktopModeImpl();
private final SettingsObserver mSettingsObserver;
public DesktopModeController(Context context,
ShellInit shellInit,
ShellController shellController,
ShellTaskOrganizer shellTaskOrganizer,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
Transitions transitions,
DesktopModeTaskRepository desktopModeTaskRepository,
@ShellMainThread Handler mainHandler,
@ShellMainThread ShellExecutor mainExecutor) {
mContext = context;
mShellController = shellController;
mShellTaskOrganizer = shellTaskOrganizer;
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
mTransitions = transitions;
mDesktopModeTaskRepository = desktopModeTaskRepository;
mMainExecutor = mainExecutor;
mSettingsObserver = new SettingsObserver(mContext, mainHandler);
if (DesktopModeStatus.isProto1Enabled()) {
shellInit.addInitCallback(this::onInit, this);
}
}
private void onInit() {
ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopModeController");
mShellController.addExternalInterface(KEY_EXTRA_SHELL_DESKTOP_MODE,
this::createExternalInterface, this);
mSettingsObserver.observe();
if (DesktopModeStatus.isActive(mContext)) {
updateDesktopModeActive(true);
}
mTransitions.addHandler(this);
}
@Override
public Context getContext() {
return mContext;
}
@Override
public ShellExecutor getRemoteCallExecutor() {
return mMainExecutor;
}
/**
* Get connection interface between sysui and shell
*/
public DesktopMode asDesktopMode() {
return mDesktopModeImpl;
}
/**
* Creates a new instance of the external interface to pass to another process.
*/
private ExternalInterfaceBinder createExternalInterface() {
return new IDesktopModeImpl(this);
}
/**
* Adds a listener to find out about changes in the visibility of freeform tasks.
*
* @param listener the listener to add.
* @param callbackExecutor the executor to call the listener on.
*/
public void addVisibleTasksListener(DesktopModeTaskRepository.VisibleTasksListener listener,
Executor callbackExecutor) {
mDesktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor);
}
/**
* Adds a listener to track changes to corners of desktop mode tasks.
* @param listener the listener to add.
* @param callbackExecutor the executor to call the listener on.
*/
public void addTaskCornerListener(Consumer<Region> listener,
Executor callbackExecutor) {
mDesktopModeTaskRepository.setTaskCornerListener(listener, callbackExecutor);
}
@VisibleForTesting
void updateDesktopModeActive(boolean active) {
ProtoLog.d(WM_SHELL_DESKTOP_MODE, "updateDesktopModeActive: active=%s", active);
int displayId = mContext.getDisplayId();
ArrayList<RunningTaskInfo> runningTasks = mShellTaskOrganizer.getRunningTasks(displayId);
WindowContainerTransaction wct = new WindowContainerTransaction();
// Reset freeform windowing mode that is set per task level so tasks inherit it
clearFreeformForStandardTasks(runningTasks, wct);
if (active) {
moveHomeBehindVisibleTasks(runningTasks, wct);
setDisplayAreaWindowingMode(displayId, WINDOWING_MODE_FREEFORM, wct);
} else {
clearBoundsForStandardTasks(runningTasks, wct);
setDisplayAreaWindowingMode(displayId, WINDOWING_MODE_FULLSCREEN, wct);
}
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
mTransitions.startTransition(TRANSIT_CHANGE, wct, null);
} else {
mRootTaskDisplayAreaOrganizer.applyTransaction(wct);
}
}
private WindowContainerTransaction clearBoundsForStandardTasks(
ArrayList<RunningTaskInfo> runningTasks, WindowContainerTransaction wct) {
ProtoLog.v(WM_SHELL_DESKTOP_MODE, "prepareClearBoundsForTasks");
for (RunningTaskInfo taskInfo : runningTasks) {
if (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) {
ProtoLog.v(WM_SHELL_DESKTOP_MODE, "clearing bounds for token=%s taskInfo=%s",
taskInfo.token, taskInfo);
wct.setBounds(taskInfo.token, null);
}
}
return wct;
}
private void clearFreeformForStandardTasks(ArrayList<RunningTaskInfo> runningTasks,
WindowContainerTransaction wct) {
ProtoLog.v(WM_SHELL_DESKTOP_MODE, "prepareClearFreeformForTasks");
for (RunningTaskInfo taskInfo : runningTasks) {
if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
&& taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD) {
ProtoLog.v(WM_SHELL_DESKTOP_MODE,
"clearing windowing mode for token=%s taskInfo=%s", taskInfo.token,
taskInfo);
wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
}
}
}
private void moveHomeBehindVisibleTasks(ArrayList<RunningTaskInfo> runningTasks,
WindowContainerTransaction wct) {
ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks");
RunningTaskInfo homeTask = null;
ArrayList<RunningTaskInfo> visibleTasks = new ArrayList<>();
for (RunningTaskInfo taskInfo : runningTasks) {
if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) {
homeTask = taskInfo;
} else if (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
&& taskInfo.isVisible()) {
visibleTasks.add(taskInfo);
}
}
if (homeTask == null) {
ProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks: home task not found");
} else {
ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveHomeBehindVisibleTasks: visible tasks %d",
visibleTasks.size());
wct.reorder(homeTask.getToken(), true /* onTop */);
for (RunningTaskInfo task : visibleTasks) {
wct.reorder(task.getToken(), true /* onTop */);
}
}
}
private void setDisplayAreaWindowingMode(int displayId,
@WindowConfiguration.WindowingMode int windowingMode, WindowContainerTransaction wct) {
DisplayAreaInfo displayAreaInfo = mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(
displayId);
if (displayAreaInfo == null) {
ProtoLog.e(WM_SHELL_DESKTOP_MODE,
"unable to update windowing mode for display %d display not found", displayId);
return;
}
ProtoLog.v(WM_SHELL_DESKTOP_MODE,
"setWindowingMode: displayId=%d current wmMode=%d new wmMode=%d", displayId,
displayAreaInfo.configuration.windowConfiguration.getWindowingMode(),
windowingMode);
wct.setWindowingMode(displayAreaInfo.token, windowingMode);
}
/**
* Show apps on desktop
*/
void showDesktopApps(int displayId) {
// Bring apps to front, ignoring their visibility status to always ensure they are on top.
WindowContainerTransaction wct = new WindowContainerTransaction();
bringDesktopAppsToFront(displayId, wct);
if (!wct.isEmpty()) {
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
// TODO(b/268662477): add animation for the transition
mTransitions.startTransition(TRANSIT_NONE, wct, null /* handler */);
} else {
mShellTaskOrganizer.applyTransaction(wct);
}
}
}
/** Get number of tasks that are marked as visible */
int getVisibleTaskCount(int displayId) {
return mDesktopModeTaskRepository.getVisibleTaskCount(displayId);
}
private void bringDesktopAppsToFront(int displayId, WindowContainerTransaction wct) {
final ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks(displayId);
ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size());
final List<RunningTaskInfo> taskInfos = new ArrayList<>();
for (Integer taskId : activeTasks) {
RunningTaskInfo taskInfo = mShellTaskOrganizer.getRunningTaskInfo(taskId);
if (taskInfo != null) {
taskInfos.add(taskInfo);
}
}
if (taskInfos.isEmpty()) {
return;
}
moveHomeTaskToFront(wct);
ProtoLog.d(WM_SHELL_DESKTOP_MODE,
"bringDesktopAppsToFront: reordering all active tasks to the front");
final List<Integer> allTasksInZOrder =
mDesktopModeTaskRepository.getFreeformTasksInZOrder();
// Sort by z-order, bottom to top, so that the top-most task is reordered to the top last
// in the WCT.
taskInfos.sort(Comparator.comparingInt(task -> -allTasksInZOrder.indexOf(task.taskId)));
for (RunningTaskInfo task : taskInfos) {
wct.reorder(task.token, true);
}
}
private void moveHomeTaskToFront(WindowContainerTransaction wct) {
for (RunningTaskInfo task : mShellTaskOrganizer.getRunningTasks(mContext.getDisplayId())) {
if (task.getActivityType() == ACTIVITY_TYPE_HOME) {
wct.reorder(task.token, true /* onTop */);
return;
}
}
}
/**
* Update corner rects stored for a specific task
* @param taskId task to update
* @param taskCorners task's new corner handles
*/
public void onTaskCornersChanged(int taskId, Region taskCorners) {
mDesktopModeTaskRepository.updateTaskCorners(taskId, taskCorners);
}
/**
* Remove corners saved for a task. Likely used due to task closure.
* @param taskId task to remove
*/
public void removeCornersForTask(int taskId) {
mDesktopModeTaskRepository.removeTaskCorners(taskId);
}
/**
* Moves a specifc task to the front.
* @param taskInfo the task to show in front.
*/
public void moveTaskToFront(RunningTaskInfo taskInfo) {
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.reorder(taskInfo.token, true /* onTop */);
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
mTransitions.startTransition(TRANSIT_TO_FRONT, wct, null);
} else {
mShellTaskOrganizer.applyTransaction(wct);
}
}
/**
* Turn desktop mode on or off
* @param active the desired state for desktop mode setting
*/
public void setDesktopModeActive(boolean active) {
int value = active ? 1 : 0;
Settings.System.putInt(mContext.getContentResolver(), Settings.System.DESKTOP_MODE, value);
}
/**
* Returns the windowing mode of the display area with the specified displayId.
* @param displayId
* @return
*/
public int getDisplayAreaWindowingMode(int displayId) {
return mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)
.configuration.windowConfiguration.getWindowingMode();
}
@Override
public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
// This handler should never be the sole handler, so should not animate anything.
return false;
}
@Nullable
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@NonNull TransitionRequestInfo request) {
RunningTaskInfo triggerTask = request.getTriggerTask();
// Only do anything if we are in desktop mode and opening/moving-to-front a task/app in
// freeform
if (!DesktopModeStatus.isActive(mContext)) {
ProtoLog.d(WM_SHELL_DESKTOP_MODE,
"skip shell transition request: desktop mode not active");
return null;
}
if (request.getType() != TRANSIT_OPEN && request.getType() != TRANSIT_TO_FRONT) {
ProtoLog.d(WM_SHELL_DESKTOP_MODE,
"skip shell transition request: unsupported type %s",
WindowManager.transitTypeToString(request.getType()));
return null;
}
if (triggerTask == null || triggerTask.getWindowingMode() != WINDOWING_MODE_FREEFORM) {
ProtoLog.d(WM_SHELL_DESKTOP_MODE, "skip shell transition request: not freeform task");
return null;
}
ProtoLog.d(WM_SHELL_DESKTOP_MODE, "handle shell transition request: %s", request);
WindowContainerTransaction wct = new WindowContainerTransaction();
bringDesktopAppsToFront(triggerTask.displayId, wct);
wct.reorder(triggerTask.token, true /* onTop */);
return wct;
}
/**
* A {@link ContentObserver} for listening to changes to {@link Settings.System#DESKTOP_MODE}
*/
private final class SettingsObserver extends ContentObserver {
private final Uri mDesktopModeSetting = Settings.System.getUriFor(
Settings.System.DESKTOP_MODE);
private final Context mContext;
SettingsObserver(Context context, Handler handler) {
super(handler);
mContext = context;
}
public void observe() {
// TODO(b/242867463): listen for setting change for all users
mContext.getContentResolver().registerContentObserver(mDesktopModeSetting,
false /* notifyForDescendants */, this /* observer */, UserHandle.USER_CURRENT);
}
@Override
public void onChange(boolean selfChange, @Nullable Uri uri) {
if (mDesktopModeSetting.equals(uri)) {
ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Received update for desktop mode setting");
desktopModeSettingChanged();
}
}
private void desktopModeSettingChanged() {
boolean enabled = DesktopModeStatus.isActive(mContext);
updateDesktopModeActive(enabled);
}
}
/**
* The interface for calls from outside the shell, within the host process.
*/
@ExternalThread
private final class DesktopModeImpl implements DesktopMode {
@Override
public void addVisibleTasksListener(
DesktopModeTaskRepository.VisibleTasksListener listener,
Executor callbackExecutor) {
mMainExecutor.execute(() -> {
DesktopModeController.this.addVisibleTasksListener(listener, callbackExecutor);
});
}
@Override
public void addDesktopGestureExclusionRegionListener(Consumer<Region> listener,
Executor callbackExecutor) {
mMainExecutor.execute(() -> {
DesktopModeController.this.addTaskCornerListener(listener, callbackExecutor);
});
}
}
/**
* The interface for calls from outside the host process.
*/
@BinderThread
private static class IDesktopModeImpl extends IDesktopMode.Stub
implements ExternalInterfaceBinder {
private DesktopModeController mController;
IDesktopModeImpl(DesktopModeController controller) {
mController = controller;
}
/**
* Invalidates this instance, preventing future calls from updating the controller.
*/
@Override
public void invalidate() {
mController = null;
}
@Override
public void showDesktopApps(int displayId) {
executeRemoteCallWithTaskPermission(mController, "showDesktopApps",
controller -> controller.showDesktopApps(displayId));
}
@Override
public int getVisibleTaskCount(int displayId) throws RemoteException {
int[] result = new int[1];
executeRemoteCallWithTaskPermission(mController, "getVisibleTaskCount",
controller -> result[0] = controller.getVisibleTaskCount(displayId),
true /* blocking */
);
return result[0];
}
}
}