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 androidx.window.extensions.embedding;
18 
19 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
20 import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT;
21 import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS;
22 import static android.window.TaskFragmentOperation.OP_TYPE_SET_DIM_ON_TASK;
23 import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION;
24 import static android.window.TaskFragmentOperation.OP_TYPE_SET_PINNED;
25 
26 import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior;
27 import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior;
28 import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked;
29 import static androidx.window.extensions.embedding.SplitContainer.shouldFinishPrimaryWithSecondary;
30 import static androidx.window.extensions.embedding.SplitContainer.shouldFinishSecondaryWithPrimary;
31 
32 import android.app.Activity;
33 import android.app.WindowConfiguration.WindowingMode;
34 import android.content.Intent;
35 import android.graphics.Rect;
36 import android.os.Bundle;
37 import android.os.IBinder;
38 import android.util.ArrayMap;
39 import android.window.TaskFragmentAnimationParams;
40 import android.window.TaskFragmentCreationParams;
41 import android.window.TaskFragmentInfo;
42 import android.window.TaskFragmentOperation;
43 import android.window.TaskFragmentOrganizer;
44 import android.window.TaskFragmentTransaction;
45 import android.window.WindowContainerTransaction;
46 
47 import androidx.annotation.NonNull;
48 import androidx.annotation.Nullable;
49 
50 import com.android.internal.annotations.VisibleForTesting;
51 
52 import java.util.Map;
53 import java.util.concurrent.Executor;
54 
55 /**
56  * Platform default Extensions implementation of {@link TaskFragmentOrganizer} to organize
57  * task fragments.
58  *
59  * All calls into methods of this class are expected to be on the UI thread.
60  */
61 class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
62 
63     /** Mapping from the client assigned unique token to the {@link TaskFragmentInfo}. */
64     @VisibleForTesting
65     final Map<IBinder, TaskFragmentInfo> mFragmentInfos = new ArrayMap<>();
66 
67     @NonNull
68     private final TaskFragmentCallback mCallback;
69 
70     @VisibleForTesting
71     @Nullable
72     TaskFragmentAnimationController mAnimationController;
73 
74     /**
75      * Callback that notifies the controller about changes to task fragments.
76      */
77     interface TaskFragmentCallback {
onTransactionReady(@onNull TaskFragmentTransaction transaction)78         void onTransactionReady(@NonNull TaskFragmentTransaction transaction);
79     }
80 
81     /**
82      * @param executor  callbacks from WM Core are posted on this executor. It should be tied to the
83      *                  UI thread that all other calls into methods of this class are also on.
84      */
JetpackTaskFragmentOrganizer(@onNull Executor executor, @NonNull TaskFragmentCallback callback)85     JetpackTaskFragmentOrganizer(@NonNull Executor executor,
86             @NonNull TaskFragmentCallback callback) {
87         super(executor);
88         mCallback = callback;
89     }
90 
91     @Override
unregisterOrganizer()92     public void unregisterOrganizer() {
93         if (mAnimationController != null) {
94             mAnimationController.unregisterRemoteAnimations();
95             mAnimationController = null;
96         }
97         super.unregisterOrganizer();
98     }
99 
100     /**
101      * Overrides the animation for transitions of embedded activities organized by this organizer.
102      */
overrideSplitAnimation()103     void overrideSplitAnimation() {
104         if (mAnimationController == null) {
105             mAnimationController = new TaskFragmentAnimationController(this);
106         }
107         mAnimationController.registerRemoteAnimations();
108     }
109 
110     /**
111      * Starts a new Activity and puts it into split with an existing Activity side-by-side.
112      * @param launchingFragmentToken    token for the launching TaskFragment. If it exists, it will
113      *                                  be resized based on {@param launchingFragmentBounds}.
114      *                                  Otherwise, we will create a new TaskFragment with the given
115      *                                  token for the {@param launchingActivity}.
116      * @param launchingRelBounds    the initial relative bounds for the launching TaskFragment.
117      * @param launchingActivity the Activity to put on the left hand side of the split as the
118      *                          primary.
119      * @param secondaryFragmentToken    token to create the secondary TaskFragment with.
120      * @param secondaryRelBounds    the initial relative bounds for the secondary TaskFragment
121      * @param activityIntent    Intent to start the secondary Activity with.
122      * @param activityOptions   ActivityOptions to start the secondary Activity with.
123      * @param windowingMode     the windowing mode to set for the TaskFragments.
124      * @param splitAttributes   the {@link SplitAttributes} to represent the split.
125      */
startActivityToSide(@onNull WindowContainerTransaction wct, @NonNull IBinder launchingFragmentToken, @NonNull Rect launchingRelBounds, @NonNull Activity launchingActivity, @NonNull IBinder secondaryFragmentToken, @NonNull Rect secondaryRelBounds, @NonNull Intent activityIntent, @Nullable Bundle activityOptions, @NonNull SplitRule rule, @WindowingMode int windowingMode, @NonNull SplitAttributes splitAttributes)126     void startActivityToSide(@NonNull WindowContainerTransaction wct,
127             @NonNull IBinder launchingFragmentToken, @NonNull Rect launchingRelBounds,
128             @NonNull Activity launchingActivity, @NonNull IBinder secondaryFragmentToken,
129             @NonNull Rect secondaryRelBounds, @NonNull Intent activityIntent,
130             @Nullable Bundle activityOptions, @NonNull SplitRule rule,
131             @WindowingMode int windowingMode, @NonNull SplitAttributes splitAttributes) {
132         final IBinder ownerToken = launchingActivity.getActivityToken();
133 
134         // Create or resize the launching TaskFragment.
135         if (mFragmentInfos.containsKey(launchingFragmentToken)) {
136             resizeTaskFragment(wct, launchingFragmentToken, launchingRelBounds);
137             updateWindowingMode(wct, launchingFragmentToken, windowingMode);
138         } else {
139             createTaskFragmentAndReparentActivity(wct, launchingFragmentToken, ownerToken,
140                     launchingRelBounds, windowingMode, launchingActivity);
141         }
142         updateAnimationParams(wct, launchingFragmentToken, splitAttributes);
143 
144         // Create a TaskFragment for the secondary activity.
145         final TaskFragmentCreationParams fragmentOptions = new TaskFragmentCreationParams.Builder(
146                 getOrganizerToken(), secondaryFragmentToken, ownerToken)
147                 .setInitialRelativeBounds(secondaryRelBounds)
148                 .setWindowingMode(windowingMode)
149                 // Make sure to set the paired fragment token so that the new TaskFragment will be
150                 // positioned right above the paired TaskFragment.
151                 // This is needed in case we need to launch a placeholder Activity to split below a
152                 // transparent always-expand Activity.
153                 .setPairedPrimaryFragmentToken(launchingFragmentToken)
154                 .build();
155         createTaskFragment(wct, fragmentOptions);
156         updateAnimationParams(wct, secondaryFragmentToken, splitAttributes);
157         wct.startActivityInTaskFragment(secondaryFragmentToken, ownerToken, activityIntent,
158                 activityOptions);
159 
160         // Set adjacent to each other so that the containers below will be invisible.
161         setAdjacentTaskFragmentsWithRule(wct, launchingFragmentToken, secondaryFragmentToken, rule);
162         setCompanionTaskFragment(wct, launchingFragmentToken, secondaryFragmentToken, rule,
163                 false /* isStacked */);
164     }
165 
166     /**
167      * Expands an existing TaskFragment to fill parent.
168      * @param wct WindowContainerTransaction in which the task fragment should be resized.
169      * @param container the {@link TaskFragmentContainer} to be expanded.
170      */
expandTaskFragment(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container)171     void expandTaskFragment(@NonNull WindowContainerTransaction wct,
172             @NonNull TaskFragmentContainer container) {
173         final IBinder fragmentToken = container.getTaskFragmentToken();
174         resizeTaskFragment(wct, fragmentToken, new Rect());
175         clearAdjacentTaskFragments(wct, fragmentToken);
176         updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED);
177         updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT);
178     }
179 
180     /**
181      * Expands an Activity to fill parent by moving it to a new TaskFragment.
182      * @param fragmentToken token to create new TaskFragment with.
183      * @param activity      activity to move to the fill-parent TaskFragment.
184      */
expandActivity(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @NonNull Activity activity)185     void expandActivity(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
186             @NonNull Activity activity) {
187         createTaskFragmentAndReparentActivity(
188                 wct, fragmentToken, activity.getActivityToken(), new Rect(),
189                 WINDOWING_MODE_UNDEFINED, activity);
190         updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT);
191     }
192 
193     /**
194      * @param ownerToken The token of the activity that creates this task fragment. It does not
195      *                   have to be a child of this task fragment, but must belong to the same task.
196      */
createTaskFragment(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect relBounds, @WindowingMode int windowingMode)197     void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
198             @NonNull IBinder ownerToken, @NonNull Rect relBounds,
199             @WindowingMode int windowingMode) {
200         createTaskFragment(wct, fragmentToken, ownerToken, relBounds, windowingMode,
201                 null /* pairedActivityToken */);
202     }
203 
204     /**
205      * @param ownerToken The token of the activity that creates this task fragment. It does not
206      *                   have to be a child of this task fragment, but must belong to the same task.
207      * @param pairedActivityToken The token of the activity that will be reparented to this task
208      *                            fragment. When it is not {@code null}, the task fragment will be
209      *                            positioned right above it.
210      */
createTaskFragment(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect relBounds, @WindowingMode int windowingMode, @Nullable IBinder pairedActivityToken)211     void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
212             @NonNull IBinder ownerToken, @NonNull Rect relBounds, @WindowingMode int windowingMode,
213             @Nullable IBinder pairedActivityToken) {
214         final TaskFragmentCreationParams fragmentOptions = new TaskFragmentCreationParams.Builder(
215                 getOrganizerToken(), fragmentToken, ownerToken)
216                 .setInitialRelativeBounds(relBounds)
217                 .setWindowingMode(windowingMode)
218                 .setPairedActivityToken(pairedActivityToken)
219                 .build();
220         createTaskFragment(wct, fragmentOptions);
221     }
222 
createTaskFragment(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentCreationParams fragmentOptions)223     void createTaskFragment(@NonNull WindowContainerTransaction wct,
224             @NonNull TaskFragmentCreationParams fragmentOptions) {
225         if (mFragmentInfos.containsKey(fragmentOptions.getFragmentToken())) {
226             throw new IllegalArgumentException(
227                     "There is an existing TaskFragment with fragmentToken="
228                             + fragmentOptions.getFragmentToken());
229         }
230         wct.createTaskFragment(fragmentOptions);
231     }
232 
233     /**
234      * @param ownerToken The token of the activity that creates this task fragment. It does not
235      *                   have to be a child of this task fragment, but must belong to the same task.
236      */
createTaskFragmentAndReparentActivity(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect relBounds, @WindowingMode int windowingMode, @NonNull Activity activity)237     private void createTaskFragmentAndReparentActivity(@NonNull WindowContainerTransaction wct,
238             @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect relBounds,
239             @WindowingMode int windowingMode, @NonNull Activity activity) {
240         final IBinder reparentActivityToken = activity.getActivityToken();
241         createTaskFragment(wct, fragmentToken, ownerToken, relBounds, windowingMode,
242                 reparentActivityToken);
243         wct.reparentActivityToTaskFragment(fragmentToken, reparentActivityToken);
244     }
245 
246     /**
247      * Sets the two given TaskFragments as adjacent to each other with respecting the given
248      * {@link SplitRule} for {@link WindowContainerTransaction.TaskFragmentAdjacentParams}.
249      */
setAdjacentTaskFragmentsWithRule(@onNull WindowContainerTransaction wct, @NonNull IBinder primary, @NonNull IBinder secondary, @NonNull SplitRule splitRule)250     void setAdjacentTaskFragmentsWithRule(@NonNull WindowContainerTransaction wct,
251             @NonNull IBinder primary, @NonNull IBinder secondary, @NonNull SplitRule splitRule) {
252         WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams = null;
253         final boolean finishSecondaryWithPrimary =
254                 SplitContainer.shouldFinishSecondaryWithPrimary(splitRule);
255         final boolean finishPrimaryWithSecondary =
256                 SplitContainer.shouldFinishPrimaryWithSecondary(splitRule);
257         if (finishSecondaryWithPrimary || finishPrimaryWithSecondary) {
258             adjacentParams = new WindowContainerTransaction.TaskFragmentAdjacentParams();
259             adjacentParams.setShouldDelayPrimaryLastActivityRemoval(finishSecondaryWithPrimary);
260             adjacentParams.setShouldDelaySecondaryLastActivityRemoval(finishPrimaryWithSecondary);
261         }
262         setAdjacentTaskFragments(wct, primary, secondary, adjacentParams);
263     }
264 
setAdjacentTaskFragments(@onNull WindowContainerTransaction wct, @NonNull IBinder primary, @NonNull IBinder secondary, @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams)265     void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
266             @NonNull IBinder primary, @NonNull IBinder secondary,
267             @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams) {
268         wct.setAdjacentTaskFragments(primary, secondary, adjacentParams);
269     }
270 
clearAdjacentTaskFragments(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken)271     void clearAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
272             @NonNull IBinder fragmentToken) {
273         // Clear primary will also clear secondary.
274         wct.clearAdjacentTaskFragments(fragmentToken);
275     }
276 
setCompanionTaskFragment(@onNull WindowContainerTransaction wct, @NonNull IBinder primary, @NonNull IBinder secondary, @NonNull SplitRule splitRule, boolean isStacked)277     void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct,
278             @NonNull IBinder primary, @NonNull IBinder secondary, @NonNull SplitRule splitRule,
279             boolean isStacked) {
280         final boolean finishPrimaryWithSecondary;
281         if (isStacked) {
282             finishPrimaryWithSecondary = shouldFinishAssociatedContainerWhenStacked(
283                     getFinishPrimaryWithSecondaryBehavior(splitRule));
284         } else {
285             finishPrimaryWithSecondary = shouldFinishPrimaryWithSecondary(splitRule);
286         }
287         setCompanionTaskFragment(wct, primary, finishPrimaryWithSecondary ? secondary : null);
288 
289         final boolean finishSecondaryWithPrimary;
290         if (isStacked) {
291             finishSecondaryWithPrimary = shouldFinishAssociatedContainerWhenStacked(
292                     getFinishSecondaryWithPrimaryBehavior(splitRule));
293         } else {
294             finishSecondaryWithPrimary = shouldFinishSecondaryWithPrimary(splitRule);
295         }
296         setCompanionTaskFragment(wct, secondary, finishSecondaryWithPrimary ? primary : null);
297     }
298 
setCompanionTaskFragment(@onNull WindowContainerTransaction wct, @NonNull IBinder primary, @Nullable IBinder secondary)299     void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder primary,
300             @Nullable IBinder secondary) {
301         wct.setCompanionTaskFragment(primary, secondary);
302     }
303 
resizeTaskFragment(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @Nullable Rect relBounds)304     void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
305             @Nullable Rect relBounds) {
306         if (!mFragmentInfos.containsKey(fragmentToken)) {
307             throw new IllegalArgumentException(
308                     "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken);
309         }
310         if (relBounds == null) {
311             relBounds = new Rect();
312         }
313         wct.setRelativeBounds(mFragmentInfos.get(fragmentToken).getToken(), relBounds);
314     }
315 
updateWindowingMode(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @WindowingMode int windowingMode)316     void updateWindowingMode(@NonNull WindowContainerTransaction wct,
317             @NonNull IBinder fragmentToken, @WindowingMode int windowingMode) {
318         if (!mFragmentInfos.containsKey(fragmentToken)) {
319             throw new IllegalArgumentException(
320                     "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken);
321         }
322         wct.setWindowingMode(mFragmentInfos.get(fragmentToken).getToken(), windowingMode);
323     }
324 
325     /**
326      * Updates the {@link TaskFragmentAnimationParams} for the given TaskFragment based on
327      * {@link SplitAttributes}.
328      */
updateAnimationParams(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @NonNull SplitAttributes splitAttributes)329     void updateAnimationParams(@NonNull WindowContainerTransaction wct,
330             @NonNull IBinder fragmentToken, @NonNull SplitAttributes splitAttributes) {
331         updateAnimationParams(wct, fragmentToken, createAnimationParamsOrDefault(splitAttributes));
332     }
333 
updateAnimationParams(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @NonNull TaskFragmentAnimationParams animationParams)334     void updateAnimationParams(@NonNull WindowContainerTransaction wct,
335             @NonNull IBinder fragmentToken, @NonNull TaskFragmentAnimationParams animationParams) {
336         final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
337                 OP_TYPE_SET_ANIMATION_PARAMS)
338                 .setAnimationParams(animationParams)
339                 .build();
340         wct.addTaskFragmentOperation(fragmentToken, operation);
341     }
342 
deleteTaskFragment(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken)343     void deleteTaskFragment(@NonNull WindowContainerTransaction wct,
344             @NonNull IBinder fragmentToken) {
345         wct.deleteTaskFragment(fragmentToken);
346     }
347 
reorderTaskFragmentToFront(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken)348     void reorderTaskFragmentToFront(@NonNull WindowContainerTransaction wct,
349             @NonNull IBinder fragmentToken) {
350         final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
351                 OP_TYPE_REORDER_TO_FRONT).build();
352         wct.addTaskFragmentOperation(fragmentToken, operation);
353     }
354 
setTaskFragmentIsolatedNavigation(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, boolean isolatedNav)355     void setTaskFragmentIsolatedNavigation(@NonNull WindowContainerTransaction wct,
356             @NonNull IBinder fragmentToken, boolean isolatedNav) {
357         final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
358                 OP_TYPE_SET_ISOLATED_NAVIGATION).setBooleanValue(isolatedNav).build();
359         wct.addTaskFragmentOperation(fragmentToken, operation);
360     }
361 
setTaskFragmentPinned(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, boolean pinned)362     void setTaskFragmentPinned(@NonNull WindowContainerTransaction wct,
363             @NonNull IBinder fragmentToken, boolean pinned) {
364         final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
365                 OP_TYPE_SET_PINNED).setBooleanValue(pinned).build();
366         wct.addTaskFragmentOperation(fragmentToken, operation);
367     }
368 
setTaskFragmentDimOnTask(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, boolean dimOnTask)369     void setTaskFragmentDimOnTask(@NonNull WindowContainerTransaction wct,
370             @NonNull IBinder fragmentToken, boolean dimOnTask) {
371         final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
372                 OP_TYPE_SET_DIM_ON_TASK).setBooleanValue(dimOnTask).build();
373         wct.addTaskFragmentOperation(fragmentToken, operation);
374     }
375 
updateTaskFragmentInfo(@onNull TaskFragmentInfo taskFragmentInfo)376     void updateTaskFragmentInfo(@NonNull TaskFragmentInfo taskFragmentInfo) {
377         mFragmentInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo);
378     }
379 
removeTaskFragmentInfo(@onNull TaskFragmentInfo taskFragmentInfo)380     void removeTaskFragmentInfo(@NonNull TaskFragmentInfo taskFragmentInfo) {
381         mFragmentInfos.remove(taskFragmentInfo.getFragmentToken());
382     }
383 
384     @Override
onTransactionReady(@onNull TaskFragmentTransaction transaction)385     public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) {
386         mCallback.onTransactionReady(transaction);
387     }
388 
createAnimationParamsOrDefault( @ullable SplitAttributes splitAttributes)389     private static TaskFragmentAnimationParams createAnimationParamsOrDefault(
390             @Nullable SplitAttributes splitAttributes) {
391         if (splitAttributes == null) {
392             return TaskFragmentAnimationParams.DEFAULT;
393         }
394         final AnimationBackground animationBackground = splitAttributes.getAnimationBackground();
395         if (animationBackground instanceof AnimationBackground.ColorBackground colorBackground) {
396             return new TaskFragmentAnimationParams.Builder()
397                     .setAnimationBackgroundColor(colorBackground.getColor())
398                     .build();
399         } else {
400             return TaskFragmentAnimationParams.DEFAULT;
401         }
402     }
403 }
404