| /* |
| * 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.apppairs; |
| |
| import static android.app.ActivityTaskManager.INVALID_TASK_ID; |
| import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; |
| import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; |
| |
| import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT; |
| import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT; |
| import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED; |
| import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; |
| |
| import android.app.ActivityManager; |
| import android.graphics.Rect; |
| import android.view.SurfaceControl; |
| import android.view.SurfaceSession; |
| import android.window.WindowContainerToken; |
| import android.window.WindowContainerTransaction; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import com.android.internal.protolog.common.ProtoLog; |
| import com.android.wm.shell.ShellTaskOrganizer; |
| import com.android.wm.shell.common.DisplayController; |
| import com.android.wm.shell.common.DisplayImeController; |
| import com.android.wm.shell.common.SurfaceUtils; |
| import com.android.wm.shell.common.SyncTransactionQueue; |
| import com.android.wm.shell.common.split.SplitLayout; |
| |
| import java.io.PrintWriter; |
| |
| /** |
| * An app-pairs consisting of {@link #mRootTaskInfo} that acts as the hierarchy parent of |
| * {@link #mTaskInfo1} and {@link #mTaskInfo2} in the pair. |
| * Also includes all UI for managing the pair like the divider. |
| */ |
| class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayoutHandler { |
| private static final String TAG = AppPair.class.getSimpleName(); |
| |
| private ActivityManager.RunningTaskInfo mRootTaskInfo; |
| private SurfaceControl mRootTaskLeash; |
| private ActivityManager.RunningTaskInfo mTaskInfo1; |
| private SurfaceControl mTaskLeash1; |
| private ActivityManager.RunningTaskInfo mTaskInfo2; |
| private SurfaceControl mTaskLeash2; |
| private SurfaceControl mDimLayer1; |
| private SurfaceControl mDimLayer2; |
| private final SurfaceSession mSurfaceSession = new SurfaceSession(); |
| |
| private final AppPairsController mController; |
| private final SyncTransactionQueue mSyncQueue; |
| private final DisplayController mDisplayController; |
| private final DisplayImeController mDisplayImeController; |
| private SplitLayout mSplitLayout; |
| |
| AppPair(AppPairsController controller) { |
| mController = controller; |
| mSyncQueue = controller.getSyncTransactionQueue(); |
| mDisplayController = controller.getDisplayController(); |
| mDisplayImeController = controller.getDisplayImeController(); |
| } |
| |
| int getRootTaskId() { |
| return mRootTaskInfo != null ? mRootTaskInfo.taskId : INVALID_TASK_ID; |
| } |
| |
| private int getTaskId1() { |
| return mTaskInfo1 != null ? mTaskInfo1.taskId : INVALID_TASK_ID; |
| } |
| |
| private int getTaskId2() { |
| return mTaskInfo2 != null ? mTaskInfo2.taskId : INVALID_TASK_ID; |
| } |
| |
| boolean contains(int taskId) { |
| return taskId == getRootTaskId() || taskId == getTaskId1() || taskId == getTaskId2(); |
| } |
| |
| boolean pair(ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) { |
| ProtoLog.v(WM_SHELL_TASK_ORG, "pair task1=%d task2=%d in AppPair=%s", |
| task1.taskId, task2.taskId, this); |
| |
| if (!task1.supportsMultiWindow || !task2.supportsMultiWindow) { |
| ProtoLog.e(WM_SHELL_TASK_ORG, |
| "Can't pair tasks that doesn't support multi window, " |
| + "task1.supportsMultiWindow=%b, task2.supportsMultiWindow=%b", |
| task1.supportsMultiWindow, task2.supportsMultiWindow); |
| return false; |
| } |
| |
| mTaskInfo1 = task1; |
| mTaskInfo2 = task2; |
| mSplitLayout = new SplitLayout(TAG + "SplitDivider", |
| mDisplayController.getDisplayContext(mRootTaskInfo.displayId), |
| mRootTaskInfo.configuration, this /* layoutChangeListener */, |
| b -> b.setParent(mRootTaskLeash), mDisplayImeController, |
| mController.getTaskOrganizer()); |
| |
| final WindowContainerToken token1 = task1.token; |
| final WindowContainerToken token2 = task2.token; |
| final WindowContainerTransaction wct = new WindowContainerTransaction(); |
| |
| wct.setHidden(mRootTaskInfo.token, false) |
| .reparent(token1, mRootTaskInfo.token, true /* onTop */) |
| .reparent(token2, mRootTaskInfo.token, true /* onTop */) |
| .setWindowingMode(token1, WINDOWING_MODE_MULTI_WINDOW) |
| .setWindowingMode(token2, WINDOWING_MODE_MULTI_WINDOW) |
| .setBounds(token1, mSplitLayout.getBounds1()) |
| .setBounds(token2, mSplitLayout.getBounds2()) |
| // Moving the root task to top after the child tasks were repareted , or the root |
| // task cannot be visible and focused. |
| .reorder(mRootTaskInfo.token, true); |
| mController.getTaskOrganizer().applyTransaction(wct); |
| return true; |
| } |
| |
| void unpair() { |
| unpair(null /* toTopToken */); |
| } |
| |
| private void unpair(@Nullable WindowContainerToken toTopToken) { |
| final WindowContainerToken token1 = mTaskInfo1.token; |
| final WindowContainerToken token2 = mTaskInfo2.token; |
| final WindowContainerTransaction wct = new WindowContainerTransaction(); |
| |
| // Reparent out of this container and reset windowing mode. |
| wct.setHidden(mRootTaskInfo.token, true) |
| .reorder(mRootTaskInfo.token, false) |
| .reparent(token1, null, token1 == toTopToken /* onTop */) |
| .reparent(token2, null, token2 == toTopToken /* onTop */) |
| .setWindowingMode(token1, WINDOWING_MODE_UNDEFINED) |
| .setWindowingMode(token2, WINDOWING_MODE_UNDEFINED); |
| mController.getTaskOrganizer().applyTransaction(wct); |
| |
| mTaskInfo1 = null; |
| mTaskInfo2 = null; |
| mSplitLayout.release(); |
| mSplitLayout = null; |
| } |
| |
| @Override |
| public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { |
| if (mRootTaskInfo == null || taskInfo.taskId == mRootTaskInfo.taskId) { |
| mRootTaskInfo = taskInfo; |
| mRootTaskLeash = leash; |
| } else if (taskInfo.taskId == getTaskId1()) { |
| mTaskInfo1 = taskInfo; |
| mTaskLeash1 = leash; |
| mSyncQueue.runInSync(t -> mDimLayer1 = |
| SurfaceUtils.makeDimLayer(t, mTaskLeash1, "Dim layer", mSurfaceSession)); |
| } else if (taskInfo.taskId == getTaskId2()) { |
| mTaskInfo2 = taskInfo; |
| mTaskLeash2 = leash; |
| mSyncQueue.runInSync(t -> mDimLayer2 = |
| SurfaceUtils.makeDimLayer(t, mTaskLeash2, "Dim layer", mSurfaceSession)); |
| } else { |
| throw new IllegalStateException("Unknown task=" + taskInfo.taskId); |
| } |
| |
| if (mTaskLeash1 == null || mTaskLeash2 == null) return; |
| |
| mSplitLayout.init(); |
| final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); |
| final Rect dividerBounds = mSplitLayout.getDividerBounds(); |
| |
| // TODO: Is there more we need to do here? |
| mSyncQueue.runInSync(t -> { |
| t.setLayer(dividerLeash, Integer.MAX_VALUE) |
| .setPosition(mTaskLeash1, mTaskInfo1.positionInParent.x, |
| mTaskInfo1.positionInParent.y) |
| .setPosition(mTaskLeash2, mTaskInfo2.positionInParent.x, |
| mTaskInfo2.positionInParent.y) |
| .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top) |
| .show(mRootTaskLeash) |
| .show(mTaskLeash1) |
| .show(mTaskLeash2); |
| }); |
| } |
| |
| @Override |
| public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { |
| if (!taskInfo.supportsMultiWindow) { |
| // Dismiss AppPair if the task no longer supports multi window. |
| mController.unpair(mRootTaskInfo.taskId); |
| return; |
| } |
| if (taskInfo.taskId == getRootTaskId()) { |
| if (mRootTaskInfo.isVisible != taskInfo.isVisible) { |
| mSyncQueue.runInSync(t -> { |
| if (taskInfo.isVisible) { |
| t.show(mRootTaskLeash); |
| } else { |
| t.hide(mRootTaskLeash); |
| } |
| }); |
| } |
| mRootTaskInfo = taskInfo; |
| |
| if (mSplitLayout != null |
| && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)) { |
| onBoundsChanged(mSplitLayout); |
| } |
| } else if (taskInfo.taskId == getTaskId1()) { |
| mTaskInfo1 = taskInfo; |
| } else if (taskInfo.taskId == getTaskId2()) { |
| mTaskInfo2 = taskInfo; |
| } else { |
| throw new IllegalStateException("Unknown task=" + taskInfo.taskId); |
| } |
| } |
| |
| @Override |
| public int getSplitItemPosition(WindowContainerToken token) { |
| if (token == null) { |
| return SPLIT_POSITION_UNDEFINED; |
| } |
| |
| if (token.equals(mTaskInfo1.getToken())) { |
| return SPLIT_POSITION_TOP_OR_LEFT; |
| } else if (token.equals(mTaskInfo2.getToken())) { |
| return SPLIT_POSITION_BOTTOM_OR_RIGHT; |
| } |
| |
| return SPLIT_POSITION_UNDEFINED; |
| } |
| |
| @Override |
| public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { |
| if (taskInfo.taskId == getRootTaskId()) { |
| // We don't want to release this object back to the pool since the root task went away. |
| mController.unpair(mRootTaskInfo.taskId, false /* releaseToPool */); |
| } else if (taskInfo.taskId == getTaskId1()) { |
| mController.unpair(mRootTaskInfo.taskId); |
| mSyncQueue.runInSync(t -> t.remove(mDimLayer1)); |
| } else if (taskInfo.taskId == getTaskId2()) { |
| mController.unpair(mRootTaskInfo.taskId); |
| mSyncQueue.runInSync(t -> t.remove(mDimLayer2)); |
| } |
| } |
| |
| @Override |
| public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { |
| if (getRootTaskId() == taskId) { |
| b.setParent(mRootTaskLeash); |
| } else if (getTaskId1() == taskId) { |
| b.setParent(mTaskLeash1); |
| } else if (getTaskId2() == taskId) { |
| b.setParent(mTaskLeash2); |
| } else { |
| throw new IllegalArgumentException("There is no surface for taskId=" + 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 + "Root taskId=" + getRootTaskId() |
| + " winMode=" + mRootTaskInfo.getWindowingMode()); |
| if (mTaskInfo1 != null) { |
| pw.println(innerPrefix + "1 taskId=" + mTaskInfo1.taskId |
| + " winMode=" + mTaskInfo1.getWindowingMode()); |
| } |
| if (mTaskInfo2 != null) { |
| pw.println(innerPrefix + "2 taskId=" + mTaskInfo2.taskId |
| + " winMode=" + mTaskInfo2.getWindowingMode()); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return TAG + "#" + getRootTaskId(); |
| } |
| |
| @Override |
| public void onSnappedToDismiss(boolean snappedToEnd) { |
| unpair(snappedToEnd ? mTaskInfo1.token : mTaskInfo2.token /* toTopToken */); |
| } |
| |
| @Override |
| public void onBoundsChanging(SplitLayout layout) { |
| mSyncQueue.runInSync(t -> |
| layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2)); |
| } |
| |
| @Override |
| public void onBoundsChanged(SplitLayout layout) { |
| final WindowContainerTransaction wct = new WindowContainerTransaction(); |
| layout.applyTaskChanges(wct, mTaskInfo1, mTaskInfo2); |
| mSyncQueue.queue(wct); |
| mSyncQueue.runInSync(t -> |
| layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2)); |
| } |
| } |