1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.legacysplitscreen;
18 
19 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
20 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
21 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
22 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
23 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
24 import static android.view.Display.DEFAULT_DISPLAY;
25 
26 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
27 
28 import android.app.ActivityManager.RunningTaskInfo;
29 import android.graphics.Point;
30 import android.graphics.Rect;
31 import android.util.Log;
32 import android.util.SparseArray;
33 import android.view.SurfaceControl;
34 import android.view.SurfaceSession;
35 import android.window.TaskOrganizer;
36 
37 import androidx.annotation.NonNull;
38 
39 import com.android.internal.protolog.common.ProtoLog;
40 import com.android.wm.shell.ShellTaskOrganizer;
41 import com.android.wm.shell.common.SurfaceUtils;
42 import com.android.wm.shell.common.SyncTransactionQueue;
43 import com.android.wm.shell.transition.Transitions;
44 
45 import java.io.PrintWriter;
46 import java.util.ArrayList;
47 
48 class LegacySplitScreenTaskListener implements ShellTaskOrganizer.TaskListener {
49     private static final String TAG = LegacySplitScreenTaskListener.class.getSimpleName();
50     private static final boolean DEBUG = LegacySplitScreenController.DEBUG;
51 
52     private final ShellTaskOrganizer mTaskOrganizer;
53     private final SyncTransactionQueue mSyncQueue;
54     private final SparseArray<SurfaceControl> mLeashByTaskId = new SparseArray<>();
55 
56     // TODO(shell-transitions): Remove when switched to shell-transitions.
57     private final SparseArray<Point> mPositionByTaskId = new SparseArray<>();
58 
59     RunningTaskInfo mPrimary;
60     RunningTaskInfo mSecondary;
61     SurfaceControl mPrimarySurface;
62     SurfaceControl mSecondarySurface;
63     SurfaceControl mPrimaryDim;
64     SurfaceControl mSecondaryDim;
65     Rect mHomeBounds = new Rect();
66     final LegacySplitScreenController mSplitScreenController;
67     private boolean mSplitScreenSupported = false;
68 
69     final SurfaceSession mSurfaceSession = new SurfaceSession();
70 
71     private final LegacySplitScreenTransitions mSplitTransitions;
72 
LegacySplitScreenTaskListener(LegacySplitScreenController splitScreenController, ShellTaskOrganizer shellTaskOrganizer, Transitions transitions, SyncTransactionQueue syncQueue)73     LegacySplitScreenTaskListener(LegacySplitScreenController splitScreenController,
74             ShellTaskOrganizer shellTaskOrganizer,
75             Transitions transitions,
76             SyncTransactionQueue syncQueue) {
77         mSplitScreenController = splitScreenController;
78         mTaskOrganizer = shellTaskOrganizer;
79         mSplitTransitions = new LegacySplitScreenTransitions(splitScreenController.mTransactionPool,
80                 transitions, mSplitScreenController, this);
81         transitions.addHandler(mSplitTransitions);
82         mSyncQueue = syncQueue;
83     }
84 
init()85     void init() {
86         synchronized (this) {
87             try {
88                 mTaskOrganizer.createRootTask(
89                         DEFAULT_DISPLAY, WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, this);
90                 mTaskOrganizer.createRootTask(
91                         DEFAULT_DISPLAY, WINDOWING_MODE_SPLIT_SCREEN_SECONDARY, this);
92             } catch (Exception e) {
93                 // teardown to prevent callbacks
94                 mTaskOrganizer.removeListener(this);
95                 throw e;
96             }
97         }
98     }
99 
isSplitScreenSupported()100     boolean isSplitScreenSupported() {
101         return mSplitScreenSupported;
102     }
103 
getTransaction()104     SurfaceControl.Transaction getTransaction() {
105         return mSplitScreenController.mTransactionPool.acquire();
106     }
107 
releaseTransaction(SurfaceControl.Transaction t)108     void releaseTransaction(SurfaceControl.Transaction t) {
109         mSplitScreenController.mTransactionPool.release(t);
110     }
111 
getTaskOrganizer()112     TaskOrganizer getTaskOrganizer() {
113         return mTaskOrganizer;
114     }
115 
getSplitTransitions()116     LegacySplitScreenTransitions getSplitTransitions() {
117         return mSplitTransitions;
118     }
119 
120     @Override
onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash)121     public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
122         synchronized (this) {
123             if (taskInfo.hasParentTask()) {
124                 handleChildTaskAppeared(taskInfo, leash);
125                 return;
126             }
127 
128             final int winMode = taskInfo.getWindowingMode();
129             if (winMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) {
130                 ProtoLog.v(WM_SHELL_TASK_ORG,
131                         "%s onTaskAppeared Primary taskId=%d", TAG, taskInfo.taskId);
132                 mPrimary = taskInfo;
133                 mPrimarySurface = leash;
134             } else if (winMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) {
135                 ProtoLog.v(WM_SHELL_TASK_ORG,
136                         "%s onTaskAppeared Secondary taskId=%d", TAG, taskInfo.taskId);
137                 mSecondary = taskInfo;
138                 mSecondarySurface = leash;
139             } else {
140                 ProtoLog.v(WM_SHELL_TASK_ORG, "%s onTaskAppeared unknown taskId=%d winMode=%d",
141                         TAG, taskInfo.taskId, winMode);
142             }
143 
144             if (!mSplitScreenSupported && mPrimarySurface != null && mSecondarySurface != null) {
145                 mSplitScreenSupported = true;
146                 mSplitScreenController.onSplitScreenSupported();
147                 ProtoLog.v(WM_SHELL_TASK_ORG, "%s onTaskAppeared Supported", TAG);
148 
149                 // Initialize dim surfaces:
150                 SurfaceControl.Transaction t = getTransaction();
151                 mPrimaryDim = SurfaceUtils.makeDimLayer(
152                         t, mPrimarySurface, "Primary Divider Dim", mSurfaceSession);
153                 mSecondaryDim = SurfaceUtils.makeDimLayer(
154                         t, mSecondarySurface, "Secondary Divider Dim", mSurfaceSession);
155                 t.apply();
156                 releaseTransaction(t);
157             }
158         }
159     }
160 
161     @Override
onTaskVanished(RunningTaskInfo taskInfo)162     public void onTaskVanished(RunningTaskInfo taskInfo) {
163         synchronized (this) {
164             mPositionByTaskId.remove(taskInfo.taskId);
165             if (taskInfo.hasParentTask()) {
166                 mLeashByTaskId.remove(taskInfo.taskId);
167                 return;
168             }
169 
170             final boolean isPrimaryTask = mPrimary != null
171                     && taskInfo.token.equals(mPrimary.token);
172             final boolean isSecondaryTask = mSecondary != null
173                     && taskInfo.token.equals(mSecondary.token);
174 
175             if (mSplitScreenSupported && (isPrimaryTask || isSecondaryTask)) {
176                 mSplitScreenSupported = false;
177 
178                 SurfaceControl.Transaction t = getTransaction();
179                 t.remove(mPrimaryDim);
180                 t.remove(mSecondaryDim);
181                 t.remove(mPrimarySurface);
182                 t.remove(mSecondarySurface);
183                 t.apply();
184                 releaseTransaction(t);
185 
186                 mSplitScreenController.onTaskVanished();
187             }
188         }
189     }
190 
191     @Override
onTaskInfoChanged(RunningTaskInfo taskInfo)192     public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
193         if (taskInfo.displayId != DEFAULT_DISPLAY) {
194             return;
195         }
196         synchronized (this) {
197             if (!taskInfo.supportsMultiWindow) {
198                 if (mSplitScreenController.isDividerVisible()) {
199                     // Dismiss the split screen if the task no longer supports multi window.
200                     if (taskInfo.taskId == mPrimary.taskId
201                             || taskInfo.parentTaskId == mPrimary.taskId) {
202                         // If the primary is focused, dismiss to primary.
203                         mSplitScreenController
204                                 .startDismissSplit(taskInfo.isFocused /* toPrimaryTask */);
205                     } else {
206                         // If the secondary is not focused, dismiss to primary.
207                         mSplitScreenController
208                                 .startDismissSplit(!taskInfo.isFocused /* toPrimaryTask */);
209                     }
210                 }
211                 return;
212             }
213             if (taskInfo.hasParentTask()) {
214                 // changed messages are noisy since it reports on every ensureVisibility. This
215                 // conflicts with legacy app-transitions which "swaps" the position to a
216                 // leash. For now, only update when position actually changes to avoid
217                 // poorly-timed duplicate calls.
218                 if (taskInfo.positionInParent.equals(mPositionByTaskId.get(taskInfo.taskId))) {
219                     return;
220                 }
221                 handleChildTaskChanged(taskInfo);
222             } else {
223                 handleTaskInfoChanged(taskInfo);
224             }
225             mPositionByTaskId.put(taskInfo.taskId, new Point(taskInfo.positionInParent));
226         }
227     }
228 
handleChildTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash)229     private void handleChildTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
230         mLeashByTaskId.put(taskInfo.taskId, leash);
231         mPositionByTaskId.put(taskInfo.taskId, new Point(taskInfo.positionInParent));
232         if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
233         updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */);
234     }
235 
handleChildTaskChanged(RunningTaskInfo taskInfo)236     private void handleChildTaskChanged(RunningTaskInfo taskInfo) {
237         if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
238         final SurfaceControl leash = mLeashByTaskId.get(taskInfo.taskId);
239         updateChildTaskSurface(taskInfo, leash, false /* firstAppeared */);
240     }
241 
updateChildTaskSurface( RunningTaskInfo taskInfo, SurfaceControl leash, boolean firstAppeared)242     private void updateChildTaskSurface(
243             RunningTaskInfo taskInfo, SurfaceControl leash, boolean firstAppeared) {
244         final Point taskPositionInParent = taskInfo.positionInParent;
245         mSyncQueue.runInSync(t -> {
246             t.setWindowCrop(leash, null);
247             t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y);
248             if (firstAppeared && !Transitions.ENABLE_SHELL_TRANSITIONS) {
249                 t.setAlpha(leash, 1f);
250                 t.setMatrix(leash, 1, 0, 0, 1);
251                 t.show(leash);
252             }
253         });
254     }
255 
256     /**
257      * This is effectively a finite state machine which moves between the various split-screen
258      * presentations based on the contents of the split regions.
259      */
handleTaskInfoChanged(RunningTaskInfo info)260     private void handleTaskInfoChanged(RunningTaskInfo info) {
261         if (!mSplitScreenSupported) {
262             // This shouldn't happen; but apparently there is a chance that SysUI crashes without
263             // system server receiving binder-death (or maybe it receives binder-death too late?).
264             // In this situation, when sys-ui restarts, the split root-tasks will still exist so
265             // there is a small window of time during init() where WM might send messages here
266             // before init() fails. So, avoid a cycle of crashes by returning early.
267             Log.e(TAG, "Got handleTaskInfoChanged when not initialized: " + info);
268             return;
269         }
270         final boolean secondaryImpliedMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME
271                 || (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS
272                         && mSplitScreenController.isHomeStackResizable());
273         final boolean primaryWasEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
274         final boolean secondaryWasEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
275         if (info.token.asBinder() == mPrimary.token.asBinder()) {
276             mPrimary = info;
277         } else if (info.token.asBinder() == mSecondary.token.asBinder()) {
278             mSecondary = info;
279         }
280         if (DEBUG) {
281             Log.d(TAG, "onTaskInfoChanged " + mPrimary + "  " + mSecondary);
282         }
283         if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
284         final boolean primaryIsEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
285         final boolean secondaryIsEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
286         final boolean secondaryImpliesMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME
287                 || (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS
288                         && mSplitScreenController.isHomeStackResizable());
289         if (primaryIsEmpty == primaryWasEmpty && secondaryWasEmpty == secondaryIsEmpty
290                 && secondaryImpliedMinimize == secondaryImpliesMinimize) {
291             // No relevant changes
292             return;
293         }
294         if (primaryIsEmpty || secondaryIsEmpty) {
295             // At-least one of the splits is empty which means we are currently transitioning
296             // into or out-of split-screen mode.
297             if (DEBUG) {
298                 Log.d(TAG, " at-least one split empty " + mPrimary.topActivityType
299                         + "  " + mSecondary.topActivityType);
300             }
301             if (mSplitScreenController.isDividerVisible()) {
302                 // Was in split-mode, which means we are leaving split, so continue that.
303                 // This happens when the stack in the primary-split is dismissed.
304                 if (DEBUG) {
305                     Log.d(TAG, "    was in split, so this means leave it "
306                             + mPrimary.topActivityType + "  " + mSecondary.topActivityType);
307                 }
308                 mSplitScreenController.startDismissSplit(false /* toPrimaryTask */);
309             } else if (!primaryIsEmpty && primaryWasEmpty && secondaryWasEmpty) {
310                 // Wasn't in split-mode (both were empty), but now that the primary split is
311                 // populated, we should fully enter split by moving everything else into secondary.
312                 // This just tells window-manager to reparent things, the UI will respond
313                 // when it gets new task info for the secondary split.
314                 if (DEBUG) {
315                     Log.d(TAG, "   was not in split, but primary is populated, so enter it");
316                 }
317                 mSplitScreenController.startEnterSplit();
318             }
319         } else if (secondaryImpliesMinimize) {
320             // Workaround for b/172686383, we can't rely on the sync bounds change transaction for
321             // the home task to finish before the last updateChildTaskSurface() call even if it's
322             // queued on the sync transaction queue, so ensure that the home task surface is updated
323             // again before we minimize
324             final ArrayList<RunningTaskInfo> tasks = new ArrayList<>();
325             mSplitScreenController.getWmProxy().getHomeAndRecentsTasks(tasks,
326                     mSplitScreenController.getSecondaryRoot());
327             for (int i = 0; i < tasks.size(); i++) {
328                 final RunningTaskInfo taskInfo = tasks.get(i);
329                 final SurfaceControl leash = mLeashByTaskId.get(taskInfo.taskId);
330                 if (leash != null) {
331                     updateChildTaskSurface(taskInfo, leash, false /* firstAppeared */);
332                 }
333             }
334 
335             // Both splits are populated but the secondary split has a home/recents stack on top,
336             // so enter minimized mode.
337             mSplitScreenController.ensureMinimizedSplit();
338         } else {
339             // Both splits are populated by normal activities, so make sure we aren't minimized.
340             mSplitScreenController.ensureNormalSplit();
341         }
342     }
343 
344     @Override
attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b)345     public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
346         if (!mLeashByTaskId.contains(taskId)) {
347             throw new IllegalArgumentException("There is no surface for taskId=" + taskId);
348         }
349         b.setParent(mLeashByTaskId.get(taskId));
350     }
351 
352     @Override
dump(@onNull PrintWriter pw, String prefix)353     public void dump(@NonNull PrintWriter pw, String prefix) {
354         final String innerPrefix = prefix + "  ";
355         final String childPrefix = innerPrefix + "  ";
356         pw.println(prefix + this);
357         pw.println(innerPrefix + "mSplitScreenSupported=" + mSplitScreenSupported);
358         if (mPrimary != null) pw.println(innerPrefix + "mPrimary.taskId=" + mPrimary.taskId);
359         if (mSecondary != null) pw.println(innerPrefix + "mSecondary.taskId=" + mSecondary.taskId);
360     }
361 
362     @Override
toString()363     public String toString() {
364         return TAG;
365     }
366 }
367