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