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 androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior; 20 import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior; 21 import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceholderRule; 22 import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent; 23 import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked; 24 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.app.Activity; 28 import android.app.ActivityClient; 29 import android.app.ActivityOptions; 30 import android.app.ActivityThread; 31 import android.app.Application.ActivityLifecycleCallbacks; 32 import android.app.Instrumentation; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.content.res.Configuration; 36 import android.graphics.Rect; 37 import android.os.Bundle; 38 import android.os.Handler; 39 import android.os.IBinder; 40 import android.os.Looper; 41 import android.window.TaskFragmentInfo; 42 import android.window.WindowContainerTransaction; 43 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.Set; 47 import java.util.concurrent.Executor; 48 import java.util.function.Consumer; 49 50 /** 51 * Main controller class that manages split states and presentation. 52 */ 53 public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback, 54 ActivityEmbeddingComponent { 55 56 private final SplitPresenter mPresenter; 57 58 // Currently applied split configuration. 59 private final List<EmbeddingRule> mSplitRules = new ArrayList<>(); 60 private final List<TaskFragmentContainer> mContainers = new ArrayList<>(); 61 private final List<SplitContainer> mSplitContainers = new ArrayList<>(); 62 63 // Callback to Jetpack to notify about changes to split states. 64 private @NonNull Consumer<List<SplitInfo>> mEmbeddingCallback; 65 private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>(); 66 67 // We currently only support split activity embedding within the one root Task. 68 private final Rect mParentBounds = new Rect(); 69 SplitController()70 public SplitController() { 71 mPresenter = new SplitPresenter(new MainThreadExecutor(), this); 72 ActivityThread activityThread = ActivityThread.currentActivityThread(); 73 // Register a callback to be notified about activities being created. 74 activityThread.getApplication().registerActivityLifecycleCallbacks( 75 new LifecycleCallbacks()); 76 // Intercept activity starts to route activities to new containers if necessary. 77 Instrumentation instrumentation = activityThread.getInstrumentation(); 78 instrumentation.addMonitor(new ActivityStartMonitor()); 79 } 80 81 /** Updates the embedding rules applied to future activity launches. */ 82 @Override setEmbeddingRules(@onNull Set<EmbeddingRule> rules)83 public void setEmbeddingRules(@NonNull Set<EmbeddingRule> rules) { 84 mSplitRules.clear(); 85 mSplitRules.addAll(rules); 86 updateAnimationOverride(); 87 } 88 89 @NonNull getSplitRules()90 public List<EmbeddingRule> getSplitRules() { 91 return mSplitRules; 92 } 93 94 /** 95 * Starts an activity to side of the launchingActivity with the provided split config. 96 */ startActivityToSide(@onNull Activity launchingActivity, @NonNull Intent intent, @Nullable Bundle options, @NonNull SplitRule sideRule, @Nullable Consumer<Exception> failureCallback)97 public void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent, 98 @Nullable Bundle options, @NonNull SplitRule sideRule, 99 @Nullable Consumer<Exception> failureCallback) { 100 try { 101 mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule); 102 } catch (Exception e) { 103 if (failureCallback != null) { 104 failureCallback.accept(e); 105 } 106 } 107 } 108 109 /** 110 * Registers the split organizer callback to notify about changes to active splits. 111 */ 112 @Override setSplitInfoCallback(@onNull Consumer<List<SplitInfo>> callback)113 public void setSplitInfoCallback(@NonNull Consumer<List<SplitInfo>> callback) { 114 mEmbeddingCallback = callback; 115 updateCallbackIfNecessary(); 116 } 117 118 @Override onTaskFragmentAppeared(@onNull TaskFragmentInfo taskFragmentInfo)119 public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) { 120 TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); 121 if (container == null) { 122 return; 123 } 124 125 container.setInfo(taskFragmentInfo); 126 if (container.isFinished()) { 127 mPresenter.cleanupContainer(container, false /* shouldFinishDependent */); 128 } 129 updateCallbackIfNecessary(); 130 } 131 132 @Override onTaskFragmentInfoChanged(@onNull TaskFragmentInfo taskFragmentInfo)133 public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) { 134 TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); 135 if (container == null) { 136 return; 137 } 138 139 container.setInfo(taskFragmentInfo); 140 // Check if there are no running activities - consider the container empty if there are no 141 // non-finishing activities left. 142 if (!taskFragmentInfo.hasRunningActivity()) { 143 // Do not finish the dependents if this TaskFragment was cleared due to launching 144 // activity in the Task. 145 final boolean shouldFinishDependent = 146 !taskFragmentInfo.isTaskClearedForReuse(); 147 mPresenter.cleanupContainer(container, shouldFinishDependent); 148 } 149 updateCallbackIfNecessary(); 150 } 151 152 @Override onTaskFragmentVanished(@onNull TaskFragmentInfo taskFragmentInfo)153 public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) { 154 TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); 155 if (container == null) { 156 return; 157 } 158 159 mPresenter.cleanupContainer(container, true /* shouldFinishDependent */); 160 updateCallbackIfNecessary(); 161 } 162 163 @Override onTaskFragmentParentInfoChanged(@onNull IBinder fragmentToken, @NonNull Configuration parentConfig)164 public void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken, 165 @NonNull Configuration parentConfig) { 166 onParentBoundsMayChange(parentConfig.windowConfiguration.getBounds()); 167 TaskFragmentContainer container = getContainer(fragmentToken); 168 if (container != null) { 169 mPresenter.updateContainer(container); 170 updateCallbackIfNecessary(); 171 } 172 } 173 onParentBoundsMayChange(Activity activity)174 private void onParentBoundsMayChange(Activity activity) { 175 if (activity.isFinishing()) { 176 return; 177 } 178 179 onParentBoundsMayChange(mPresenter.getParentContainerBounds(activity)); 180 } 181 onParentBoundsMayChange(Rect parentBounds)182 private void onParentBoundsMayChange(Rect parentBounds) { 183 if (!parentBounds.isEmpty() && !mParentBounds.equals(parentBounds)) { 184 mParentBounds.set(parentBounds); 185 updateAnimationOverride(); 186 } 187 } 188 189 /** 190 * Updates if we should override transition animation. We only want to override if the Task 191 * bounds is large enough for at least one split rule. 192 */ updateAnimationOverride()193 private void updateAnimationOverride() { 194 if (mParentBounds.isEmpty()) { 195 // We don't know about the parent bounds yet. 196 return; 197 } 198 199 // Check if the parent container bounds can support any split rule. 200 boolean supportSplit = false; 201 for (EmbeddingRule rule : mSplitRules) { 202 if (!(rule instanceof SplitRule)) { 203 continue; 204 } 205 if (mPresenter.shouldShowSideBySide(mParentBounds, (SplitRule) rule)) { 206 supportSplit = true; 207 break; 208 } 209 } 210 211 // We only want to override if it supports split. 212 if (supportSplit) { 213 mPresenter.startOverrideSplitAnimation(); 214 } else { 215 mPresenter.stopOverrideSplitAnimation(); 216 } 217 } 218 onActivityCreated(@onNull Activity launchedActivity)219 void onActivityCreated(@NonNull Activity launchedActivity) { 220 handleActivityCreated(launchedActivity); 221 updateCallbackIfNecessary(); 222 } 223 224 /** 225 * Checks if the activity start should be routed to a particular container. It can create a new 226 * container for the activity and a new split container if necessary. 227 */ 228 // TODO(b/190433398): Break down into smaller functions. handleActivityCreated(@onNull Activity launchedActivity)229 void handleActivityCreated(@NonNull Activity launchedActivity) { 230 final List<EmbeddingRule> splitRules = getSplitRules(); 231 final TaskFragmentContainer currentContainer = getContainerWithActivity( 232 launchedActivity.getActivityToken()); 233 234 if (currentContainer == null) { 235 // Initial check before any TaskFragment is created. 236 onParentBoundsMayChange(launchedActivity); 237 } 238 239 // Check if the activity is configured to always be expanded. 240 if (shouldExpand(launchedActivity, null, splitRules)) { 241 if (shouldContainerBeExpanded(currentContainer)) { 242 // Make sure that the existing container is expanded 243 mPresenter.expandTaskFragment(currentContainer.getTaskFragmentToken()); 244 } else { 245 // Put activity into a new expanded container 246 final TaskFragmentContainer newContainer = newContainer(launchedActivity); 247 mPresenter.expandActivity(newContainer.getTaskFragmentToken(), 248 launchedActivity); 249 } 250 return; 251 } 252 253 // Check if activity requires a placeholder 254 if (launchPlaceholderIfNecessary(launchedActivity)) { 255 return; 256 } 257 258 // TODO(b/190433398): Check if it is a placeholder and there is already another split 259 // created by the primary activity. This is necessary for the case when the primary activity 260 // launched another secondary in the split, but the placeholder was still launched by the 261 // logic above. We didn't prevent the placeholder launcher because we didn't know that 262 // another secondary activity is coming up. 263 264 // Check if the activity should form a split with the activity below in the same task 265 // fragment. 266 Activity activityBelow = null; 267 if (currentContainer != null) { 268 final List<Activity> containerActivities = currentContainer.collectActivities(); 269 final int index = containerActivities.indexOf(launchedActivity); 270 if (index > 0) { 271 activityBelow = containerActivities.get(index - 1); 272 } 273 } 274 if (activityBelow == null) { 275 IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow( 276 launchedActivity.getActivityToken()); 277 if (belowToken != null) { 278 activityBelow = ActivityThread.currentActivityThread().getActivity(belowToken); 279 } 280 } 281 if (activityBelow == null) { 282 return; 283 } 284 285 // Check if the split is already set. 286 final TaskFragmentContainer activityBelowContainer = getContainerWithActivity( 287 activityBelow.getActivityToken()); 288 if (currentContainer != null && activityBelowContainer != null) { 289 final SplitContainer existingSplit = getActiveSplitForContainers(currentContainer, 290 activityBelowContainer); 291 if (existingSplit != null) { 292 // There is already an active split with the activity below. 293 return; 294 } 295 } 296 297 final SplitPairRule splitPairRule = getSplitRule(activityBelow, launchedActivity, 298 splitRules); 299 if (splitPairRule == null) { 300 return; 301 } 302 303 mPresenter.createNewSplitContainer(activityBelow, launchedActivity, 304 splitPairRule); 305 } 306 onActivityConfigurationChanged(@onNull Activity activity)307 private void onActivityConfigurationChanged(@NonNull Activity activity) { 308 final TaskFragmentContainer currentContainer = getContainerWithActivity( 309 activity.getActivityToken()); 310 311 if (currentContainer != null) { 312 // Changes to activities in controllers are handled in 313 // onTaskFragmentParentInfoChanged 314 return; 315 } 316 // The bounds of the container may have been changed. 317 onParentBoundsMayChange(activity); 318 319 // Check if activity requires a placeholder 320 launchPlaceholderIfNecessary(activity); 321 } 322 323 /** 324 * Returns a container that this activity is registered with. An activity can only belong to one 325 * container, or no container at all. 326 */ 327 @Nullable getContainerWithActivity(@onNull IBinder activityToken)328 TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { 329 for (TaskFragmentContainer container : mContainers) { 330 if (container.hasActivity(activityToken)) { 331 return container; 332 } 333 } 334 335 return null; 336 } 337 338 /** 339 * Creates and registers a new organized container with an optional activity that will be 340 * re-parented to it in a WCT. 341 */ newContainer(@ullable Activity activity)342 TaskFragmentContainer newContainer(@Nullable Activity activity) { 343 TaskFragmentContainer container = new TaskFragmentContainer(activity); 344 mContainers.add(container); 345 return container; 346 } 347 348 /** 349 * Creates and registers a new split with the provided containers and configuration. Finishes 350 * existing secondary containers if found for the given primary container. 351 */ registerSplit(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity, @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule)352 void registerSplit(@NonNull WindowContainerTransaction wct, 353 @NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity, 354 @NonNull TaskFragmentContainer secondaryContainer, 355 @NonNull SplitRule splitRule) { 356 SplitContainer splitContainer = new SplitContainer(primaryContainer, primaryActivity, 357 secondaryContainer, splitRule); 358 // Remove container later to prevent pinning escaping toast showing in lock task mode. 359 if (splitRule instanceof SplitPairRule && ((SplitPairRule) splitRule).shouldClearTop()) { 360 removeExistingSecondaryContainers(wct, primaryContainer); 361 } 362 mSplitContainers.add(splitContainer); 363 } 364 365 /** 366 * Removes the container from bookkeeping records. 367 */ removeContainer(@onNull TaskFragmentContainer container)368 void removeContainer(@NonNull TaskFragmentContainer container) { 369 // Remove all split containers that included this one 370 mContainers.remove(container); 371 List<SplitContainer> containersToRemove = new ArrayList<>(); 372 for (SplitContainer splitContainer : mSplitContainers) { 373 if (container.equals(splitContainer.getSecondaryContainer()) 374 || container.equals(splitContainer.getPrimaryContainer())) { 375 containersToRemove.add(splitContainer); 376 } 377 } 378 mSplitContainers.removeAll(containersToRemove); 379 } 380 381 /** 382 * Removes a secondary container for the given primary container if an existing split is 383 * already registered. 384 */ removeExistingSecondaryContainers(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer)385 void removeExistingSecondaryContainers(@NonNull WindowContainerTransaction wct, 386 @NonNull TaskFragmentContainer primaryContainer) { 387 // If the primary container was already in a split - remove the secondary container that 388 // is now covered by the new one that replaced it. 389 final SplitContainer existingSplitContainer = getActiveSplitForContainer( 390 primaryContainer); 391 if (existingSplitContainer == null 392 || primaryContainer == existingSplitContainer.getSecondaryContainer()) { 393 return; 394 } 395 396 existingSplitContainer.getSecondaryContainer().finish( 397 false /* shouldFinishDependent */, mPresenter, wct, this); 398 } 399 400 /** 401 * Returns the topmost not finished container. 402 */ 403 @Nullable getTopActiveContainer()404 TaskFragmentContainer getTopActiveContainer() { 405 for (int i = mContainers.size() - 1; i >= 0; i--) { 406 TaskFragmentContainer container = mContainers.get(i); 407 if (!container.isFinished() && container.getTopNonFinishingActivity() != null) { 408 return container; 409 } 410 } 411 return null; 412 } 413 414 /** 415 * Updates the presentation of the container. If the container is part of the split or should 416 * have a placeholder, it will also update the other part of the split. 417 */ updateContainer(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container)418 void updateContainer(@NonNull WindowContainerTransaction wct, 419 @NonNull TaskFragmentContainer container) { 420 if (launchPlaceholderIfNecessary(container)) { 421 // Placeholder was launched, the positions will be updated when the activity is added 422 // to the secondary container. 423 return; 424 } 425 if (shouldContainerBeExpanded(container)) { 426 if (container.getInfo() != null) { 427 mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken()); 428 } 429 // If the info is not available yet the task fragment will be expanded when it's ready 430 return; 431 } 432 SplitContainer splitContainer = getActiveSplitForContainer(container); 433 if (splitContainer == null) { 434 return; 435 } 436 if (splitContainer != mSplitContainers.get(mSplitContainers.size() - 1)) { 437 // Skip position update - it isn't the topmost split. 438 return; 439 } 440 if (splitContainer.getPrimaryContainer().isEmpty() 441 || splitContainer.getSecondaryContainer().isEmpty()) { 442 // Skip position update - one or both containers are empty. 443 return; 444 } 445 if (dismissPlaceholderIfNecessary(splitContainer)) { 446 // Placeholder was finished, the positions will be updated when its container is emptied 447 return; 448 } 449 mPresenter.updateSplitContainer(splitContainer, container, wct); 450 } 451 452 /** 453 * Returns the top active split container that has the provided container, if available. 454 */ 455 @Nullable getActiveSplitForContainer(@onNull TaskFragmentContainer container)456 private SplitContainer getActiveSplitForContainer(@NonNull TaskFragmentContainer container) { 457 for (int i = mSplitContainers.size() - 1; i >= 0; i--) { 458 SplitContainer splitContainer = mSplitContainers.get(i); 459 if (container.equals(splitContainer.getSecondaryContainer()) 460 || container.equals(splitContainer.getPrimaryContainer())) { 461 return splitContainer; 462 } 463 } 464 return null; 465 } 466 467 /** 468 * Returns the active split that has the provided containers as primary and secondary or as 469 * secondary and primary, if available. 470 */ 471 @Nullable getActiveSplitForContainers( @onNull TaskFragmentContainer firstContainer, @NonNull TaskFragmentContainer secondContainer)472 private SplitContainer getActiveSplitForContainers( 473 @NonNull TaskFragmentContainer firstContainer, 474 @NonNull TaskFragmentContainer secondContainer) { 475 for (int i = mSplitContainers.size() - 1; i >= 0; i--) { 476 SplitContainer splitContainer = mSplitContainers.get(i); 477 final TaskFragmentContainer primary = splitContainer.getPrimaryContainer(); 478 final TaskFragmentContainer secondary = splitContainer.getSecondaryContainer(); 479 if ((firstContainer == secondary && secondContainer == primary) 480 || (firstContainer == primary && secondContainer == secondary)) { 481 return splitContainer; 482 } 483 } 484 return null; 485 } 486 487 /** 488 * Checks if the container requires a placeholder and launches it if necessary. 489 */ launchPlaceholderIfNecessary(@onNull TaskFragmentContainer container)490 private boolean launchPlaceholderIfNecessary(@NonNull TaskFragmentContainer container) { 491 final Activity topActivity = container.getTopNonFinishingActivity(); 492 if (topActivity == null) { 493 return false; 494 } 495 496 return launchPlaceholderIfNecessary(topActivity); 497 } 498 launchPlaceholderIfNecessary(@onNull Activity activity)499 boolean launchPlaceholderIfNecessary(@NonNull Activity activity) { 500 final TaskFragmentContainer container = getContainerWithActivity( 501 activity.getActivityToken()); 502 503 SplitContainer splitContainer = container != null ? getActiveSplitForContainer(container) 504 : null; 505 if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) { 506 // Don't launch placeholder in primary split container 507 return false; 508 } 509 510 // Check if there is enough space for launch 511 final SplitPlaceholderRule placeholderRule = getPlaceholderRule(activity); 512 if (placeholderRule == null || !mPresenter.shouldShowSideBySide( 513 mPresenter.getParentContainerBounds(activity), placeholderRule)) { 514 return false; 515 } 516 517 // TODO(b/190433398): Handle failed request 518 startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), null, 519 placeholderRule, null); 520 return true; 521 } 522 dismissPlaceholderIfNecessary(@onNull SplitContainer splitContainer)523 private boolean dismissPlaceholderIfNecessary(@NonNull SplitContainer splitContainer) { 524 if (!splitContainer.isPlaceholderContainer()) { 525 return false; 526 } 527 528 if (isStickyPlaceholderRule(splitContainer.getSplitRule())) { 529 // The placeholder should remain after it was first shown. 530 return false; 531 } 532 533 if (mPresenter.shouldShowSideBySide(splitContainer)) { 534 return false; 535 } 536 537 mPresenter.cleanupContainer(splitContainer.getSecondaryContainer(), 538 false /* shouldFinishDependent */); 539 return true; 540 } 541 542 /** 543 * Returns the rule to launch a placeholder for the activity with the provided component name 544 * if it is configured in the split config. 545 */ getPlaceholderRule(@onNull Activity activity)546 private SplitPlaceholderRule getPlaceholderRule(@NonNull Activity activity) { 547 for (EmbeddingRule rule : mSplitRules) { 548 if (!(rule instanceof SplitPlaceholderRule)) { 549 continue; 550 } 551 SplitPlaceholderRule placeholderRule = (SplitPlaceholderRule) rule; 552 if (placeholderRule.matchesActivity(activity)) { 553 return placeholderRule; 554 } 555 } 556 return null; 557 } 558 559 /** 560 * Notifies listeners about changes to split states if necessary. 561 */ updateCallbackIfNecessary()562 private void updateCallbackIfNecessary() { 563 if (mEmbeddingCallback == null) { 564 return; 565 } 566 if (!allActivitiesCreated()) { 567 return; 568 } 569 List<SplitInfo> currentSplitStates = getActiveSplitStates(); 570 if (currentSplitStates == null || mLastReportedSplitStates.equals(currentSplitStates)) { 571 return; 572 } 573 mLastReportedSplitStates.clear(); 574 mLastReportedSplitStates.addAll(currentSplitStates); 575 mEmbeddingCallback.accept(currentSplitStates); 576 } 577 578 /** 579 * @return a list of descriptors for currently active split states. If the value returned is 580 * null, that indicates that the active split states are in an intermediate state and should 581 * not be reported. 582 */ 583 @Nullable getActiveSplitStates()584 private List<SplitInfo> getActiveSplitStates() { 585 List<SplitInfo> splitStates = new ArrayList<>(); 586 for (SplitContainer container : mSplitContainers) { 587 if (container.getPrimaryContainer().isEmpty() 588 || container.getSecondaryContainer().isEmpty()) { 589 // We are in an intermediate state because either the split container is about to be 590 // removed or the primary or secondary container are about to receive an activity. 591 return null; 592 } 593 ActivityStack primaryContainer = container.getPrimaryContainer().toActivityStack(); 594 ActivityStack secondaryContainer = container.getSecondaryContainer().toActivityStack(); 595 SplitInfo splitState = new SplitInfo(primaryContainer, 596 secondaryContainer, 597 // Splits that are not showing side-by-side are reported as having 0 split 598 // ratio, since by definition in the API the primary container occupies no 599 // width of the split when covered by the secondary. 600 mPresenter.shouldShowSideBySide(container) 601 ? container.getSplitRule().getSplitRatio() 602 : 0.0f); 603 splitStates.add(splitState); 604 } 605 return splitStates; 606 } 607 608 /** 609 * Checks if all activities that are registered with the containers have already appeared in 610 * the client. 611 */ allActivitiesCreated()612 private boolean allActivitiesCreated() { 613 for (TaskFragmentContainer container : mContainers) { 614 if (container.getInfo() == null 615 || container.getInfo().getActivities().size() 616 != container.collectActivities().size()) { 617 return false; 618 } 619 } 620 return true; 621 } 622 623 /** 624 * Returns {@code true} if the container is expanded to occupy full task size. 625 * Returns {@code false} if the container is included in an active split. 626 */ shouldContainerBeExpanded(@ullable TaskFragmentContainer container)627 boolean shouldContainerBeExpanded(@Nullable TaskFragmentContainer container) { 628 if (container == null) { 629 return false; 630 } 631 for (SplitContainer splitContainer : mSplitContainers) { 632 if (container.equals(splitContainer.getPrimaryContainer()) 633 || container.equals(splitContainer.getSecondaryContainer())) { 634 return false; 635 } 636 } 637 return true; 638 } 639 640 /** 641 * Returns a split rule for the provided pair of primary activity and secondary activity intent 642 * if available. 643 */ 644 @Nullable getSplitRule(@onNull Activity primaryActivity, @NonNull Intent secondaryActivityIntent, @NonNull List<EmbeddingRule> splitRules)645 private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity, 646 @NonNull Intent secondaryActivityIntent, @NonNull List<EmbeddingRule> splitRules) { 647 for (EmbeddingRule rule : splitRules) { 648 if (!(rule instanceof SplitPairRule)) { 649 continue; 650 } 651 SplitPairRule pairRule = (SplitPairRule) rule; 652 if (pairRule.matchesActivityIntentPair(primaryActivity, secondaryActivityIntent)) { 653 return pairRule; 654 } 655 } 656 return null; 657 } 658 659 /** 660 * Returns a split rule for the provided pair of primary and secondary activities if available. 661 */ 662 @Nullable getSplitRule(@onNull Activity primaryActivity, @NonNull Activity secondaryActivity, @NonNull List<EmbeddingRule> splitRules)663 private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity, 664 @NonNull Activity secondaryActivity, @NonNull List<EmbeddingRule> splitRules) { 665 for (EmbeddingRule rule : splitRules) { 666 if (!(rule instanceof SplitPairRule)) { 667 continue; 668 } 669 SplitPairRule pairRule = (SplitPairRule) rule; 670 final Intent intent = secondaryActivity.getIntent(); 671 if (pairRule.matchesActivityPair(primaryActivity, secondaryActivity) 672 && (intent == null 673 || pairRule.matchesActivityIntentPair(primaryActivity, intent))) { 674 return pairRule; 675 } 676 } 677 return null; 678 } 679 680 @Nullable getContainer(@onNull IBinder fragmentToken)681 TaskFragmentContainer getContainer(@NonNull IBinder fragmentToken) { 682 for (TaskFragmentContainer container : mContainers) { 683 if (container.getTaskFragmentToken().equals(fragmentToken)) { 684 return container; 685 } 686 } 687 return null; 688 } 689 690 /** 691 * Returns {@code true} if an Activity with the provided component name should always be 692 * expanded to occupy full task bounds. Such activity must not be put in a split. 693 */ shouldExpand(@ullable Activity activity, @Nullable Intent intent, List<EmbeddingRule> splitRules)694 private static boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent, 695 List<EmbeddingRule> splitRules) { 696 if (splitRules == null) { 697 return false; 698 } 699 for (EmbeddingRule rule : splitRules) { 700 if (!(rule instanceof ActivityRule)) { 701 continue; 702 } 703 ActivityRule activityRule = (ActivityRule) rule; 704 if (!activityRule.shouldAlwaysExpand()) { 705 continue; 706 } 707 if (activity != null && activityRule.matchesActivity(activity)) { 708 return true; 709 } else if (intent != null && activityRule.matchesIntent(intent)) { 710 return true; 711 } 712 } 713 return false; 714 } 715 716 /** 717 * Checks whether the associated container should be destroyed together with a finishing 718 * container. There is a case when primary containers for placeholders should be retained 719 * despite the rule configuration to finish primary with secondary - if they are marked as 720 * 'sticky' and the placeholder was finished when fully overlapping the primary container. 721 * @return {@code true} if the associated container should be retained (and not be finished). 722 */ shouldRetainAssociatedContainer(@onNull TaskFragmentContainer finishingContainer, @NonNull TaskFragmentContainer associatedContainer)723 boolean shouldRetainAssociatedContainer(@NonNull TaskFragmentContainer finishingContainer, 724 @NonNull TaskFragmentContainer associatedContainer) { 725 SplitContainer splitContainer = getActiveSplitForContainers(associatedContainer, 726 finishingContainer); 727 if (splitContainer == null) { 728 // Containers are not in the same split, no need to retain. 729 return false; 730 } 731 // Find the finish behavior for the associated container 732 int finishBehavior; 733 SplitRule splitRule = splitContainer.getSplitRule(); 734 if (finishingContainer == splitContainer.getPrimaryContainer()) { 735 finishBehavior = getFinishSecondaryWithPrimaryBehavior(splitRule); 736 } else { 737 finishBehavior = getFinishPrimaryWithSecondaryBehavior(splitRule); 738 } 739 // Decide whether the associated container should be retained based on the current 740 // presentation mode. 741 if (mPresenter.shouldShowSideBySide(splitContainer)) { 742 return !shouldFinishAssociatedContainerWhenAdjacent(finishBehavior); 743 } else { 744 return !shouldFinishAssociatedContainerWhenStacked(finishBehavior); 745 } 746 } 747 748 /** 749 * @see #shouldRetainAssociatedContainer(TaskFragmentContainer, TaskFragmentContainer) 750 */ shouldRetainAssociatedActivity(@onNull TaskFragmentContainer finishingContainer, @NonNull Activity associatedActivity)751 boolean shouldRetainAssociatedActivity(@NonNull TaskFragmentContainer finishingContainer, 752 @NonNull Activity associatedActivity) { 753 TaskFragmentContainer associatedContainer = getContainerWithActivity( 754 associatedActivity.getActivityToken()); 755 if (associatedContainer == null) { 756 return false; 757 } 758 759 return shouldRetainAssociatedContainer(finishingContainer, associatedContainer); 760 } 761 762 private final class LifecycleCallbacks implements ActivityLifecycleCallbacks { 763 764 @Override onActivityCreated(Activity activity, Bundle savedInstanceState)765 public void onActivityCreated(Activity activity, Bundle savedInstanceState) { 766 } 767 768 @Override onActivityPostCreated(Activity activity, Bundle savedInstanceState)769 public void onActivityPostCreated(Activity activity, Bundle savedInstanceState) { 770 // Calling after Activity#onCreate is complete to allow the app launch something 771 // first. In case of a configured placeholder activity we want to make sure 772 // that we don't launch it if an activity itself already requested something to be 773 // launched to side. 774 SplitController.this.onActivityCreated(activity); 775 } 776 777 @Override onActivityStarted(Activity activity)778 public void onActivityStarted(Activity activity) { 779 } 780 781 @Override onActivityResumed(Activity activity)782 public void onActivityResumed(Activity activity) { 783 } 784 785 @Override onActivityPaused(Activity activity)786 public void onActivityPaused(Activity activity) { 787 } 788 789 @Override onActivityStopped(Activity activity)790 public void onActivityStopped(Activity activity) { 791 } 792 793 @Override onActivitySaveInstanceState(Activity activity, Bundle outState)794 public void onActivitySaveInstanceState(Activity activity, Bundle outState) { 795 } 796 797 @Override onActivityDestroyed(Activity activity)798 public void onActivityDestroyed(Activity activity) { 799 } 800 801 @Override onActivityConfigurationChanged(Activity activity)802 public void onActivityConfigurationChanged(Activity activity) { 803 SplitController.this.onActivityConfigurationChanged(activity); 804 } 805 } 806 807 /** Executor that posts on the main application thread. */ 808 private static class MainThreadExecutor implements Executor { 809 private final Handler mHandler = new Handler(Looper.getMainLooper()); 810 811 @Override execute(Runnable r)812 public void execute(Runnable r) { 813 mHandler.post(r); 814 } 815 } 816 817 /** 818 * A monitor that intercepts all activity start requests originating in the client process and 819 * can amend them to target a specific task fragment to form a split. 820 */ 821 private class ActivityStartMonitor extends Instrumentation.ActivityMonitor { 822 823 @Override onStartActivity(@onNull Context who, @NonNull Intent intent, @NonNull Bundle options)824 public Instrumentation.ActivityResult onStartActivity(@NonNull Context who, 825 @NonNull Intent intent, @NonNull Bundle options) { 826 // TODO(b/190433398): Check if the activity is configured to always be expanded. 827 828 // Check if activity should be put in a split with the activity that launched it. 829 if (!(who instanceof Activity)) { 830 return super.onStartActivity(who, intent, options); 831 } 832 final Activity launchingActivity = (Activity) who; 833 834 if (shouldExpand(null, intent, getSplitRules())) { 835 setLaunchingInExpandedContainer(launchingActivity, options); 836 } else if (!setLaunchingToSideContainer(launchingActivity, intent, options)) { 837 setLaunchingInSameContainer(launchingActivity, intent, options); 838 } 839 840 return super.onStartActivity(who, intent, options); 841 } 842 setLaunchingInExpandedContainer(Activity launchingActivity, Bundle options)843 private void setLaunchingInExpandedContainer(Activity launchingActivity, Bundle options) { 844 TaskFragmentContainer newContainer = mPresenter.createNewExpandedContainer( 845 launchingActivity); 846 847 // Amend the request to let the WM know that the activity should be placed in the 848 // dedicated container. 849 options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, 850 newContainer.getTaskFragmentToken()); 851 } 852 853 /** 854 * Returns {@code true} if the activity that is going to be started via the 855 * {@code intent} should be paired with the {@code launchingActivity} and is set to be 856 * launched in an empty side container. 857 */ setLaunchingToSideContainer(Activity launchingActivity, Intent intent, Bundle options)858 private boolean setLaunchingToSideContainer(Activity launchingActivity, Intent intent, 859 Bundle options) { 860 final SplitPairRule splitPairRule = getSplitRule(launchingActivity, intent, 861 getSplitRules()); 862 if (splitPairRule == null) { 863 return false; 864 } 865 866 // Create a new split with an empty side container 867 final TaskFragmentContainer secondaryContainer = mPresenter 868 .createNewSplitWithEmptySideContainer(launchingActivity, splitPairRule); 869 870 // Amend the request to let the WM know that the activity should be placed in the 871 // dedicated container. 872 options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, 873 secondaryContainer.getTaskFragmentToken()); 874 return true; 875 } 876 877 /** 878 * Checks if the activity that is going to be started via the {@code intent} should be 879 * paired with the existing top activity which is currently paired with the 880 * {@code launchingActivity}. If so, set the activity to be launched in the same 881 * container of the {@code launchingActivity}. 882 */ setLaunchingInSameContainer(Activity launchingActivity, Intent intent, Bundle options)883 private void setLaunchingInSameContainer(Activity launchingActivity, Intent intent, 884 Bundle options) { 885 final TaskFragmentContainer launchingContainer = getContainerWithActivity( 886 launchingActivity.getActivityToken()); 887 if (launchingContainer == null) { 888 return; 889 } 890 891 final SplitContainer splitContainer = getActiveSplitForContainer(launchingContainer); 892 if (splitContainer == null) { 893 return; 894 } 895 896 if (splitContainer.getSecondaryContainer() != launchingContainer) { 897 return; 898 } 899 900 // The launching activity is on the secondary container. Retrieve the primary 901 // activity from the other container. 902 Activity primaryActivity = 903 splitContainer.getPrimaryContainer().getTopNonFinishingActivity(); 904 if (primaryActivity == null) { 905 return; 906 } 907 908 final SplitPairRule splitPairRule = getSplitRule(primaryActivity, intent, 909 getSplitRules()); 910 if (splitPairRule == null) { 911 return; 912 } 913 914 // Amend the request to let the WM know that the activity should be placed in the 915 // dedicated container. This is necessary for the case that the activity is started 916 // into a new Task, or new Task will be escaped from the current host Task and be 917 // displayed in fullscreen. 918 options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, 919 launchingContainer.getTaskFragmentToken()); 920 } 921 } 922 923 /** 924 * Checks if an activity is embedded and its presentation is customized by a 925 * {@link android.window.TaskFragmentOrganizer} to only occupy a portion of Task bounds. 926 */ isActivityEmbedded(@onNull Activity activity)927 public boolean isActivityEmbedded(@NonNull Activity activity) { 928 return mPresenter.isActivityEmbedded(activity.getActivityToken()); 929 } 930 } 931