• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.ActivityTaskManager.INVALID_STACK_ID;
20 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
21 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
22 import static android.server.wm.WindowManagerState.STATE_RESUMED;
23 import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN;
24 import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
25 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
26 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR;
27 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED;
28 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED;
29 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED;
30 
31 import static com.google.common.truth.Truth.assertThat;
32 import static com.google.common.truth.Truth.assertWithMessage;
33 
34 import static org.junit.Assert.fail;
35 import static org.junit.Assume.assumeTrue;
36 
37 import android.app.Activity;
38 import android.app.Instrumentation;
39 import android.content.ComponentName;
40 import android.content.Intent;
41 import android.content.res.Configuration;
42 import android.graphics.Rect;
43 import android.os.Binder;
44 import android.os.Bundle;
45 import android.os.IBinder;
46 import android.server.wm.WindowContextTests.TestActivity;
47 import android.server.wm.WindowManagerState.WindowContainer;
48 import android.util.ArrayMap;
49 import android.util.Log;
50 import android.window.TaskFragmentCreationParams;
51 import android.window.TaskFragmentInfo;
52 import android.window.TaskFragmentOrganizer;
53 import android.window.TaskFragmentTransaction;
54 import android.window.WindowContainerTransaction;
55 
56 import androidx.annotation.NonNull;
57 import androidx.annotation.Nullable;
58 import androidx.test.InstrumentationRegistry;
59 
60 import org.junit.After;
61 import org.junit.Before;
62 
63 import java.util.List;
64 import java.util.Map;
65 import java.util.concurrent.CountDownLatch;
66 import java.util.concurrent.TimeUnit;
67 import java.util.function.Predicate;
68 
69 import javax.annotation.concurrent.GuardedBy;
70 
71 public class TaskFragmentOrganizerTestBase extends WindowManagerTestBase {
72     private static final String TAG = "TaskFragmentOrganizerTestBase";
73 
74     public BasicTaskFragmentOrganizer mTaskFragmentOrganizer;
75     Activity mOwnerActivity;
76     IBinder mOwnerToken;
77     ComponentName mOwnerActivityName;
78     int mOwnerTaskId;
79 
80     @Before
81     @Override
setUp()82     public void setUp() throws Exception {
83         super.setUp();
84         assumeTrue(supportsMultiWindow());
85         mTaskFragmentOrganizer = new BasicTaskFragmentOrganizer();
86         mTaskFragmentOrganizer.registerOrganizer();
87         mOwnerActivity = setUpOwnerActivity();
88         mOwnerToken = getActivityToken(mOwnerActivity);
89         mOwnerActivityName = mOwnerActivity.getComponentName();
90         mOwnerTaskId = mOwnerActivity.getTaskId();
91         // Make sure the activity is launched and resumed, otherwise the window state may not be
92         // stable.
93         waitAndAssertResumedActivity(mOwnerActivity.getComponentName(),
94                 "The owner activity must be resumed.");
95     }
96 
97     /** Setups the owner activity of the organized TaskFragment. */
setUpOwnerActivity()98     Activity setUpOwnerActivity() {
99         // Launch activities in fullscreen in case the device may use freeform as the default
100         // windowing mode.
101         return startActivityInWindowingModeFullScreen(TestActivity.class);
102     }
103 
104     @After
tearDown()105     public void tearDown() {
106         if (mTaskFragmentOrganizer != null) {
107             mTaskFragmentOrganizer.unregisterOrganizer();
108         }
109     }
110 
getActivityToken(@onNull Activity activity)111     public static IBinder getActivityToken(@NonNull Activity activity) {
112         return activity.getWindow().getAttributes().token;
113     }
114 
assertEmptyTaskFragment(TaskFragmentInfo info, IBinder expectedTaskFragToken)115     public static void assertEmptyTaskFragment(TaskFragmentInfo info,
116             IBinder expectedTaskFragToken) {
117         assertTaskFragmentInfoValidity(info, expectedTaskFragToken);
118         assertWithMessage("TaskFragment must be empty").that(info.isEmpty()).isTrue();
119         assertWithMessage("TaskFragmentInfo#getActivities must be empty")
120                 .that(info.getActivities()).isEmpty();
121         assertWithMessage("TaskFragment must not contain any running Activity")
122                 .that(info.hasRunningActivity()).isFalse();
123         assertWithMessage("TaskFragment must not be visible").that(info.isVisible()).isFalse();
124     }
125 
assertNotEmptyTaskFragment(TaskFragmentInfo info, IBinder expectedTaskFragToken, @Nullable IBinder ... expectedActivityTokens)126     public static void assertNotEmptyTaskFragment(TaskFragmentInfo info,
127             IBinder expectedTaskFragToken, @Nullable IBinder ... expectedActivityTokens) {
128         assertTaskFragmentInfoValidity(info, expectedTaskFragToken);
129         assertWithMessage("TaskFragment must not be empty").that(info.isEmpty()).isFalse();
130         assertWithMessage("TaskFragment must contain running Activity")
131                 .that(info.hasRunningActivity()).isTrue();
132         if (expectedActivityTokens != null) {
133             assertWithMessage("TaskFragmentInfo#getActivities must be empty")
134                     .that(info.getActivities()).containsAtLeastElementsIn(expectedActivityTokens);
135         }
136     }
137 
assertTaskFragmentInfoValidity(TaskFragmentInfo info, IBinder expectedTaskFragToken)138     private static void assertTaskFragmentInfoValidity(TaskFragmentInfo info,
139             IBinder expectedTaskFragToken) {
140         assertWithMessage("TaskFragmentToken must match the token from "
141                 + "TaskFragmentCreationParams#getFragmentToken")
142                 .that(info.getFragmentToken()).isEqualTo(expectedTaskFragToken);
143         assertWithMessage("WindowContainerToken must not be null")
144                 .that(info.getToken()).isNotNull();
145         assertWithMessage("TaskFragmentInfo#getPositionInParent must not be null")
146                 .that(info.getPositionInParent()).isNotNull();
147         assertWithMessage("Configuration must not be empty")
148                 .that(info.getConfiguration()).isNotEqualTo(new Configuration());
149     }
150 
151     /**
152      * Verifies whether the window hierarchy is as expected or not.
153      * <p>
154      * The sample usage is as follows:
155      * <pre class="prettyprint">
156      * assertWindowHierarchy(rootTask, leafTask, taskFragment, activity);
157      * </pre></p>
158      *
159      * @param containers The containers to be verified. It should be put from top to down
160      */
assertWindowHierarchy(WindowContainer... containers)161     public static void assertWindowHierarchy(WindowContainer... containers) {
162         for (int i = 0; i < containers.length - 2; i++) {
163             final WindowContainer parent = containers[i];
164             final WindowContainer child = containers[i + 1];
165             assertWithMessage(parent + " must contains " + child)
166                     .that(parent.mChildren).contains(child);
167         }
168     }
169 
170     /**
171      * Builds, runs and waits for completion of task fragment creation transaction.
172      * @param componentName name of the activity to launch in the TF, or {@code null} if none.
173      * @return token of the created task fragment.
174      */
createTaskFragment(@ullable ComponentName componentName)175     TaskFragmentInfo createTaskFragment(@Nullable ComponentName componentName) {
176         return createTaskFragment(componentName, new Rect());
177     }
178 
179     /**
180      * Same as {@link #createTaskFragment(ComponentName)}, but allows to specify the bounds for the
181      * new task fragment.
182      */
createTaskFragment(@ullable ComponentName componentName, @NonNull Rect relativeBounds)183     TaskFragmentInfo createTaskFragment(@Nullable ComponentName componentName,
184             @NonNull Rect relativeBounds) {
185         return createTaskFragment(componentName, relativeBounds, new WindowContainerTransaction());
186     }
187 
188     /**
189      * Same as {@link #createTaskFragment(ComponentName, Rect)}, but allows to specify the
190      * {@link WindowContainerTransaction} to use.
191      */
createTaskFragment(@ullable ComponentName componentName, @NonNull Rect relativeBounds, @NonNull WindowContainerTransaction wct)192     TaskFragmentInfo createTaskFragment(@Nullable ComponentName componentName,
193             @NonNull Rect relativeBounds, @NonNull WindowContainerTransaction wct) {
194         final TaskFragmentCreationParams params = generateTaskFragCreationParams(relativeBounds);
195         final IBinder taskFragToken = params.getFragmentToken();
196         wct.createTaskFragment(params);
197         if (componentName != null) {
198             wct.startActivityInTaskFragment(taskFragToken, mOwnerToken,
199                     new Intent().setComponent(componentName), null /* activityOptions */);
200         }
201         mTaskFragmentOrganizer.applyTransaction(wct, TASK_FRAGMENT_TRANSIT_OPEN,
202                 false /* shouldApplyIndependently */);
203         mTaskFragmentOrganizer.waitForTaskFragmentCreated();
204 
205         if (componentName != null) {
206             mWmState.waitForActivityState(componentName, STATE_RESUMED);
207         }
208 
209         return mTaskFragmentOrganizer.getTaskFragmentInfo(taskFragToken);
210     }
211 
212     @NonNull
generateTaskFragCreationParams()213     TaskFragmentCreationParams generateTaskFragCreationParams() {
214         return mTaskFragmentOrganizer.generateTaskFragParams(mOwnerToken);
215     }
216 
217     @NonNull
generateTaskFragCreationParams(@onNull Rect relativeBounds)218     TaskFragmentCreationParams generateTaskFragCreationParams(@NonNull Rect relativeBounds) {
219         return mTaskFragmentOrganizer.generateTaskFragParams(mOwnerToken, relativeBounds,
220                 WINDOWING_MODE_UNDEFINED);
221     }
222 
startNewActivity()223     static Activity startNewActivity() {
224         return startNewActivity(TestActivity.class);
225     }
226 
startNewActivity(Class<?> className)227     static Activity startNewActivity(Class<?> className) {
228         final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
229         final Intent intent = new Intent(instrumentation.getTargetContext(), className)
230                 .addFlags(FLAG_ACTIVITY_NEW_TASK);
231         return instrumentation.startActivitySync(intent);
232     }
233 
234     public static class BasicTaskFragmentOrganizer extends TaskFragmentOrganizer {
235         private final static int WAIT_TIMEOUT_IN_SECOND = 10;
236 
237         private final Object mLock = new Object();
238 
239         @GuardedBy("mLock")
240         private final Map<IBinder, TaskFragmentInfo> mInfos = new ArrayMap<>();
241         @GuardedBy("mLock")
242         private final Map<IBinder, TaskFragmentInfo> mRemovedInfos = new ArrayMap<>();
243         @GuardedBy("mLock")
244         private int mParentTaskId = INVALID_STACK_ID;
245         @Nullable
246         @GuardedBy("mLock")
247         private Configuration mParentConfig;
248         @Nullable
249         @GuardedBy("mLock")
250         private IBinder mErrorToken;
251         @Nullable
252         @GuardedBy("mLock")
253         private Throwable mThrowable;
254         @GuardedBy("mLock")
255         private boolean mIsRegistered;
256 
257         private CountDownLatch mAppearedLatch = new CountDownLatch(1);
258         private CountDownLatch mChangedLatch = new CountDownLatch(1);
259         private CountDownLatch mVanishedLatch = new CountDownLatch(1);
260         private CountDownLatch mParentChangedLatch = new CountDownLatch(1);
261         private CountDownLatch mErrorLatch = new CountDownLatch(1);
262 
BasicTaskFragmentOrganizer()263         BasicTaskFragmentOrganizer() {
264             super(Runnable::run);
265         }
266 
getTaskFragmentInfo(IBinder taskFragToken)267         public TaskFragmentInfo getTaskFragmentInfo(IBinder taskFragToken) {
268             synchronized (mLock) {
269                 return mInfos.get(taskFragToken);
270             }
271         }
272 
getRemovedTaskFragmentInfo(IBinder taskFragToken)273         public TaskFragmentInfo getRemovedTaskFragmentInfo(IBinder taskFragToken) {
274             synchronized (mLock) {
275                 return mRemovedInfos.get(taskFragToken);
276             }
277         }
278 
getThrowable()279         public Throwable getThrowable() {
280             synchronized (mLock) {
281                 return mThrowable;
282             }
283         }
284 
getErrorCallbackToken()285         public IBinder getErrorCallbackToken() {
286             synchronized (mLock) {
287                 return mErrorToken;
288             }
289         }
290 
resetLatch()291         public void resetLatch() {
292             mAppearedLatch = new CountDownLatch(1);
293             mChangedLatch = new CountDownLatch(1);
294             mVanishedLatch = new CountDownLatch(1);
295             mParentChangedLatch = new CountDownLatch(1);
296             mErrorLatch = new CountDownLatch(1);
297         }
298 
299         /**
300          * Generates a {@link TaskFragmentCreationParams} with {@code ownerToken} specified.
301          *
302          * @param ownerToken The token of {@link Activity} to create a TaskFragment under its parent
303          *                   Task
304          * @return the generated {@link TaskFragmentCreationParams}
305          */
306         @NonNull
generateTaskFragParams(@onNull IBinder ownerToken)307         public TaskFragmentCreationParams generateTaskFragParams(@NonNull IBinder ownerToken) {
308             return generateTaskFragParams(ownerToken, new Rect(), WINDOWING_MODE_UNDEFINED);
309         }
310 
311         @NonNull
generateTaskFragParams(@onNull IBinder ownerToken, @NonNull Rect relativeBounds, int windowingMode)312         public TaskFragmentCreationParams generateTaskFragParams(@NonNull IBinder ownerToken,
313                 @NonNull Rect relativeBounds, int windowingMode) {
314             return generateTaskFragParams(new Binder(), ownerToken, relativeBounds, windowingMode);
315         }
316 
317         @NonNull
generateTaskFragParams(@onNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect relativeBounds, int windowingMode)318         public TaskFragmentCreationParams generateTaskFragParams(@NonNull IBinder fragmentToken,
319                 @NonNull IBinder ownerToken, @NonNull Rect relativeBounds, int windowingMode) {
320             return new TaskFragmentCreationParams.Builder(getOrganizerToken(), fragmentToken,
321                     ownerToken)
322                     .setInitialRelativeBounds(relativeBounds)
323                     .setWindowingMode(windowingMode)
324                     .build();
325         }
326 
setAppearedCount(int count)327         public void setAppearedCount(int count) {
328             mAppearedLatch = new CountDownLatch(count);
329         }
330 
waitForAndGetTaskFragmentInfo(IBinder taskFragToken, Predicate<TaskFragmentInfo> condition, String message)331         public TaskFragmentInfo waitForAndGetTaskFragmentInfo(IBinder taskFragToken,
332                 Predicate<TaskFragmentInfo> condition, String message) {
333             final TaskFragmentInfo[] info = new TaskFragmentInfo[1];
334             waitForOrFail(message, () -> {
335                 info[0] = getTaskFragmentInfo(taskFragToken);
336                 return condition.test(info[0]);
337             });
338             return info[0];
339         }
340 
waitForTaskFragmentCreated()341         public void waitForTaskFragmentCreated() {
342             try {
343                 assertThat(mAppearedLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)).isTrue();
344             } catch (InterruptedException e) {
345                 fail("Assertion failed because of" + e);
346             }
347         }
348 
waitForTaskFragmentInfoChanged()349         public void waitForTaskFragmentInfoChanged() {
350             try {
351                 assertThat(mChangedLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)).isTrue();
352             } catch (InterruptedException e) {
353                 fail("Assertion failed because of" + e);
354             }
355         }
356 
waitForTaskFragmentRemoved()357         public void waitForTaskFragmentRemoved() {
358             try {
359                 assertThat(mVanishedLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)).isTrue();
360             } catch (InterruptedException e) {
361                 fail("Assertion failed because of" + e);
362             }
363         }
364 
waitForParentConfigChanged()365         public void waitForParentConfigChanged() {
366             try {
367                 assertThat(mParentChangedLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS))
368                         .isTrue();
369             } catch (InterruptedException e) {
370                 fail("Assertion failed because of" + e);
371             }
372         }
373 
waitForTaskFragmentError()374         public void waitForTaskFragmentError() {
375             try {
376                 assertThat(mErrorLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)).isTrue();
377             } catch (InterruptedException e) {
378                 fail("Assertion failed because of" + e);
379             }
380         }
381 
382         @GuardedBy("mLock")
removeAllTaskFragments()383         private void removeAllTaskFragments() {
384             final WindowContainerTransaction wct = new WindowContainerTransaction();
385             for (TaskFragmentInfo info : mInfos.values()) {
386                 wct.deleteTaskFragment(info.getFragmentToken());
387             }
388             applyTransaction(wct, TASK_FRAGMENT_TRANSIT_CLOSE,
389                     false /* shouldApplyIndependently */);
390         }
391 
392         @Override
registerOrganizer()393         public void registerOrganizer() {
394             synchronized (mLock) {
395                 mIsRegistered = true;
396             }
397             super.registerOrganizer();
398         }
399 
400         @Override
unregisterOrganizer()401         public void unregisterOrganizer() {
402             synchronized (mLock) {
403                 mIsRegistered = false;
404                 removeAllTaskFragments();
405                 mRemovedInfos.clear();
406                 mInfos.clear();
407                 mParentTaskId = INVALID_STACK_ID;
408                 mParentConfig = null;
409                 mErrorToken = null;
410                 mThrowable = null;
411             }
412             super.unregisterOrganizer();
413         }
414 
415         @Override
onTransactionReady(@onNull TaskFragmentTransaction transaction)416         public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) {
417             synchronized (mLock) {
418                 if (!mIsRegistered) {
419                     // Ignore callback that is invoked after unregister. This can be a racing
420                     // condition before the unregister reaches the server side.
421                     return;
422                 }
423                 final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
424                 for (TaskFragmentTransaction.Change change : changes) {
425                     final int taskId = change.getTaskId();
426                     final TaskFragmentInfo info = change.getTaskFragmentInfo();
427                     switch (change.getType()) {
428                         case TYPE_TASK_FRAGMENT_APPEARED:
429                             onTaskFragmentAppeared(info);
430                             break;
431                         case TYPE_TASK_FRAGMENT_INFO_CHANGED:
432                             onTaskFragmentInfoChanged(info);
433                             break;
434                         case TYPE_TASK_FRAGMENT_VANISHED:
435                             onTaskFragmentVanished(info);
436                             break;
437                         case TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED:
438                             onTaskFragmentParentInfoChanged(taskId, change.getTaskConfiguration());
439                             break;
440                         case TYPE_TASK_FRAGMENT_ERROR:
441                             final Bundle errorBundle = change.getErrorBundle();
442                             final IBinder errorToken = change.getErrorCallbackToken();
443                             final TaskFragmentInfo errorTaskFragmentInfo =
444                                     errorBundle.getParcelable(
445                                             KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO,
446                                             TaskFragmentInfo.class);
447                             final int opType = errorBundle.getInt(KEY_ERROR_CALLBACK_OP_TYPE);
448                             final Throwable exception = errorBundle.getSerializable(
449                                     KEY_ERROR_CALLBACK_THROWABLE, Throwable.class);
450                             onTaskFragmentError(errorToken, errorTaskFragmentInfo, opType,
451                                     exception);
452                             break;
453                         case TYPE_ACTIVITY_REPARENTED_TO_TASK:
454                             onActivityReparentedToTask(
455                                     taskId,
456                                     change.getActivityIntent(),
457                                     change.getActivityToken());
458                             break;
459                         default:
460                             // Log instead of throwing exception in case we will add more types
461                             // between releases.
462                             Log.w(TAG, "Unknown TaskFragmentEvent=" + change.getType());
463                     }
464                 }
465                 onTransactionHandled(transaction.getTransactionToken(),
466                         new WindowContainerTransaction(), TASK_FRAGMENT_TRANSIT_NONE,
467                         false /* shouldApplyIndependently */);
468             }
469         }
470 
471         @GuardedBy("mLock")
onTaskFragmentAppeared(@onNull TaskFragmentInfo taskFragmentInfo)472         private void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) {
473             mInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo);
474             mAppearedLatch.countDown();
475         }
476 
477         @GuardedBy("mLock")
onTaskFragmentInfoChanged(@onNull TaskFragmentInfo taskFragmentInfo)478         private void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {
479             mInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo);
480             mChangedLatch.countDown();
481         }
482 
483         @GuardedBy("mLock")
onTaskFragmentVanished(@onNull TaskFragmentInfo taskFragmentInfo)484         private void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {
485             mInfos.remove(taskFragmentInfo.getFragmentToken());
486             mRemovedInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo);
487             mVanishedLatch.countDown();
488         }
489 
490         @GuardedBy("mLock")
onTaskFragmentParentInfoChanged(int taskId, @NonNull Configuration parentConfig)491         private void onTaskFragmentParentInfoChanged(int taskId,
492                 @NonNull Configuration parentConfig) {
493             mParentTaskId = taskId;
494             mParentConfig = parentConfig;
495             mParentChangedLatch.countDown();
496         }
497 
498         @GuardedBy("mLock")
onTaskFragmentError(@onNull IBinder errorCallbackToken, @Nullable TaskFragmentInfo taskFragmentInfo, int opType, @NonNull Throwable exception)499         private void onTaskFragmentError(@NonNull IBinder errorCallbackToken,
500                 @Nullable TaskFragmentInfo taskFragmentInfo, int opType,
501                 @NonNull Throwable exception) {
502             mErrorToken = errorCallbackToken;
503             if (taskFragmentInfo != null) {
504                 mInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo);
505             }
506             mThrowable = exception;
507             mErrorLatch.countDown();
508         }
509 
onActivityReparentedToTask(int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken)510         private void onActivityReparentedToTask(int taskId, @NonNull Intent activityIntent,
511                 @NonNull IBinder activityToken) {
512             // TODO(b/232476698) Add CTS to verify PIP behavior with ActivityEmbedding
513         }
514     }
515 }
516