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 android.server.wm;
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_MULTI_WINDOW;
25 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
26 import static android.view.Display.DEFAULT_DISPLAY;
27 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
28 
29 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
30 
31 import android.app.ActivityManager;
32 import android.app.WindowConfiguration;
33 import android.content.Context;
34 import android.graphics.Rect;
35 import android.hardware.display.DisplayManager;
36 import android.os.Binder;
37 import android.os.IBinder;
38 import android.os.SystemClock;
39 import android.util.ArraySet;
40 import android.util.Log;
41 import android.view.Surface;
42 import android.view.SurfaceControl;
43 import android.view.WindowManager;
44 import android.window.TaskAppearedInfo;
45 import android.window.TaskOrganizer;
46 import android.window.WindowContainerToken;
47 import android.window.WindowContainerTransaction;
48 
49 import androidx.annotation.NonNull;
50 import androidx.annotation.Nullable;
51 
52 import org.junit.Assert;
53 
54 import java.util.HashMap;
55 import java.util.List;
56 import java.util.concurrent.TimeUnit;
57 import java.util.function.Predicate;
58 
59 public class TestTaskOrganizer extends TaskOrganizer {
60     private static final String TAG = TestTaskOrganizer.class.getSimpleName();
61     public static final int INVALID_TASK_ID = -1;
62 
63     private boolean mRegistered;
64     private ActivityManager.RunningTaskInfo mRootPrimary;
65     private ActivityManager.RunningTaskInfo mRootSecondary;
66     @Nullable private ActivityManager.RunningTaskInfo mPrePrimarySplitTaskInfo;
67     @Nullable private ActivityManager.RunningTaskInfo mPreSecondarySplitTaskInfo;
68     private IBinder mPrimaryCookie;
69     private IBinder mSecondaryCookie;
70     private final HashMap<Integer, ActivityManager.RunningTaskInfo> mKnownTasks = new HashMap<>();
71     private final HashMap<Integer, SurfaceControl> mTaskLeashes = new HashMap<>();
72     private final ArraySet<Integer> mPrimaryChildrenTaskIds = new ArraySet<>();
73     private final ArraySet<Integer> mSecondaryChildrenTaskIds = new ArraySet<>();
74     private final Rect mPrimaryBounds = new Rect();
75     private final Rect mSecondaryBounds = new Rect();
76 
77     private static final int[] CONTROLLED_ACTIVITY_TYPES = {
78             ACTIVITY_TYPE_STANDARD,
79             ACTIVITY_TYPE_HOME,
80             ACTIVITY_TYPE_RECENTS,
81             ACTIVITY_TYPE_UNDEFINED
82     };
83     private static final int[] CONTROLLED_WINDOWING_MODES = {
84             WINDOWING_MODE_FULLSCREEN,
85             WINDOWING_MODE_MULTI_WINDOW,
86             WINDOWING_MODE_UNDEFINED
87     };
88 
89     @Override
registerOrganizer()90     public List<TaskAppearedInfo> registerOrganizer() {
91         final Context context = getInstrumentation().getContext();
92         final Rect bounds = context.createDisplayContext(
93                 context.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY))
94                 .createWindowContext(TYPE_APPLICATION, null /* options */)
95                 .getSystemService(WindowManager.class)
96                 .getCurrentWindowMetrics()
97                 .getBounds();
98 
99         final boolean isLandscape = bounds.width() > bounds.height();
100         if (isLandscape) {
101             bounds.splitVertically(mPrimaryBounds, mSecondaryBounds);
102         } else {
103             bounds.splitHorizontally(mPrimaryBounds, mSecondaryBounds);
104         }
105         Log.i(TAG, "registerOrganizer with PrimaryBounds=" + mPrimaryBounds
106                 + " SecondaryBounds=" + mSecondaryBounds);
107 
108         synchronized (this) {
109             final List<TaskAppearedInfo> taskInfos = super.registerOrganizer();
110             for (int i = 0; i < taskInfos.size(); i++) {
111                 final TaskAppearedInfo info = taskInfos.get(i);
112                 onTaskAppeared(info.getTaskInfo(), info.getLeash());
113             }
114             createRootTasksIfNeeded();
115             return taskInfos;
116         }
117     }
118 
createRootTasksIfNeeded()119     private void createRootTasksIfNeeded() {
120         synchronized (this) {
121             if (mPrimaryCookie != null) return;
122             mPrimaryCookie = new Binder();
123             mSecondaryCookie = new Binder();
124 
125             createRootTask(DEFAULT_DISPLAY, WINDOWING_MODE_MULTI_WINDOW, mPrimaryCookie);
126             createRootTask(DEFAULT_DISPLAY, WINDOWING_MODE_MULTI_WINDOW, mSecondaryCookie);
127 
128             waitForAndAssert(o -> mRootPrimary != null && mRootSecondary != null,
129                     "Failed to get root tasks");
130             Log.e(TAG, "createRootTasksIfNeeded primary=" + mRootPrimary.taskId
131                     + " secondary=" + mRootSecondary.taskId);
132 
133             // Set the roots as adjacent to each other.
134             final WindowContainerTransaction wct = new WindowContainerTransaction();
135             wct.setAdjacentRoots(mRootPrimary.getToken(), mRootSecondary.getToken());
136             wct.setLaunchAdjacentFlagRoot(mRootSecondary.getToken());
137             applyTransaction(wct);
138         }
139     }
140 
waitForAndAssert(Predicate<Object> condition, String failureMessage)141     private void waitForAndAssert(Predicate<Object> condition, String failureMessage) {
142         waitFor(condition);
143         if (!condition.test(this)) {
144             Assert.fail(failureMessage);
145         }
146     }
147 
waitFor(Predicate<Object> condition)148     private void waitFor(Predicate<Object> condition) {
149         final long waitTillTime = SystemClock.elapsedRealtime() + TimeUnit.SECONDS.toMillis(5);
150         while (!condition.test(this)
151                 && SystemClock.elapsedRealtime() < waitTillTime) {
152             try {
153                 wait(TimeUnit.SECONDS.toMillis(5));
154             } catch (InterruptedException e) {
155                 e.printStackTrace();
156             }
157         }
158     }
159 
notifyOnEnd(Runnable r)160     private void notifyOnEnd(Runnable r) {
161         r.run();
162         notifyAll();
163     }
164 
registerOrganizerIfNeeded()165     public void registerOrganizerIfNeeded() {
166         synchronized (this) {
167             if (mRegistered) return;
168 
169             NestedShellPermission.run(() -> {
170                 registerOrganizer();
171             });
172             mRegistered = true;
173         }
174     }
175 
unregisterOrganizerIfNeeded()176     public void unregisterOrganizerIfNeeded() {
177         synchronized (this) {
178             if (!mRegistered) return;
179             mRegistered = false;
180 
181             NestedShellPermission.run(() -> {
182                 dismissSplitScreen();
183 
184                 deleteRootTask(mRootPrimary.getToken());
185                 mRootPrimary = null;
186                 mPrimaryCookie = null;
187                 mPrimaryChildrenTaskIds.clear();
188                 deleteRootTask(mRootSecondary.getToken());
189                 mRootSecondary = null;
190                 mSecondaryCookie = null;
191                 mSecondaryChildrenTaskIds.clear();
192                 mPrePrimarySplitTaskInfo = null;
193                 mPreSecondarySplitTaskInfo = null;
194 
195                 super.unregisterOrganizer();
196             });
197         }
198     }
199 
putTaskInSplitPrimary(int taskId)200     public void putTaskInSplitPrimary(int taskId) {
201         NestedShellPermission.run(() -> {
202             synchronized (this) {
203                 registerOrganizerIfNeeded();
204                 ActivityManager.RunningTaskInfo taskInfo = getTaskInfo(taskId);
205                 final WindowContainerTransaction t = new WindowContainerTransaction()
206                         .setBounds(mRootPrimary.getToken(), mPrimaryBounds)
207                         .setBounds(taskInfo.getToken(), null)
208                         .setWindowingMode(taskInfo.getToken(), WINDOWING_MODE_UNDEFINED)
209                         .reparent(taskInfo.getToken(), mRootPrimary.getToken(), true /* onTop */)
210                         .reorder(mRootPrimary.getToken(), true /* onTop */);
211                 applyTransaction(t);
212                 mPrePrimarySplitTaskInfo = taskInfo;
213 
214                 waitForAndAssert(
215                         o -> mPrimaryChildrenTaskIds.contains(taskId),
216                         "Can't put putTaskInSplitPrimary taskId=" + taskId);
217 
218                 Log.e(TAG, "putTaskInSplitPrimary taskId=" + taskId);
219             }
220         });
221     }
222 
putTaskInSplitSecondary(int taskId)223     public void putTaskInSplitSecondary(int taskId) {
224         NestedShellPermission.run(() -> {
225             synchronized (this) {
226                 registerOrganizerIfNeeded();
227                 ActivityManager.RunningTaskInfo taskInfo = getTaskInfo(taskId);
228                 final WindowContainerTransaction t = new WindowContainerTransaction()
229                         .setBounds(mRootSecondary.getToken(), mSecondaryBounds)
230                         .setBounds(taskInfo.getToken(), null)
231                         .setWindowingMode(taskInfo.getToken(), WINDOWING_MODE_UNDEFINED)
232                         .reparent(taskInfo.getToken(), mRootSecondary.getToken(), true /* onTop */)
233                         .reorder(mRootSecondary.getToken(), true /* onTop */);
234                 applyTransaction(t);
235                 mPreSecondarySplitTaskInfo = taskInfo;
236 
237                 waitForAndAssert(
238                         o -> mSecondaryChildrenTaskIds.contains(taskId),
239                         "Can't put putTaskInSplitSecondary taskId=" + taskId);
240 
241                 Log.e(TAG, "putTaskInSplitSecondary taskId=" + taskId);
242             }
243         });
244     }
245 
setLaunchRoot(int taskId)246     public void setLaunchRoot(int taskId) {
247         NestedShellPermission.run(() -> {
248             synchronized (this) {
249                 final WindowContainerTransaction t = new WindowContainerTransaction()
250                         .setLaunchRoot(mKnownTasks.get(taskId).getToken(),
251                                 CONTROLLED_WINDOWING_MODES, CONTROLLED_ACTIVITY_TYPES);
252                 applyTransaction(t);
253             }
254         });
255     }
256 
dismissSplitScreen()257     void dismissSplitScreen() {
258         dismissSplitScreen(false /* primaryOnTop */);
259     }
260 
dismissSplitScreen(boolean primaryOnTop)261     public void dismissSplitScreen(boolean primaryOnTop) {
262         dismissSplitScreen(new WindowContainerTransaction(), primaryOnTop);
263     }
264 
dismissSplitScreen(WindowContainerTransaction t, boolean primaryOnTop)265     public void dismissSplitScreen(WindowContainerTransaction t, boolean primaryOnTop) {
266         synchronized (this) {
267             NestedShellPermission.run(() -> {
268                 t.setLaunchRoot(mRootPrimary.getToken(), null, null)
269                         .setLaunchRoot(
270                                 mRootSecondary.getToken(),
271                                 null,
272                                 null)
273                         .reparentTasks(
274                                 primaryOnTop ? mRootSecondary.getToken() : mRootPrimary.getToken(),
275                                 null /* newParent */,
276                                 CONTROLLED_WINDOWING_MODES,
277                                 CONTROLLED_ACTIVITY_TYPES,
278                                 true /* onTop */)
279                         .reparentTasks(
280                                 primaryOnTop ? mRootPrimary.getToken() : mRootSecondary.getToken(),
281                                 null /* newParent */,
282                                 CONTROLLED_WINDOWING_MODES,
283                                 CONTROLLED_ACTIVITY_TYPES,
284                                 true /* onTop */);
285                 applyPreSplitTaskInfo(t, mPrePrimarySplitTaskInfo);
286                 applyPreSplitTaskInfo(t, mPreSecondarySplitTaskInfo);
287                 applyTransaction(t);
288             });
289         }
290     }
291 
applyPreSplitTaskInfo(WindowContainerTransaction t, @Nullable ActivityManager.RunningTaskInfo preSplitTaskInfo)292     private void applyPreSplitTaskInfo(WindowContainerTransaction t,
293                                        @Nullable ActivityManager.RunningTaskInfo preSplitTaskInfo) {
294         if (preSplitTaskInfo == null) {
295             return;
296         }
297         final int restoreWindowingMode =
298                 preSplitTaskInfo.getConfiguration().windowConfiguration.getWindowingMode();
299         // TODO(b/215556258): We should also consider restoring WINDOWING_MODE_FREEFORM and its
300         //  bounds. For now, restoring freeform when dismissing split seems to mess up activity
301         //  lifecycle especially with shell transitions enabled.
302         if (restoreWindowingMode == WINDOWING_MODE_FULLSCREEN) {
303             t.setWindowingMode(preSplitTaskInfo.getToken(), restoreWindowingMode);
304             t.setBounds(preSplitTaskInfo.getToken(), null);
305         }
306     }
307 
setRootPrimaryTaskBounds(Rect bounds)308     public void setRootPrimaryTaskBounds(Rect bounds) {
309         setTaskBounds(mRootPrimary.getToken(), bounds);
310     }
311 
setRootSecondaryTaskBounds(Rect bounds)312     void setRootSecondaryTaskBounds(Rect bounds) {
313         setTaskBounds(mRootSecondary.getToken(), bounds);
314     }
315 
getPrimaryTaskBounds()316     public Rect getPrimaryTaskBounds() {
317         return mPrimaryBounds;
318     }
319 
getSecondaryTaskBounds()320     public Rect getSecondaryTaskBounds() {
321         return mSecondaryBounds;
322     }
323 
setTaskBounds(WindowContainerToken container, Rect bounds)324     private void setTaskBounds(WindowContainerToken container, Rect bounds) {
325         synchronized (this) {
326             NestedShellPermission.run(() -> {
327                 final WindowContainerTransaction t = new WindowContainerTransaction()
328                         .setBounds(container, bounds);
329                 applyTransaction(t);
330             });
331         }
332     }
333 
getPrimarySplitTaskCount()334     public int getPrimarySplitTaskCount() {
335         return mPrimaryChildrenTaskIds.size();
336     }
337 
getSecondarySplitTaskCount()338     public int getSecondarySplitTaskCount() {
339         return mSecondaryChildrenTaskIds.size();
340     }
341 
getPrimarySplitTaskId()342     public int getPrimarySplitTaskId() {
343         return mRootPrimary != null ? mRootPrimary.taskId : INVALID_TASK_ID;
344     }
345 
getSecondarySplitTaskId()346     public int getSecondarySplitTaskId() {
347         return mRootSecondary != null ? mRootSecondary.taskId : INVALID_TASK_ID;
348     }
349 
getTaskInfo(int taskId)350     public ActivityManager.RunningTaskInfo getTaskInfo(int taskId) {
351         synchronized (this) {
352             ActivityManager.RunningTaskInfo taskInfo = mKnownTasks.get(taskId);
353             if (taskInfo != null) return taskInfo;
354 
355             final List<ActivityManager.RunningTaskInfo> rootTasks = getRootTasks(DEFAULT_DISPLAY,
356                     null);
357             for (ActivityManager.RunningTaskInfo info : rootTasks) {
358                 addTask(info);
359             }
360 
361             return mKnownTasks.get(taskId);
362         }
363     }
364 
365     /** Waits and asserts that given Task to appear. */
waitForAndAssertTaskAppeared(int taskId)366     public void waitForAndAssertTaskAppeared(int taskId) {
367         synchronized (this) {
368             waitForAndAssert(o -> mKnownTasks.containsKey(taskId), "Can't find task=" + taskId);
369         }
370     }
371 
372     @Override
onTaskAppeared( @onNull ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash)373     public void onTaskAppeared(
374             @NonNull ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
375         synchronized (this) {
376             notifyOnEnd(() -> {
377                 SurfaceControl.Transaction t = new SurfaceControl.Transaction();
378                 t.setVisibility(leash, true /* visible */);
379                 addTask(taskInfo, leash, t);
380                 t.apply();
381             });
382         }
383     }
384 
385     @Override
onTaskVanished(@onNull ActivityManager.RunningTaskInfo taskInfo)386     public void onTaskVanished(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
387         synchronized (this) {
388             removeTask(taskInfo);
389         }
390     }
391 
392     @Override
onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo)393     public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
394         synchronized (this) {
395             notifyOnEnd(() -> {
396                 SurfaceControl.Transaction t = new SurfaceControl.Transaction();
397                 addTask(taskInfo, null /* leash */, t);
398                 t.apply();
399             });
400         }
401     }
402 
addTask(ActivityManager.RunningTaskInfo taskInfo)403     private void addTask(ActivityManager.RunningTaskInfo taskInfo) {
404         addTask(taskInfo, null /* SurfaceControl */, null /* Transaction */);
405     }
406 
addTask(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, SurfaceControl.Transaction t)407     private void addTask(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash,
408             SurfaceControl.Transaction t) {
409         mKnownTasks.put(taskInfo.taskId, taskInfo);
410         if (leash != null) {
411             mTaskLeashes.put(taskInfo.taskId, leash);
412         } else {
413             leash = mTaskLeashes.get(taskInfo.taskId);
414         }
415         if (taskInfo.hasParentTask()) {
416             Rect sourceCrop = null;
417             if (mRootPrimary != null
418                     && mRootPrimary.taskId == taskInfo.getParentTaskId()) {
419                 sourceCrop = new Rect(mPrimaryBounds);
420                 mPrimaryChildrenTaskIds.add(taskInfo.taskId);
421             } else if (mRootSecondary != null
422                     && mRootSecondary.taskId == taskInfo.getParentTaskId()) {
423                 sourceCrop = new Rect(mSecondaryBounds);
424                 mSecondaryChildrenTaskIds.add(taskInfo.taskId);
425             }
426             if (t != null && leash != null && sourceCrop != null) {
427                 sourceCrop.offsetTo(0, 0);
428                 t.setGeometry(leash, sourceCrop, sourceCrop, Surface.ROTATION_0);
429             }
430             return;
431         }
432 
433         if (mRootPrimary == null
434                 && mPrimaryCookie != null
435                 && taskInfo.containsLaunchCookie(mPrimaryCookie)) {
436             mRootPrimary = taskInfo;
437             if (t != null && leash != null) {
438                 Rect sourceCrop = new Rect(mPrimaryBounds);
439                 sourceCrop.offsetTo(0, 0);
440                 t.setGeometry(leash, sourceCrop, mPrimaryBounds, Surface.ROTATION_0);
441             }
442             return;
443         }
444 
445         if (mRootSecondary == null
446                 && mSecondaryCookie != null
447                 && taskInfo.containsLaunchCookie(mSecondaryCookie)) {
448             mRootSecondary = taskInfo;
449             if (t != null && leash != null) {
450                 Rect sourceCrop = new Rect(mSecondaryBounds);
451                 sourceCrop.offsetTo(0, 0);
452                 t.setGeometry(leash, sourceCrop, mSecondaryBounds, Surface.ROTATION_0);
453             }
454             return;
455         }
456 
457         if (t == null || leash == null) {
458             return;
459         }
460         WindowConfiguration config = taskInfo.getConfiguration().windowConfiguration;
461         Rect bounds = config.getBounds();
462         Rect sourceCrop = null;
463         if (config.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
464             sourceCrop = new Rect(bounds);
465             sourceCrop.offsetTo(0, 0);
466         }
467         t.setGeometry(leash, sourceCrop, bounds, Surface.ROTATION_0);
468     }
469 
removeTask(ActivityManager.RunningTaskInfo taskInfo)470     private void removeTask(ActivityManager.RunningTaskInfo taskInfo) {
471         final int taskId = taskInfo.taskId;
472         // ignores cleanup on duplicated removal request
473         if (mKnownTasks.remove(taskId) == null) {
474             return;
475         }
476         mTaskLeashes.remove(taskId);
477         mPrimaryChildrenTaskIds.remove(taskId);
478         mSecondaryChildrenTaskIds.remove(taskId);
479 
480         if ((mRootPrimary != null && taskId == mRootPrimary.taskId)
481                 || (mRootSecondary != null && taskId == mRootSecondary.taskId)) {
482             unregisterOrganizerIfNeeded();
483         }
484     }
485 }
486