1 /*
2  * Copyright (C) 2015 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_STANDARD;
22 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
23 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
24 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
25 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
26 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
27 import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
28 import static android.view.Display.DEFAULT_DISPLAY;
29 
30 import android.annotation.NonNull;
31 import android.app.ActivityManager;
32 import android.app.ActivityTaskManager;
33 import android.graphics.Rect;
34 import android.os.RemoteException;
35 import android.util.Log;
36 import android.view.Display;
37 import android.view.SurfaceControl;
38 import android.view.WindowManagerGlobal;
39 import android.window.TaskOrganizer;
40 import android.window.WindowContainerToken;
41 import android.window.WindowContainerTransaction;
42 
43 import com.android.internal.annotations.GuardedBy;
44 import com.android.internal.util.ArrayUtils;
45 import com.android.wm.shell.common.SyncTransactionQueue;
46 import com.android.wm.shell.transition.Transitions;
47 
48 import java.util.ArrayList;
49 import java.util.List;
50 
51 /**
52  * Proxy to simplify calls into window manager/activity manager
53  */
54 class WindowManagerProxy {
55 
56     private static final String TAG = "WindowManagerProxy";
57     private static final int[] HOME_AND_RECENTS = {ACTIVITY_TYPE_HOME, ACTIVITY_TYPE_RECENTS};
58     private static final int[] CONTROLLED_ACTIVITY_TYPES = {
59             ACTIVITY_TYPE_STANDARD,
60             ACTIVITY_TYPE_HOME,
61             ACTIVITY_TYPE_RECENTS,
62             ACTIVITY_TYPE_UNDEFINED
63     };
64     private static final int[] CONTROLLED_WINDOWING_MODES = {
65             WINDOWING_MODE_FULLSCREEN,
66             WINDOWING_MODE_SPLIT_SCREEN_SECONDARY,
67             WINDOWING_MODE_UNDEFINED
68     };
69 
70     @GuardedBy("mDockedRect")
71     private final Rect mDockedRect = new Rect();
72 
73     private final Rect mTmpRect1 = new Rect();
74 
75     @GuardedBy("mDockedRect")
76     private final Rect mTouchableRegion = new Rect();
77 
78     private final SyncTransactionQueue mSyncTransactionQueue;
79     private final TaskOrganizer mTaskOrganizer;
80 
WindowManagerProxy(SyncTransactionQueue syncQueue, TaskOrganizer taskOrganizer)81     WindowManagerProxy(SyncTransactionQueue syncQueue, TaskOrganizer taskOrganizer) {
82         mSyncTransactionQueue = syncQueue;
83         mTaskOrganizer = taskOrganizer;
84     }
85 
dismissOrMaximizeDocked(final LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, final boolean dismissOrMaximize)86     void dismissOrMaximizeDocked(final LegacySplitScreenTaskListener tiles,
87             LegacySplitDisplayLayout layout, final boolean dismissOrMaximize) {
88         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
89             tiles.mSplitScreenController.startDismissSplit(!dismissOrMaximize, true /* snapped */);
90         } else {
91             applyDismissSplit(tiles, layout, dismissOrMaximize);
92         }
93     }
94 
setResizing(final boolean resizing)95     public void setResizing(final boolean resizing) {
96         try {
97             ActivityTaskManager.getService().setSplitScreenResizing(resizing);
98         } catch (RemoteException e) {
99             Log.w(TAG, "Error calling setDockedStackResizing: " + e);
100         }
101     }
102 
103     /** Sets a touch region */
setTouchRegion(Rect region)104     public void setTouchRegion(Rect region) {
105         try {
106             synchronized (mDockedRect) {
107                 mTouchableRegion.set(region);
108             }
109             WindowManagerGlobal.getWindowManagerService().setDockedTaskDividerTouchRegion(
110                     mTouchableRegion);
111         } catch (RemoteException e) {
112             Log.w(TAG, "Failed to set touchable region: " + e);
113         }
114     }
115 
applyResizeSplits(int position, LegacySplitDisplayLayout splitLayout)116     void applyResizeSplits(int position, LegacySplitDisplayLayout splitLayout) {
117         WindowContainerTransaction t = new WindowContainerTransaction();
118         splitLayout.resizeSplits(position, t);
119         applySyncTransaction(t);
120     }
121 
getHomeAndRecentsTasks(List<ActivityManager.RunningTaskInfo> out, WindowContainerToken parent)122     boolean getHomeAndRecentsTasks(List<ActivityManager.RunningTaskInfo> out,
123             WindowContainerToken parent) {
124         boolean resizable = false;
125         List<ActivityManager.RunningTaskInfo> rootTasks = parent == null
126                 ? mTaskOrganizer.getRootTasks(Display.DEFAULT_DISPLAY, HOME_AND_RECENTS)
127                 : mTaskOrganizer.getChildTasks(parent, HOME_AND_RECENTS);
128         for (int i = 0, n = rootTasks.size(); i < n; ++i) {
129             final ActivityManager.RunningTaskInfo ti = rootTasks.get(i);
130             out.add(ti);
131             if (ti.topActivityType == ACTIVITY_TYPE_HOME) {
132                 resizable = ti.isResizeable;
133             }
134         }
135         return resizable;
136     }
137 
138     /**
139      * Assign a fixed override-bounds to home tasks that reflect their geometry while the primary
140      * split is minimized. This actually "sticks out" of the secondary split area, but when in
141      * minimized mode, the secondary split gets a 'negative' crop to expose it.
142      */
applyHomeTasksMinimized(LegacySplitDisplayLayout layout, WindowContainerToken parent, @NonNull WindowContainerTransaction wct)143     boolean applyHomeTasksMinimized(LegacySplitDisplayLayout layout, WindowContainerToken parent,
144             @NonNull WindowContainerTransaction wct) {
145         // Resize the home/recents stacks to the larger minimized-state size
146         final Rect homeBounds;
147         final ArrayList<ActivityManager.RunningTaskInfo> homeStacks = new ArrayList<>();
148         boolean isHomeResizable = getHomeAndRecentsTasks(homeStacks, parent);
149         if (isHomeResizable) {
150             homeBounds = layout.calcResizableMinimizedHomeStackBounds();
151         } else {
152             // home is not resizable, so lock it to its inherent orientation size.
153             homeBounds = new Rect(0, 0, 0, 0);
154             for (int i = homeStacks.size() - 1; i >= 0; --i) {
155                 if (homeStacks.get(i).topActivityType == ACTIVITY_TYPE_HOME) {
156                     final int orient = homeStacks.get(i).configuration.orientation;
157                     final boolean displayLandscape = layout.mDisplayLayout.isLandscape();
158                     final boolean isLandscape = orient == ORIENTATION_LANDSCAPE
159                             || (orient == ORIENTATION_UNDEFINED && displayLandscape);
160                     homeBounds.right = isLandscape == displayLandscape
161                             ? layout.mDisplayLayout.width() : layout.mDisplayLayout.height();
162                     homeBounds.bottom = isLandscape == displayLandscape
163                             ? layout.mDisplayLayout.height() : layout.mDisplayLayout.width();
164                     break;
165                 }
166             }
167         }
168         for (int i = homeStacks.size() - 1; i >= 0; --i) {
169             // For non-resizable homes, the minimized size is actually the fullscreen-size. As a
170             // result, we don't minimize for recents since it only shows half-size screenshots.
171             if (!isHomeResizable) {
172                 if (homeStacks.get(i).topActivityType == ACTIVITY_TYPE_RECENTS) {
173                     continue;
174                 }
175                 wct.setWindowingMode(homeStacks.get(i).token, WINDOWING_MODE_FULLSCREEN);
176             }
177             wct.setBounds(homeStacks.get(i).token, homeBounds);
178         }
179         layout.mTiles.mHomeBounds.set(homeBounds);
180         return isHomeResizable;
181     }
182 
183     /** @see #buildEnterSplit */
applyEnterSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout)184     boolean applyEnterSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout) {
185         // Set launchtile first so that any stack created after
186         // getAllRootTaskInfos and before reparent (even if unlikely) are placed
187         // correctly.
188         WindowContainerTransaction wct = new WindowContainerTransaction();
189         wct.setLaunchRoot(tiles.mSecondary.token, CONTROLLED_WINDOWING_MODES,
190                 CONTROLLED_ACTIVITY_TYPES);
191         final boolean isHomeResizable = buildEnterSplit(wct, tiles, layout);
192         applySyncTransaction(wct);
193         return isHomeResizable;
194     }
195 
196     /**
197      * Finishes entering split-screen by reparenting all FULLSCREEN tasks into the secondary split.
198      * This assumes there is already something in the primary split since that is usually what
199      * triggers a call to this. In the same transaction, this overrides the home task bounds via
200      * {@link #applyHomeTasksMinimized}.
201      *
202      * @return whether the home stack is resizable
203      */
buildEnterSplit(WindowContainerTransaction outWct, LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout)204     boolean buildEnterSplit(WindowContainerTransaction outWct, LegacySplitScreenTaskListener tiles,
205             LegacySplitDisplayLayout layout) {
206         List<ActivityManager.RunningTaskInfo> rootTasks =
207                 mTaskOrganizer.getRootTasks(DEFAULT_DISPLAY, null /* activityTypes */);
208         if (rootTasks.isEmpty()) {
209             return false;
210         }
211         ActivityManager.RunningTaskInfo topHomeTask = null;
212         for (int i = rootTasks.size() - 1; i >= 0; --i) {
213             final ActivityManager.RunningTaskInfo rootTask = rootTasks.get(i);
214             // Check whether the task can be moved to split secondary.
215             if (!rootTask.supportsMultiWindow && rootTask.topActivityType != ACTIVITY_TYPE_HOME) {
216                 continue;
217             }
218             // Only move split controlling tasks to split secondary.
219             final int windowingMode = rootTask.getWindowingMode();
220             if (!ArrayUtils.contains(CONTROLLED_WINDOWING_MODES, windowingMode)
221                     || !ArrayUtils.contains(CONTROLLED_ACTIVITY_TYPES, rootTask.getActivityType())
222                     // Excludes split screen secondary due to it's the root we're reparenting to.
223                     || windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) {
224                 continue;
225             }
226             // Since this iterates from bottom to top, update topHomeTask for every fullscreen task
227             // so it will be left with the status of the top one.
228             topHomeTask = isHomeOrRecentTask(rootTask) ? rootTask : null;
229             outWct.reparent(rootTask.token, tiles.mSecondary.token, true /* onTop */);
230         }
231         // Move the secondary split-forward.
232         outWct.reorder(tiles.mSecondary.token, true /* onTop */);
233         boolean isHomeResizable = applyHomeTasksMinimized(layout, null /* parent */,
234                 outWct);
235         if (topHomeTask != null && !Transitions.ENABLE_SHELL_TRANSITIONS) {
236             // Translate/update-crop of secondary out-of-band with sync transaction -- Until BALST
237             // is enabled, this temporarily syncs the home surface position with offset until
238             // sync transaction finishes.
239             outWct.setBoundsChangeTransaction(topHomeTask.token, tiles.mHomeBounds);
240         }
241         return isHomeResizable;
242     }
243 
isHomeOrRecentTask(ActivityManager.RunningTaskInfo ti)244     static boolean isHomeOrRecentTask(ActivityManager.RunningTaskInfo ti) {
245         final int atype = ti.getActivityType();
246         return atype == ACTIVITY_TYPE_HOME || atype == ACTIVITY_TYPE_RECENTS;
247     }
248 
249     /** @see #buildDismissSplit */
applyDismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, boolean dismissOrMaximize)250     void applyDismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout,
251             boolean dismissOrMaximize) {
252         // TODO(task-org): Once task-org is more complete, consider using Appeared/Vanished
253         //                 plus specific APIs to clean this up.
254         final WindowContainerTransaction wct = new WindowContainerTransaction();
255         // Set launch root first so that any task created after getChildContainers and
256         // before reparent (pretty unlikely) are put into fullscreen.
257         wct.setLaunchRoot(tiles.mSecondary.token, null, null);
258         buildDismissSplit(wct, tiles, layout, dismissOrMaximize);
259         applySyncTransaction(wct);
260     }
261 
262     /**
263      * Reparents all tile members back to their display and resets home task override bounds.
264      * @param dismissOrMaximize When {@code true} this resolves the split by closing the primary
265      *                          split (thus resulting in the top of the secondary split becoming
266      *                          fullscreen. {@code false} resolves the other way.
267      */
buildDismissSplit(WindowContainerTransaction outWct, LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, boolean dismissOrMaximize)268     static void buildDismissSplit(WindowContainerTransaction outWct,
269             LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout,
270             boolean dismissOrMaximize) {
271         // TODO(task-org): Once task-org is more complete, consider using Appeared/Vanished
272         //                 plus specific APIs to clean this up.
273         final TaskOrganizer taskOrg = tiles.getTaskOrganizer();
274         List<ActivityManager.RunningTaskInfo> primaryChildren =
275                 taskOrg.getChildTasks(tiles.mPrimary.token, null /* activityTypes */);
276         List<ActivityManager.RunningTaskInfo> secondaryChildren =
277                 taskOrg.getChildTasks(tiles.mSecondary.token, null /* activityTypes */);
278         // In some cases (eg. non-resizable is launched), system-server will leave split-screen.
279         // as a result, the above will not capture any tasks; yet, we need to clean-up the
280         // home task bounds.
281         List<ActivityManager.RunningTaskInfo> freeHomeAndRecents =
282                 taskOrg.getRootTasks(DEFAULT_DISPLAY, HOME_AND_RECENTS);
283         // Filter out the root split tasks
284         freeHomeAndRecents.removeIf(p -> p.token.equals(tiles.mSecondary.token)
285                 || p.token.equals(tiles.mPrimary.token));
286 
287         if (primaryChildren.isEmpty() && secondaryChildren.isEmpty()
288                 && freeHomeAndRecents.isEmpty()) {
289             return;
290         }
291         if (dismissOrMaximize) {
292             // Dismissing, so move all primary split tasks first
293             for (int i = primaryChildren.size() - 1; i >= 0; --i) {
294                 outWct.reparent(primaryChildren.get(i).token, null /* parent */,
295                         true /* onTop */);
296             }
297             boolean homeOnTop = false;
298             // Don't need to worry about home tasks because they are already in the "proper"
299             // order within the secondary split.
300             for (int i = secondaryChildren.size() - 1; i >= 0; --i) {
301                 final ActivityManager.RunningTaskInfo ti = secondaryChildren.get(i);
302                 outWct.reparent(ti.token, null /* parent */, true /* onTop */);
303                 if (isHomeOrRecentTask(ti)) {
304                     outWct.setBounds(ti.token, null);
305                     outWct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED);
306                     if (i == 0) {
307                         homeOnTop = true;
308                     }
309                 }
310             }
311             if (homeOnTop && !Transitions.ENABLE_SHELL_TRANSITIONS) {
312                 // Translate/update-crop of secondary out-of-band with sync transaction -- instead
313                 // play this in sync with new home-app frame because until BALST is enabled this
314                 // shows up on screen before the syncTransaction returns.
315                 // We only have access to the secondary root surface, though, so in order to
316                 // position things properly, we have to take into account the existing negative
317                 // offset/crop of the minimized-home task.
318                 final boolean landscape = layout.mDisplayLayout.isLandscape();
319                 final int posX = landscape ? layout.mSecondary.left - tiles.mHomeBounds.left
320                         : layout.mSecondary.left;
321                 final int posY = landscape ? layout.mSecondary.top
322                         : layout.mSecondary.top - tiles.mHomeBounds.top;
323                 final SurfaceControl.Transaction sft = new SurfaceControl.Transaction();
324                 sft.setPosition(tiles.mSecondarySurface, posX, posY);
325                 final Rect crop = new Rect(0, 0, layout.mDisplayLayout.width(),
326                         layout.mDisplayLayout.height());
327                 crop.offset(-posX, -posY);
328                 sft.setWindowCrop(tiles.mSecondarySurface, crop);
329                 outWct.setBoundsChangeTransaction(tiles.mSecondary.token, sft);
330             }
331         } else {
332             // Maximize, so move non-home secondary split first
333             for (int i = secondaryChildren.size() - 1; i >= 0; --i) {
334                 if (isHomeOrRecentTask(secondaryChildren.get(i))) {
335                     continue;
336                 }
337                 outWct.reparent(secondaryChildren.get(i).token, null /* parent */,
338                         true /* onTop */);
339             }
340             // Find and place home tasks in-between. This simulates the fact that there was
341             // nothing behind the primary split's tasks.
342             for (int i = secondaryChildren.size() - 1; i >= 0; --i) {
343                 final ActivityManager.RunningTaskInfo ti = secondaryChildren.get(i);
344                 if (isHomeOrRecentTask(ti)) {
345                     outWct.reparent(ti.token, null /* parent */, true /* onTop */);
346                     // reset bounds and mode too
347                     outWct.setBounds(ti.token, null);
348                     outWct.setWindowingMode(ti.token, WINDOWING_MODE_UNDEFINED);
349                 }
350             }
351             for (int i = primaryChildren.size() - 1; i >= 0; --i) {
352                 outWct.reparent(primaryChildren.get(i).token, null /* parent */,
353                         true /* onTop */);
354             }
355         }
356         for (int i = freeHomeAndRecents.size() - 1; i >= 0; --i) {
357             outWct.setBounds(freeHomeAndRecents.get(i).token, null);
358             outWct.setWindowingMode(freeHomeAndRecents.get(i).token, WINDOWING_MODE_UNDEFINED);
359         }
360         // Reset focusable to true
361         outWct.setFocusable(tiles.mPrimary.token, true /* focusable */);
362     }
363 
364     /**
365      * Utility to apply a sync transaction serially with other sync transactions.
366      *
367      * @see SyncTransactionQueue#queue
368      */
applySyncTransaction(WindowContainerTransaction wct)369     void applySyncTransaction(WindowContainerTransaction wct) {
370         mSyncTransactionQueue.queue(wct);
371     }
372 
373     /**
374      * @see SyncTransactionQueue#queueIfWaiting
375      */
queueSyncTransactionIfWaiting(WindowContainerTransaction wct)376     boolean queueSyncTransactionIfWaiting(WindowContainerTransaction wct) {
377         return mSyncTransactionQueue.queueIfWaiting(wct);
378     }
379 
380     /**
381      * @see SyncTransactionQueue#runInSync
382      */
runInSync(SyncTransactionQueue.TransactionRunnable runnable)383     void runInSync(SyncTransactionQueue.TransactionRunnable runnable) {
384         mSyncTransactionQueue.runInSync(runnable);
385     }
386 }
387