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_MULTI_WINDOW;
20 
21 import android.app.Activity;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.res.Configuration;
25 import android.graphics.Rect;
26 import android.os.Bundle;
27 import android.os.IBinder;
28 import android.util.LayoutDirection;
29 import android.view.View;
30 import android.view.WindowInsets;
31 import android.view.WindowMetrics;
32 import android.window.TaskFragmentCreationParams;
33 import android.window.WindowContainerTransaction;
34 
35 import androidx.annotation.IntDef;
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 
39 import java.util.concurrent.Executor;
40 
41 /**
42  * Controls the visual presentation of the splits according to the containers formed by
43  * {@link SplitController}.
44  */
45 class SplitPresenter extends JetpackTaskFragmentOrganizer {
46     private static final int POSITION_START = 0;
47     private static final int POSITION_END = 1;
48     private static final int POSITION_FILL = 2;
49 
50     @IntDef(value = {
51             POSITION_START,
52             POSITION_END,
53             POSITION_FILL,
54     })
55     private @interface Position {}
56 
57     private final SplitController mController;
58 
SplitPresenter(@onNull Executor executor, SplitController controller)59     SplitPresenter(@NonNull Executor executor, SplitController controller) {
60         super(executor, controller);
61         mController = controller;
62         registerOrganizer();
63     }
64 
65     /**
66      * Updates the presentation of the provided container.
67      */
updateContainer(TaskFragmentContainer container)68     void updateContainer(TaskFragmentContainer container) {
69         final WindowContainerTransaction wct = new WindowContainerTransaction();
70         mController.updateContainer(wct, container);
71         applyTransaction(wct);
72     }
73 
74     /**
75      * Deletes the specified container and all other associated and dependent containers in the same
76      * transaction.
77      */
cleanupContainer(@onNull TaskFragmentContainer container, boolean shouldFinishDependent)78     void cleanupContainer(@NonNull TaskFragmentContainer container, boolean shouldFinishDependent) {
79         final WindowContainerTransaction wct = new WindowContainerTransaction();
80 
81         container.finish(shouldFinishDependent, this, wct, mController);
82 
83         final TaskFragmentContainer newTopContainer = mController.getTopActiveContainer();
84         if (newTopContainer != null) {
85             mController.updateContainer(wct, newTopContainer);
86         }
87 
88         applyTransaction(wct);
89     }
90 
91     /**
92      * Creates a new split with the primary activity and an empty secondary container.
93      * @return The newly created secondary container.
94      */
createNewSplitWithEmptySideContainer(@onNull Activity primaryActivity, @NonNull SplitPairRule rule)95     TaskFragmentContainer createNewSplitWithEmptySideContainer(@NonNull Activity primaryActivity,
96             @NonNull SplitPairRule rule) {
97         final WindowContainerTransaction wct = new WindowContainerTransaction();
98 
99         final Rect parentBounds = getParentContainerBounds(primaryActivity);
100         final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
101                 isLtr(primaryActivity, rule));
102         final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
103                 primaryActivity, primaryRectBounds, null);
104 
105         // Create new empty task fragment
106         final TaskFragmentContainer secondaryContainer = mController.newContainer(null);
107         final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds,
108                 rule, isLtr(primaryActivity, rule));
109         createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(),
110                 primaryActivity.getActivityToken(), secondaryRectBounds,
111                 WINDOWING_MODE_MULTI_WINDOW);
112         secondaryContainer.setLastRequestedBounds(secondaryRectBounds);
113 
114         // Set adjacent to each other so that the containers below will be invisible.
115         setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule);
116 
117         mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
118 
119         applyTransaction(wct);
120 
121         return secondaryContainer;
122     }
123 
124     /**
125      * Creates a new split container with the two provided activities.
126      * @param primaryActivity An activity that should be in the primary container. If it is not
127      *                        currently in an existing container, a new one will be created and the
128      *                        activity will be re-parented to it.
129      * @param secondaryActivity An activity that should be in the secondary container. If it is not
130      *                          currently in an existing container, or if it is currently in the
131      *                          same container as the primary activity, a new container will be
132      *                          created and the activity will be re-parented to it.
133      * @param rule The split rule to be applied to the container.
134      */
createNewSplitContainer(@onNull Activity primaryActivity, @NonNull Activity secondaryActivity, @NonNull SplitPairRule rule)135     void createNewSplitContainer(@NonNull Activity primaryActivity,
136             @NonNull Activity secondaryActivity, @NonNull SplitPairRule rule) {
137         final WindowContainerTransaction wct = new WindowContainerTransaction();
138 
139         final Rect parentBounds = getParentContainerBounds(primaryActivity);
140         final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
141                 isLtr(primaryActivity, rule));
142         final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
143                 primaryActivity, primaryRectBounds, null);
144 
145         final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
146                 isLtr(primaryActivity, rule));
147         final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct,
148                 secondaryActivity, secondaryRectBounds, primaryContainer);
149 
150         // Set adjacent to each other so that the containers below will be invisible.
151         setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule);
152 
153         mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
154 
155         applyTransaction(wct);
156     }
157 
158     /**
159      * Creates a new expanded container.
160      */
createNewExpandedContainer(@onNull Activity launchingActivity)161     TaskFragmentContainer createNewExpandedContainer(@NonNull Activity launchingActivity) {
162         final TaskFragmentContainer newContainer = mController.newContainer(null);
163 
164         final WindowContainerTransaction wct = new WindowContainerTransaction();
165         createTaskFragment(wct, newContainer.getTaskFragmentToken(),
166                 launchingActivity.getActivityToken(), new Rect(), WINDOWING_MODE_MULTI_WINDOW);
167 
168         applyTransaction(wct);
169         return newContainer;
170     }
171 
172     /**
173      * Creates a new container or resizes an existing container for activity to the provided bounds.
174      * @param activity The activity to be re-parented to the container if necessary.
175      * @param containerToAvoid Re-parent from this container if an activity is already in it.
176      */
prepareContainerForActivity( @onNull WindowContainerTransaction wct, @NonNull Activity activity, @NonNull Rect bounds, @Nullable TaskFragmentContainer containerToAvoid)177     private TaskFragmentContainer prepareContainerForActivity(
178             @NonNull WindowContainerTransaction wct, @NonNull Activity activity,
179             @NonNull Rect bounds, @Nullable TaskFragmentContainer containerToAvoid) {
180         TaskFragmentContainer container = mController.getContainerWithActivity(
181                 activity.getActivityToken());
182         if (container == null || container == containerToAvoid) {
183             container = mController.newContainer(activity);
184 
185             final TaskFragmentCreationParams fragmentOptions =
186                     createFragmentOptions(
187                             container.getTaskFragmentToken(),
188                             activity.getActivityToken(),
189                             bounds,
190                             WINDOWING_MODE_MULTI_WINDOW);
191             wct.createTaskFragment(fragmentOptions);
192 
193             wct.reparentActivityToTaskFragment(container.getTaskFragmentToken(),
194                     activity.getActivityToken());
195 
196             container.setLastRequestedBounds(bounds);
197         } else {
198             resizeTaskFragmentIfRegistered(wct, container, bounds);
199         }
200 
201         return container;
202     }
203 
204     /**
205      * Starts a new activity to the side, creating a new split container. A new container will be
206      * created for the activity that will be started.
207      * @param launchingActivity An activity that should be in the primary container. If it is not
208      *                          currently in an existing container, a new one will be created and
209      *                          the activity will be re-parented to it.
210      * @param activityIntent The intent to start the new activity.
211      * @param activityOptions The options to apply to new activity start.
212      * @param rule The split rule to be applied to the container.
213      */
startActivityToSide(@onNull Activity launchingActivity, @NonNull Intent activityIntent, @Nullable Bundle activityOptions, @NonNull SplitRule rule)214     void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent,
215             @Nullable Bundle activityOptions, @NonNull SplitRule rule) {
216         final Rect parentBounds = getParentContainerBounds(launchingActivity);
217         final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
218                 isLtr(launchingActivity, rule));
219         final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
220                 isLtr(launchingActivity, rule));
221 
222         TaskFragmentContainer primaryContainer = mController.getContainerWithActivity(
223                 launchingActivity.getActivityToken());
224         if (primaryContainer == null) {
225             primaryContainer = mController.newContainer(launchingActivity);
226         }
227 
228         TaskFragmentContainer secondaryContainer = mController.newContainer(null);
229         final WindowContainerTransaction wct = new WindowContainerTransaction();
230         mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer,
231                 rule);
232         startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds,
233                 launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRectBounds,
234                 activityIntent, activityOptions, rule);
235         applyTransaction(wct);
236 
237         primaryContainer.setLastRequestedBounds(primaryRectBounds);
238         secondaryContainer.setLastRequestedBounds(secondaryRectBounds);
239     }
240 
241     /**
242      * Updates the positions of containers in an existing split.
243      * @param splitContainer The split container to be updated.
244      * @param updatedContainer The task fragment that was updated and caused this split update.
245      * @param wct WindowContainerTransaction that this update should be performed with.
246      */
updateSplitContainer(@onNull SplitContainer splitContainer, @NonNull TaskFragmentContainer updatedContainer, @NonNull WindowContainerTransaction wct)247     void updateSplitContainer(@NonNull SplitContainer splitContainer,
248             @NonNull TaskFragmentContainer updatedContainer,
249             @NonNull WindowContainerTransaction wct) {
250         // Getting the parent bounds using the updated container - it will have the recent value.
251         final Rect parentBounds = getParentContainerBounds(updatedContainer);
252         final SplitRule rule = splitContainer.getSplitRule();
253         final TaskFragmentContainer primaryContainer = splitContainer.getPrimaryContainer();
254         final Activity activity = primaryContainer.getTopNonFinishingActivity();
255         if (activity == null) {
256             return;
257         }
258         final boolean isLtr = isLtr(activity, rule);
259         final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
260                 isLtr);
261         final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
262                 isLtr);
263 
264         // If the task fragments are not registered yet, the positions will be updated after they
265         // are created again.
266         resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds);
267         final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer();
268         resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds);
269 
270         setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule);
271     }
272 
setAdjacentTaskFragments(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer, @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule)273     private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
274             @NonNull TaskFragmentContainer primaryContainer,
275             @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule) {
276         final Rect parentBounds = getParentContainerBounds(primaryContainer);
277         // Clear adjacent TaskFragments if the container is shown in fullscreen, or the
278         // secondaryContainer could not be finished.
279         if (!shouldShowSideBySide(parentBounds, splitRule)) {
280             setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
281                     null /* secondary */, null /* splitRule */);
282         } else {
283             setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
284                     secondaryContainer.getTaskFragmentToken(), splitRule);
285         }
286     }
287 
288     /**
289      * Resizes the task fragment if it was already registered. Skips the operation if the container
290      * creation has not been reported from the server yet.
291      */
292     // TODO(b/190433398): Handle resize if the fragment hasn't appeared yet.
resizeTaskFragmentIfRegistered(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, @Nullable Rect bounds)293     void resizeTaskFragmentIfRegistered(@NonNull WindowContainerTransaction wct,
294             @NonNull TaskFragmentContainer container,
295             @Nullable Rect bounds) {
296         if (container.getInfo() == null) {
297             return;
298         }
299         resizeTaskFragment(wct, container.getTaskFragmentToken(), bounds);
300     }
301 
302     @Override
resizeTaskFragment(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @Nullable Rect bounds)303     void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
304             @Nullable Rect bounds) {
305         TaskFragmentContainer container = mController.getContainer(fragmentToken);
306         if (container == null) {
307             throw new IllegalStateException(
308                     "Resizing a task fragment that is not registered with controller.");
309         }
310 
311         if (container.areLastRequestedBoundsEqual(bounds)) {
312             // Return early if the provided bounds were already requested
313             return;
314         }
315 
316         container.setLastRequestedBounds(bounds);
317         super.resizeTaskFragment(wct, fragmentToken, bounds);
318     }
319 
shouldShowSideBySide(@onNull SplitContainer splitContainer)320     boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) {
321         final Rect parentBounds = getParentContainerBounds(splitContainer.getPrimaryContainer());
322         return shouldShowSideBySide(parentBounds, splitContainer.getSplitRule());
323     }
324 
shouldShowSideBySide(@ullable Rect parentBounds, @NonNull SplitRule rule)325     boolean shouldShowSideBySide(@Nullable Rect parentBounds, @NonNull SplitRule rule) {
326         // TODO(b/190433398): Supply correct insets.
327         final WindowMetrics parentMetrics = new WindowMetrics(parentBounds,
328                 new WindowInsets(new Rect()));
329         return rule.checkParentMetrics(parentMetrics);
330     }
331 
332     @NonNull
getBoundsForPosition(@osition int position, @NonNull Rect parentBounds, @NonNull SplitRule rule, boolean isLtr)333     private Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds,
334             @NonNull SplitRule rule, boolean isLtr) {
335         if (!shouldShowSideBySide(parentBounds, rule)) {
336             return new Rect();
337         }
338 
339         final float splitRatio = rule.getSplitRatio();
340         final float rtlSplitRatio = 1 - splitRatio;
341         switch (position) {
342             case POSITION_START:
343                 return isLtr ? getLeftContainerBounds(parentBounds, splitRatio)
344                         : getRightContainerBounds(parentBounds, rtlSplitRatio);
345             case POSITION_END:
346                 return isLtr ? getRightContainerBounds(parentBounds, splitRatio)
347                         : getLeftContainerBounds(parentBounds, rtlSplitRatio);
348             case POSITION_FILL:
349                 return parentBounds;
350         }
351         return parentBounds;
352     }
353 
getLeftContainerBounds(@onNull Rect parentBounds, float splitRatio)354     private Rect getLeftContainerBounds(@NonNull Rect parentBounds, float splitRatio) {
355         return new Rect(
356                 parentBounds.left,
357                 parentBounds.top,
358                 (int) (parentBounds.left + parentBounds.width() * splitRatio),
359                 parentBounds.bottom);
360     }
361 
getRightContainerBounds(@onNull Rect parentBounds, float splitRatio)362     private Rect getRightContainerBounds(@NonNull Rect parentBounds, float splitRatio) {
363         return new Rect(
364                 (int) (parentBounds.left + parentBounds.width() * splitRatio),
365                 parentBounds.top,
366                 parentBounds.right,
367                 parentBounds.bottom);
368     }
369 
370     /**
371      * Checks if a split with the provided rule should be displays in left-to-right layout
372      * direction, either always or with the current configuration.
373      */
isLtr(@onNull Context context, @NonNull SplitRule rule)374     private boolean isLtr(@NonNull Context context, @NonNull SplitRule rule) {
375         switch (rule.getLayoutDirection()) {
376             case LayoutDirection.LOCALE:
377                 return context.getResources().getConfiguration().getLayoutDirection()
378                         == View.LAYOUT_DIRECTION_LTR;
379             case LayoutDirection.RTL:
380                 return false;
381             case LayoutDirection.LTR:
382             default:
383                 return true;
384         }
385     }
386 
387     @NonNull
getParentContainerBounds(@onNull TaskFragmentContainer container)388     Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) {
389         final Configuration parentConfig = mFragmentParentConfigs.get(
390                 container.getTaskFragmentToken());
391         if (parentConfig != null) {
392             return parentConfig.windowConfiguration.getBounds();
393         }
394 
395         // If there is no parent yet - then assuming that activities are running in full task bounds
396         final Activity topActivity = container.getTopNonFinishingActivity();
397         final Rect bounds = topActivity != null ? getParentContainerBounds(topActivity) : null;
398 
399         if (bounds == null) {
400             throw new IllegalStateException("Unknown parent bounds");
401         }
402         return bounds;
403     }
404 
405     @NonNull
getParentContainerBounds(@onNull Activity activity)406     Rect getParentContainerBounds(@NonNull Activity activity) {
407         final TaskFragmentContainer container = mController.getContainerWithActivity(
408                 activity.getActivityToken());
409         if (container != null) {
410             final Configuration parentConfig = mFragmentParentConfigs.get(
411                     container.getTaskFragmentToken());
412             if (parentConfig != null) {
413                 return parentConfig.windowConfiguration.getBounds();
414             }
415         }
416 
417         // TODO(b/190433398): Check if the client-side available info about parent bounds is enough.
418         if (!activity.isInMultiWindowMode()) {
419             // In fullscreen mode the max bounds should correspond to the task bounds.
420             return activity.getResources().getConfiguration().windowConfiguration.getMaxBounds();
421         }
422         return activity.getResources().getConfiguration().windowConfiguration.getBounds();
423     }
424 }
425