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