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