| /* |
| * 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; |
| } |
| } |