blob: 86bf3ff1cea98fe94fc540c1258ab7bf48f7b46e [file] [log] [blame]
/*
* Copyright (C) 2020 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.legacysplitscreen;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
import android.app.ActivityManager.RunningTaskInfo;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.Log;
import android.util.SparseArray;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.window.TaskOrganizer;
import androidx.annotation.NonNull;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.SurfaceUtils;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.transition.Transitions;
import java.io.PrintWriter;
import java.util.ArrayList;
class LegacySplitScreenTaskListener implements ShellTaskOrganizer.TaskListener {
private static final String TAG = LegacySplitScreenTaskListener.class.getSimpleName();
private static final boolean DEBUG = LegacySplitScreenController.DEBUG;
private final ShellTaskOrganizer mTaskOrganizer;
private final SyncTransactionQueue mSyncQueue;
private final SparseArray<SurfaceControl> mLeashByTaskId = new SparseArray<>();
// TODO(shell-transitions): Remove when switched to shell-transitions.
private final SparseArray<Point> mPositionByTaskId = new SparseArray<>();
RunningTaskInfo mPrimary;
RunningTaskInfo mSecondary;
SurfaceControl mPrimarySurface;
SurfaceControl mSecondarySurface;
SurfaceControl mPrimaryDim;
SurfaceControl mSecondaryDim;
Rect mHomeBounds = new Rect();
final LegacySplitScreenController mSplitScreenController;
private boolean mSplitScreenSupported = false;
final SurfaceSession mSurfaceSession = new SurfaceSession();
private final LegacySplitScreenTransitions mSplitTransitions;
LegacySplitScreenTaskListener(LegacySplitScreenController splitScreenController,
ShellTaskOrganizer shellTaskOrganizer,
Transitions transitions,
SyncTransactionQueue syncQueue) {
mSplitScreenController = splitScreenController;
mTaskOrganizer = shellTaskOrganizer;
mSplitTransitions = new LegacySplitScreenTransitions(splitScreenController.mTransactionPool,
transitions, mSplitScreenController, this);
transitions.addHandler(mSplitTransitions);
mSyncQueue = syncQueue;
}
void init() {
synchronized (this) {
try {
mTaskOrganizer.createRootTask(
DEFAULT_DISPLAY, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, this);
mTaskOrganizer.createRootTask(
DEFAULT_DISPLAY, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, this);
} catch (Exception e) {
// teardown to prevent callbacks
mTaskOrganizer.removeListener(this);
throw e;
}
}
}
boolean isSplitScreenSupported() {
return mSplitScreenSupported;
}
SurfaceControl.Transaction getTransaction() {
return mSplitScreenController.mTransactionPool.acquire();
}
void releaseTransaction(SurfaceControl.Transaction t) {
mSplitScreenController.mTransactionPool.release(t);
}
TaskOrganizer getTaskOrganizer() {
return mTaskOrganizer;
}
LegacySplitScreenTransitions getSplitTransitions() {
return mSplitTransitions;
}
@Override
public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
synchronized (this) {
if (taskInfo.hasParentTask()) {
handleChildTaskAppeared(taskInfo, leash);
return;
}
final int winMode = taskInfo.getWindowingMode();
if (winMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) {
ProtoLog.v(WM_SHELL_TASK_ORG,
"%s onTaskAppeared Primary taskId=%d", TAG, taskInfo.taskId);
mPrimary = taskInfo;
mPrimarySurface = leash;
} else if (winMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) {
ProtoLog.v(WM_SHELL_TASK_ORG,
"%s onTaskAppeared Secondary taskId=%d", TAG, taskInfo.taskId);
mSecondary = taskInfo;
mSecondarySurface = leash;
} else {
ProtoLog.v(WM_SHELL_TASK_ORG, "%s onTaskAppeared unknown taskId=%d winMode=%d",
TAG, taskInfo.taskId, winMode);
}
if (!mSplitScreenSupported && mPrimarySurface != null && mSecondarySurface != null) {
mSplitScreenSupported = true;
mSplitScreenController.onSplitScreenSupported();
ProtoLog.v(WM_SHELL_TASK_ORG, "%s onTaskAppeared Supported", TAG);
// Initialize dim surfaces:
SurfaceControl.Transaction t = getTransaction();
mPrimaryDim = SurfaceUtils.makeDimLayer(
t, mPrimarySurface, "Primary Divider Dim", mSurfaceSession);
mSecondaryDim = SurfaceUtils.makeDimLayer(
t, mSecondarySurface, "Secondary Divider Dim", mSurfaceSession);
t.apply();
releaseTransaction(t);
}
}
}
@Override
public void onTaskVanished(RunningTaskInfo taskInfo) {
synchronized (this) {
mPositionByTaskId.remove(taskInfo.taskId);
if (taskInfo.hasParentTask()) {
mLeashByTaskId.remove(taskInfo.taskId);
return;
}
final boolean isPrimaryTask = mPrimary != null
&& taskInfo.token.equals(mPrimary.token);
final boolean isSecondaryTask = mSecondary != null
&& taskInfo.token.equals(mSecondary.token);
if (mSplitScreenSupported && (isPrimaryTask || isSecondaryTask)) {
mSplitScreenSupported = false;
SurfaceControl.Transaction t = getTransaction();
t.remove(mPrimaryDim);
t.remove(mSecondaryDim);
t.remove(mPrimarySurface);
t.remove(mSecondarySurface);
t.apply();
releaseTransaction(t);
mSplitScreenController.onTaskVanished();
}
}
}
@Override
public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
if (taskInfo.displayId != DEFAULT_DISPLAY) {
return;
}
synchronized (this) {
if (!taskInfo.supportsMultiWindow) {
if (mSplitScreenController.isDividerVisible()) {
// Dismiss the split screen if the task no longer supports multi window.
if (taskInfo.taskId == mPrimary.taskId
|| taskInfo.parentTaskId == mPrimary.taskId) {
// If the primary is focused, dismiss to primary.
mSplitScreenController
.startDismissSplit(taskInfo.isFocused /* toPrimaryTask */);
} else {
// If the secondary is not focused, dismiss to primary.
mSplitScreenController
.startDismissSplit(!taskInfo.isFocused /* toPrimaryTask */);
}
}
return;
}
if (taskInfo.hasParentTask()) {
// changed messages are noisy since it reports on every ensureVisibility. This
// conflicts with legacy app-transitions which "swaps" the position to a
// leash. For now, only update when position actually changes to avoid
// poorly-timed duplicate calls.
if (taskInfo.positionInParent.equals(mPositionByTaskId.get(taskInfo.taskId))) {
return;
}
handleChildTaskChanged(taskInfo);
} else {
handleTaskInfoChanged(taskInfo);
}
mPositionByTaskId.put(taskInfo.taskId, new Point(taskInfo.positionInParent));
}
}
private void handleChildTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
mLeashByTaskId.put(taskInfo.taskId, leash);
mPositionByTaskId.put(taskInfo.taskId, new Point(taskInfo.positionInParent));
if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */);
}
private void handleChildTaskChanged(RunningTaskInfo taskInfo) {
if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
final SurfaceControl leash = mLeashByTaskId.get(taskInfo.taskId);
updateChildTaskSurface(taskInfo, leash, false /* firstAppeared */);
}
private void updateChildTaskSurface(
RunningTaskInfo taskInfo, SurfaceControl leash, boolean firstAppeared) {
final Point taskPositionInParent = taskInfo.positionInParent;
mSyncQueue.runInSync(t -> {
t.setWindowCrop(leash, null);
t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y);
if (firstAppeared && !Transitions.ENABLE_SHELL_TRANSITIONS) {
t.setAlpha(leash, 1f);
t.setMatrix(leash, 1, 0, 0, 1);
t.show(leash);
}
});
}
/**
* This is effectively a finite state machine which moves between the various split-screen
* presentations based on the contents of the split regions.
*/
private void handleTaskInfoChanged(RunningTaskInfo info) {
if (!mSplitScreenSupported) {
// This shouldn't happen; but apparently there is a chance that SysUI crashes without
// system server receiving binder-death (or maybe it receives binder-death too late?).
// In this situation, when sys-ui restarts, the split root-tasks will still exist so
// there is a small window of time during init() where WM might send messages here
// before init() fails. So, avoid a cycle of crashes by returning early.
Log.e(TAG, "Got handleTaskInfoChanged when not initialized: " + info);
return;
}
final boolean secondaryImpliedMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME
|| (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS
&& mSplitScreenController.isHomeStackResizable());
final boolean primaryWasEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
final boolean secondaryWasEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
if (info.token.asBinder() == mPrimary.token.asBinder()) {
mPrimary = info;
} else if (info.token.asBinder() == mSecondary.token.asBinder()) {
mSecondary = info;
}
if (DEBUG) {
Log.d(TAG, "onTaskInfoChanged " + mPrimary + " " + mSecondary);
}
if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
final boolean primaryIsEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
final boolean secondaryIsEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
final boolean secondaryImpliesMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME
|| (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS
&& mSplitScreenController.isHomeStackResizable());
if (primaryIsEmpty == primaryWasEmpty && secondaryWasEmpty == secondaryIsEmpty
&& secondaryImpliedMinimize == secondaryImpliesMinimize) {
// No relevant changes
return;
}
if (primaryIsEmpty || secondaryIsEmpty) {
// At-least one of the splits is empty which means we are currently transitioning
// into or out-of split-screen mode.
if (DEBUG) {
Log.d(TAG, " at-least one split empty " + mPrimary.topActivityType
+ " " + mSecondary.topActivityType);
}
if (mSplitScreenController.isDividerVisible()) {
// Was in split-mode, which means we are leaving split, so continue that.
// This happens when the stack in the primary-split is dismissed.
if (DEBUG) {
Log.d(TAG, " was in split, so this means leave it "
+ mPrimary.topActivityType + " " + mSecondary.topActivityType);
}
mSplitScreenController.startDismissSplit(false /* toPrimaryTask */);
} else if (!primaryIsEmpty && primaryWasEmpty && secondaryWasEmpty) {
// Wasn't in split-mode (both were empty), but now that the primary split is
// populated, we should fully enter split by moving everything else into secondary.
// This just tells window-manager to reparent things, the UI will respond
// when it gets new task info for the secondary split.
if (DEBUG) {
Log.d(TAG, " was not in split, but primary is populated, so enter it");
}
mSplitScreenController.startEnterSplit();
}
} else if (secondaryImpliesMinimize) {
// Workaround for b/172686383, we can't rely on the sync bounds change transaction for
// the home task to finish before the last updateChildTaskSurface() call even if it's
// queued on the sync transaction queue, so ensure that the home task surface is updated
// again before we minimize
final ArrayList<RunningTaskInfo> tasks = new ArrayList<>();
mSplitScreenController.getWmProxy().getHomeAndRecentsTasks(tasks,
mSplitScreenController.getSecondaryRoot());
for (int i = 0; i < tasks.size(); i++) {
final RunningTaskInfo taskInfo = tasks.get(i);
final SurfaceControl leash = mLeashByTaskId.get(taskInfo.taskId);
if (leash != null) {
updateChildTaskSurface(taskInfo, leash, false /* firstAppeared */);
}
}
// Both splits are populated but the secondary split has a home/recents stack on top,
// so enter minimized mode.
mSplitScreenController.ensureMinimizedSplit();
} else {
// Both splits are populated by normal activities, so make sure we aren't minimized.
mSplitScreenController.ensureNormalSplit();
}
}
@Override
public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
if (!mLeashByTaskId.contains(taskId)) {
throw new IllegalArgumentException("There is no surface for taskId=" + taskId);
}
b.setParent(mLeashByTaskId.get(taskId));
}
@Override
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
final String childPrefix = innerPrefix + " ";
pw.println(prefix + this);
pw.println(innerPrefix + "mSplitScreenSupported=" + mSplitScreenSupported);
if (mPrimary != null) pw.println(innerPrefix + "mPrimary.taskId=" + mPrimary.taskId);
if (mSecondary != null) pw.println(innerPrefix + "mSecondary.taskId=" + mSecondary.taskId);
}
@Override
public String toString() {
return TAG;
}
}