1 /*
2  * Copyright (C) 2022 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.ActivityTaskManager.INVALID_TASK_ID;
20 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
21 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
22 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
23 import static android.app.WindowConfiguration.inMultiWindowMode;
24 
25 import android.app.Activity;
26 import android.app.ActivityClient;
27 import android.app.WindowConfiguration;
28 import android.app.WindowConfiguration.WindowingMode;
29 import android.content.res.Configuration;
30 import android.graphics.Rect;
31 import android.os.IBinder;
32 import android.util.ArraySet;
33 import android.util.Log;
34 import android.window.TaskFragmentInfo;
35 import android.window.TaskFragmentParentInfo;
36 import android.window.WindowContainerTransaction;
37 
38 import androidx.annotation.NonNull;
39 import androidx.annotation.Nullable;
40 
41 import java.util.ArrayList;
42 import java.util.List;
43 import java.util.Set;
44 
45 /** Represents TaskFragments and split pairs below a Task. */
46 class TaskContainer {
47     private static final String TAG = TaskContainer.class.getSimpleName();
48 
49     /** The unique task id. */
50     private final int mTaskId;
51 
52     /** Active TaskFragments in this Task. */
53     @NonNull
54     private final List<TaskFragmentContainer> mContainers = new ArrayList<>();
55 
56     /** Active split pairs in this Task. */
57     @NonNull
58     private final List<SplitContainer> mSplitContainers = new ArrayList<>();
59 
60     /** Active pin split pair in this Task. */
61     @Nullable
62     private SplitPinContainer mSplitPinContainer;
63 
64     @NonNull
65     private final Configuration mConfiguration;
66 
67     private int mDisplayId;
68 
69     private boolean mIsVisible;
70 
71     /**
72      * TaskFragments that the organizer has requested to be closed. They should be removed when
73      * the organizer receives
74      * {@link SplitController#onTaskFragmentVanished(WindowContainerTransaction, TaskFragmentInfo)}
75      * event for them.
76      */
77     final Set<IBinder> mFinishedContainer = new ArraySet<>();
78 
79     /**
80      * The {@link TaskContainer} constructor
81      *
82      * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with
83      *               {@code activityInTask}.
84      * @param activityInTask The {@link Activity} in the Task with {@code taskId}. It is used to
85      *                       initialize the {@link TaskContainer} properties.
86      *
87      */
TaskContainer(int taskId, @NonNull Activity activityInTask)88     TaskContainer(int taskId, @NonNull Activity activityInTask) {
89         if (taskId == INVALID_TASK_ID) {
90             throw new IllegalArgumentException("Invalid Task id");
91         }
92         mTaskId = taskId;
93         final TaskProperties taskProperties = TaskProperties
94                 .getTaskPropertiesFromActivity(activityInTask);
95         mConfiguration = taskProperties.getConfiguration();
96         mDisplayId = taskProperties.getDisplayId();
97         // Note that it is always called when there's a new Activity is started, which implies
98         // the host task is visible.
99         mIsVisible = true;
100     }
101 
getTaskId()102     int getTaskId() {
103         return mTaskId;
104     }
105 
getDisplayId()106     int getDisplayId() {
107         return mDisplayId;
108     }
109 
isVisible()110     boolean isVisible() {
111         return mIsVisible;
112     }
113 
114     @NonNull
getTaskProperties()115     TaskProperties getTaskProperties() {
116         return new TaskProperties(mDisplayId, mConfiguration);
117     }
118 
updateTaskFragmentParentInfo(@onNull TaskFragmentParentInfo info)119     void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) {
120         mConfiguration.setTo(info.getConfiguration());
121         mDisplayId = info.getDisplayId();
122         mIsVisible = info.isVisible();
123     }
124 
125     /**
126      * Returns the windowing mode for the TaskFragments below this Task, which should be split with
127      * other TaskFragments.
128      *
129      * @param taskFragmentBounds    Requested bounds for the TaskFragment. It will be empty when
130      *                              the pair of TaskFragments are stacked due to the limited space.
131      */
132     @WindowingMode
getWindowingModeForSplitTaskFragment(@ullable Rect taskFragmentBounds)133     int getWindowingModeForSplitTaskFragment(@Nullable Rect taskFragmentBounds) {
134         // Only set to multi-windowing mode if the pair are showing side-by-side. Otherwise, it
135         // will be set to UNDEFINED which will then inherit the Task windowing mode.
136         if (taskFragmentBounds == null || taskFragmentBounds.isEmpty() || isInPictureInPicture()) {
137             return WINDOWING_MODE_UNDEFINED;
138         }
139         // We use WINDOWING_MODE_MULTI_WINDOW when the Task is fullscreen.
140         // However, when the Task is in other multi windowing mode, such as Freeform, we need to
141         // have the activity windowing mode to match the Task, otherwise things like
142         // DecorCaptionView won't work correctly. As a result, have the TaskFragment to be in the
143         // Task windowing mode if the Task is in multi window.
144         // TODO we won't need this anymore after we migrate Freeform caption to WM Shell.
145         return isInMultiWindow() ? getWindowingMode() : WINDOWING_MODE_MULTI_WINDOW;
146     }
147 
isInPictureInPicture()148     boolean isInPictureInPicture() {
149         return getWindowingMode() == WINDOWING_MODE_PINNED;
150     }
151 
isInMultiWindow()152     boolean isInMultiWindow() {
153         return WindowConfiguration.inMultiWindowMode(getWindowingMode());
154     }
155 
156     @WindowingMode
getWindowingMode()157     private int getWindowingMode() {
158         return mConfiguration.windowConfiguration.getWindowingMode();
159     }
160 
161     /** Whether there is any {@link TaskFragmentContainer} below this Task. */
isEmpty()162     boolean isEmpty() {
163         return mContainers.isEmpty() && mFinishedContainer.isEmpty();
164     }
165 
166     /** Called when the activity is destroyed. */
onActivityDestroyed(@onNull IBinder activityToken)167     void onActivityDestroyed(@NonNull IBinder activityToken) {
168         for (TaskFragmentContainer container : mContainers) {
169             container.onActivityDestroyed(activityToken);
170         }
171     }
172 
173     /** Removes the pending appeared activity from all TaskFragments in this Task. */
cleanupPendingAppearedActivity(@onNull IBinder activityToken)174     void cleanupPendingAppearedActivity(@NonNull IBinder activityToken) {
175         for (TaskFragmentContainer container : mContainers) {
176             container.removePendingAppearedActivity(activityToken);
177         }
178     }
179 
180     @Nullable
getTopNonFinishingTaskFragmentContainer()181     TaskFragmentContainer getTopNonFinishingTaskFragmentContainer() {
182         return getTopNonFinishingTaskFragmentContainer(true /* includePin */);
183     }
184 
185     @Nullable
getTopNonFinishingTaskFragmentContainer(boolean includePin)186     TaskFragmentContainer getTopNonFinishingTaskFragmentContainer(boolean includePin) {
187         for (int i = mContainers.size() - 1; i >= 0; i--) {
188             final TaskFragmentContainer container = mContainers.get(i);
189             if (!includePin && isTaskFragmentContainerPinned(container)) {
190                 continue;
191             }
192             if (!container.isFinished()) {
193                 return container;
194             }
195         }
196         return null;
197     }
198 
199     /** Gets a non-finishing container below the given one. */
200     @Nullable
getNonFinishingTaskFragmentContainerBelow( @onNull TaskFragmentContainer current)201     TaskFragmentContainer getNonFinishingTaskFragmentContainerBelow(
202             @NonNull TaskFragmentContainer current) {
203         final int index = mContainers.indexOf(current);
204         for (int i = index - 1; i >= 0; i--) {
205             final TaskFragmentContainer container = mContainers.get(i);
206             if (!container.isFinished()) {
207                 return container;
208             }
209         }
210         return null;
211     }
212 
213     @Nullable
getTopNonFinishingActivity()214     Activity getTopNonFinishingActivity() {
215         for (int i = mContainers.size() - 1; i >= 0; i--) {
216             final Activity activity = mContainers.get(i).getTopNonFinishingActivity();
217             if (activity != null) {
218                 return activity;
219             }
220         }
221         return null;
222     }
223 
indexOf(@onNull TaskFragmentContainer child)224     int indexOf(@NonNull TaskFragmentContainer child) {
225         return mContainers.indexOf(child);
226     }
227 
228     /** Whether the Task is in an intermediate state waiting for the server update.*/
isInIntermediateState()229     boolean isInIntermediateState() {
230         for (TaskFragmentContainer container : mContainers) {
231             if (container.isInIntermediateState()) {
232                 // We are in an intermediate state to wait for server update on this TaskFragment.
233                 return true;
234             }
235         }
236         return false;
237     }
238 
239     /**
240      * Returns a list of {@link SplitContainer}. Do not modify the containers directly on the
241      * returned list. Use {@link #addSplitContainer} or {@link #removeSplitContainers} instead.
242      */
243     @NonNull
getSplitContainers()244     List<SplitContainer> getSplitContainers() {
245         return mSplitContainers;
246     }
247 
addSplitContainer(@onNull SplitContainer splitContainer)248     void addSplitContainer(@NonNull SplitContainer splitContainer) {
249         if (splitContainer instanceof SplitPinContainer) {
250             mSplitPinContainer = (SplitPinContainer) splitContainer;
251             mSplitContainers.add(splitContainer);
252             return;
253         }
254 
255         // Keeps the SplitPinContainer on the top of the list.
256         mSplitContainers.remove(mSplitPinContainer);
257         mSplitContainers.add(splitContainer);
258         if (mSplitPinContainer != null) {
259             mSplitContainers.add(mSplitPinContainer);
260         }
261     }
262 
removeSplitContainers(@onNull List<SplitContainer> containers)263     void removeSplitContainers(@NonNull List<SplitContainer> containers) {
264         mSplitContainers.removeAll(containers);
265     }
266 
removeSplitPinContainer()267     void removeSplitPinContainer() {
268         if (mSplitPinContainer == null) {
269             return;
270         }
271 
272         final TaskFragmentContainer primaryContainer = mSplitPinContainer.getPrimaryContainer();
273         final TaskFragmentContainer secondaryContainer = mSplitPinContainer.getSecondaryContainer();
274         mSplitContainers.remove(mSplitPinContainer);
275         mSplitPinContainer = null;
276 
277         // Remove the other SplitContainers that contains the unpinned container (unless it
278         // is the current top-most split-pair), since the state are no longer valid.
279         final List<SplitContainer> splitsToRemove = new ArrayList<>();
280         for (SplitContainer splitContainer : mSplitContainers) {
281             if (splitContainer.getSecondaryContainer().equals(secondaryContainer)
282                     && !splitContainer.getPrimaryContainer().equals(primaryContainer)) {
283                 splitsToRemove.add(splitContainer);
284             }
285         }
286         removeSplitContainers(splitsToRemove);
287     }
288 
289     @Nullable
getSplitPinContainer()290     SplitPinContainer getSplitPinContainer() {
291         return mSplitPinContainer;
292     }
293 
isTaskFragmentContainerPinned(@onNull TaskFragmentContainer taskFragmentContainer)294     boolean isTaskFragmentContainerPinned(@NonNull TaskFragmentContainer taskFragmentContainer) {
295         return mSplitPinContainer != null
296                 && mSplitPinContainer.getSecondaryContainer() == taskFragmentContainer;
297     }
298 
addTaskFragmentContainer(@onNull TaskFragmentContainer taskFragmentContainer)299     void addTaskFragmentContainer(@NonNull TaskFragmentContainer taskFragmentContainer) {
300         mContainers.add(taskFragmentContainer);
301         onTaskFragmentContainerUpdated();
302     }
303 
addTaskFragmentContainer(int index, @NonNull TaskFragmentContainer taskFragmentContainer)304     void addTaskFragmentContainer(int index, @NonNull TaskFragmentContainer taskFragmentContainer) {
305         mContainers.add(index, taskFragmentContainer);
306         onTaskFragmentContainerUpdated();
307     }
308 
removeTaskFragmentContainer(@onNull TaskFragmentContainer taskFragmentContainer)309     void removeTaskFragmentContainer(@NonNull TaskFragmentContainer taskFragmentContainer) {
310         mContainers.remove(taskFragmentContainer);
311         onTaskFragmentContainerUpdated();
312     }
313 
removeTaskFragmentContainers(@onNull List<TaskFragmentContainer> taskFragmentContainer)314     void removeTaskFragmentContainers(@NonNull List<TaskFragmentContainer> taskFragmentContainer) {
315         mContainers.removeAll(taskFragmentContainer);
316         onTaskFragmentContainerUpdated();
317     }
318 
clearTaskFragmentContainer()319     void clearTaskFragmentContainer() {
320         mContainers.clear();
321         onTaskFragmentContainerUpdated();
322     }
323 
324     /**
325      * Returns a list of {@link TaskFragmentContainer}. Do not modify the containers directly on
326      * the returned list. Use {@link #addTaskFragmentContainer},
327      * {@link #removeTaskFragmentContainer} or other related methods instead.
328      */
329     @NonNull
getTaskFragmentContainers()330     List<TaskFragmentContainer> getTaskFragmentContainers() {
331         return mContainers;
332     }
333 
onTaskFragmentContainerUpdated()334     private void onTaskFragmentContainerUpdated() {
335         if (mSplitPinContainer == null) {
336             return;
337         }
338 
339         final TaskFragmentContainer pinnedContainer = mSplitPinContainer.getSecondaryContainer();
340         final int pinnedContainerIndex = mContainers.indexOf(pinnedContainer);
341         if (pinnedContainerIndex <= 0) {
342             removeSplitPinContainer();
343             return;
344         }
345 
346         // Ensure the pinned container is top-most.
347         if (pinnedContainerIndex != mContainers.size() - 1) {
348             mContainers.remove(pinnedContainer);
349             mContainers.add(pinnedContainer);
350         }
351 
352         // Update the primary container adjacent to the pinned container if needed.
353         final TaskFragmentContainer adjacentContainer =
354                 getNonFinishingTaskFragmentContainerBelow(pinnedContainer);
355         if (adjacentContainer == null) {
356             removeSplitPinContainer();
357         } else if (mSplitPinContainer.getPrimaryContainer() != adjacentContainer) {
358             mSplitPinContainer.setPrimaryContainer(adjacentContainer);
359         }
360     }
361 
362     /** Adds the descriptors of split states in this Task to {@code outSplitStates}. */
getSplitStates(@onNull List<SplitInfo> outSplitStates)363     void getSplitStates(@NonNull List<SplitInfo> outSplitStates) {
364         for (SplitContainer container : mSplitContainers) {
365             outSplitStates.add(container.toSplitInfo());
366         }
367     }
368 
369     /** A wrapper class which contains the information of {@link TaskContainer} */
370     static final class TaskProperties {
371         private final int mDisplayId;
372         @NonNull
373         private final Configuration mConfiguration;
374 
TaskProperties(int displayId, @NonNull Configuration configuration)375         TaskProperties(int displayId, @NonNull Configuration configuration) {
376             mDisplayId = displayId;
377             mConfiguration = configuration;
378         }
379 
getDisplayId()380         int getDisplayId() {
381             return mDisplayId;
382         }
383 
384         @NonNull
getConfiguration()385         Configuration getConfiguration() {
386             return mConfiguration;
387         }
388 
389         /** Translates the given absolute bounds to relative bounds in this Task coordinate. */
translateAbsoluteBoundsToRelativeBounds(@onNull Rect inOutBounds)390         void translateAbsoluteBoundsToRelativeBounds(@NonNull Rect inOutBounds) {
391             if (inOutBounds.isEmpty()) {
392                 return;
393             }
394             final Rect taskBounds = mConfiguration.windowConfiguration.getBounds();
395             inOutBounds.offset(-taskBounds.left, -taskBounds.top);
396         }
397 
398         /**
399          * Obtains the {@link TaskProperties} for the task that the provided {@link Activity} is
400          * associated with.
401          * <p>
402          * Note that for most case, caller should use
403          * {@link SplitPresenter#getTaskProperties(Activity)} instead. This method is used before
404          * the {@code activity} goes into split.
405          * </p><p>
406          * If the {@link Activity} is in fullscreen, override
407          * {@link WindowConfiguration#getBounds()} with {@link WindowConfiguration#getMaxBounds()}
408          * in case the {@link Activity} is letterboxed. Otherwise, get the Task
409          * {@link Configuration} from the server side or use {@link Activity}'s
410          * {@link Configuration} as a fallback if the Task {@link Configuration} cannot be obtained.
411          */
412         @NonNull
getTaskPropertiesFromActivity(@onNull Activity activity)413         static TaskProperties getTaskPropertiesFromActivity(@NonNull Activity activity) {
414             final int displayId = activity.getDisplayId();
415             // Use a copy of configuration because activity's configuration may be updated later,
416             // or we may get unexpected TaskContainer's configuration if Activity's configuration is
417             // updated. An example is Activity is going to be in split.
418             final Configuration activityConfig = new Configuration(
419                     activity.getResources().getConfiguration());
420             final WindowConfiguration windowConfiguration = activityConfig.windowConfiguration;
421             final int windowingMode = windowConfiguration.getWindowingMode();
422             if (!inMultiWindowMode(windowingMode)) {
423                 // Use the max bounds in fullscreen in case the Activity is letterboxed.
424                 windowConfiguration.setBounds(windowConfiguration.getMaxBounds());
425                 return new TaskProperties(displayId, activityConfig);
426             }
427             final Configuration taskConfig = ActivityClient.getInstance()
428                     .getTaskConfiguration(activity.getActivityToken());
429             if (taskConfig == null) {
430                 Log.w(TAG, "Could not obtain task configuration for activity:" + activity);
431                 // Still report activity config if task config cannot be obtained from the server
432                 // side.
433                 return new TaskProperties(displayId, activityConfig);
434             }
435             return new TaskProperties(displayId, taskConfig);
436         }
437     }
438 }
439