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 com.android.wm.shell.compatui; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 20 21 import static com.android.wm.shell.compatui.impl.CompatUIRequestsKt.DISPLAY_COMPAT_SHOW_RESTART_DIALOG; 22 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.app.TaskInfo; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.res.Configuration; 30 import android.hardware.display.DisplayManager; 31 import android.net.Uri; 32 import android.os.UserHandle; 33 import android.provider.Settings; 34 import android.util.ArraySet; 35 import android.util.Log; 36 import android.util.Pair; 37 import android.util.SparseArray; 38 import android.view.Display; 39 import android.view.InsetsSourceControl; 40 import android.view.InsetsState; 41 import android.view.accessibility.AccessibilityManager; 42 import android.window.DesktopModeFlags; 43 44 import com.android.internal.annotations.VisibleForTesting; 45 import com.android.wm.shell.ShellTaskOrganizer; 46 import com.android.wm.shell.common.DisplayController; 47 import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener; 48 import com.android.wm.shell.common.DisplayImeController; 49 import com.android.wm.shell.common.DisplayInsetsController; 50 import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; 51 import com.android.wm.shell.common.DisplayLayout; 52 import com.android.wm.shell.common.DockStateReader; 53 import com.android.wm.shell.common.ShellExecutor; 54 import com.android.wm.shell.common.SyncTransactionQueue; 55 import com.android.wm.shell.compatui.api.CompatUIEvent; 56 import com.android.wm.shell.compatui.api.CompatUIHandler; 57 import com.android.wm.shell.compatui.api.CompatUIInfo; 58 import com.android.wm.shell.compatui.api.CompatUIRequest; 59 import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonClicked; 60 import com.android.wm.shell.compatui.impl.CompatUIRequests; 61 import com.android.wm.shell.desktopmode.DesktopUserRepositories; 62 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; 63 import com.android.wm.shell.sysui.KeyguardChangeListener; 64 import com.android.wm.shell.sysui.ShellController; 65 import com.android.wm.shell.sysui.ShellInit; 66 import com.android.wm.shell.transition.Transitions; 67 68 import dagger.Lazy; 69 70 import java.lang.ref.WeakReference; 71 import java.util.ArrayList; 72 import java.util.HashSet; 73 import java.util.List; 74 import java.util.Optional; 75 import java.util.Set; 76 import java.util.function.Consumer; 77 import java.util.function.Function; 78 import java.util.function.Predicate; 79 80 /** 81 * Controller to show/update compat UI components on Tasks based on whether the foreground 82 * activities are in compatibility mode. 83 */ 84 public class CompatUIController implements OnDisplaysChangedListener, 85 DisplayImeController.ImePositionProcessor, KeyguardChangeListener, CompatUIHandler { 86 87 private static final String TAG = "CompatUIController"; 88 89 // The time to wait before education and button hiding 90 private static final int DISAPPEAR_DELAY_MS = 5000; 91 92 /** Whether the IME is shown on display id. */ 93 private final Set<Integer> mDisplaysWithIme = new ArraySet<>(1); 94 95 /** {@link PerDisplayOnInsetsChangedListener} by display id. */ 96 private final SparseArray<PerDisplayOnInsetsChangedListener> mOnInsetsChangedListeners = 97 new SparseArray<>(0); 98 99 /** 100 * The active Compat Control UI layouts by task id. 101 * 102 * <p>An active layout is a layout that is eligible to be shown for the associated task but 103 * isn't necessarily shown at a given time. 104 */ 105 private final SparseArray<CompatUIWindowManager> mActiveCompatLayouts = new SparseArray<>(0); 106 107 /** 108 * {@link SparseArray} that maps task ids to {@link RestartDialogWindowManager} that are 109 * currently visible 110 */ 111 private final SparseArray<RestartDialogWindowManager> mTaskIdToRestartDialogWindowManagerMap = 112 new SparseArray<>(0); 113 114 /** 115 * {@link SparseArray} that maps task ids to {@link CompatUIInfo}. 116 */ 117 private final SparseArray<CompatUIInfo> mTaskIdToCompatUIInfoMap = 118 new SparseArray<>(0); 119 120 /** 121 * {@link Set} of task ids for which we need to display a restart confirmation dialog 122 */ 123 private Set<Integer> mSetOfTaskIdsShowingRestartDialog = new HashSet<>(); 124 125 /** 126 * The active user aspect ratio settings button layout if there is one (there can be at most 127 * one active). 128 */ 129 @Nullable 130 private UserAspectRatioSettingsWindowManager mUserAspectRatioSettingsLayout; 131 132 /** 133 * The active Letterbox Education layout if there is one (there can be at most one active). 134 * 135 * <p>An active layout is a layout that is eligible to be shown for the associated task but 136 * isn't necessarily shown at a given time. 137 */ 138 @Nullable 139 private LetterboxEduWindowManager mActiveLetterboxEduLayout; 140 141 /** 142 * The active Reachability UI layout. 143 */ 144 @Nullable 145 private ReachabilityEduWindowManager mActiveReachabilityEduLayout; 146 147 /** Avoid creating display context frequently for non-default display. */ 148 private final SparseArray<WeakReference<Context>> mDisplayContextCache = new SparseArray<>(0); 149 150 @NonNull 151 private final Context mContext; 152 @NonNull 153 private final ShellController mShellController; 154 @NonNull 155 private final DisplayController mDisplayController; 156 @NonNull 157 private final DisplayInsetsController mDisplayInsetsController; 158 @NonNull 159 private final DisplayImeController mImeController; 160 @NonNull 161 private final SyncTransactionQueue mSyncQueue; 162 @NonNull 163 private final ShellExecutor mMainExecutor; 164 @NonNull 165 private final Lazy<Transitions> mTransitionsLazy; 166 @NonNull 167 private final DockStateReader mDockStateReader; 168 @NonNull 169 private final CompatUIConfiguration mCompatUIConfiguration; 170 // Only show each hint once automatically in the process life. 171 @NonNull 172 private final CompatUIHintsState mCompatUIHintsState; 173 @NonNull 174 private final CompatUIShellCommandHandler mCompatUIShellCommandHandler; 175 176 @NonNull 177 private final Function<Integer, Integer> mDisappearTimeSupplier; 178 179 @Nullable 180 private Consumer<CompatUIEvent> mCallback; 181 182 // Indicates if the keyguard is currently showing, in which case compat UIs shouldn't 183 // be shown. 184 private boolean mKeyguardShowing; 185 186 /** 187 * The id of the task for the application we're currently attempting to show the user aspect 188 * ratio settings button for, or have most recently shown the button for. 189 */ 190 private int mTopActivityTaskId; 191 192 /** 193 * Whether the user aspect ratio settings button has been shown for the current application 194 * associated with the task id stored in {@link CompatUIController#mTopActivityTaskId}. 195 */ 196 private boolean mHasShownUserAspectRatioSettingsButton = false; 197 198 /** 199 * This is true when the rechability education is displayed for the first time. 200 */ 201 private boolean mIsFirstReachabilityEducationRunning; 202 203 private boolean mIsInDesktopMode; 204 205 @NonNull 206 private final CompatUIStatusManager mCompatUIStatusManager; 207 208 @NonNull 209 private final Optional<DesktopUserRepositories> mDesktopUserRepositories; 210 CompatUIController(@onNull Context context, @NonNull ShellInit shellInit, @NonNull ShellController shellController, @NonNull DisplayController displayController, @NonNull DisplayInsetsController displayInsetsController, @NonNull DisplayImeController imeController, @NonNull SyncTransactionQueue syncQueue, @NonNull ShellExecutor mainExecutor, @NonNull Lazy<Transitions> transitionsLazy, @NonNull DockStateReader dockStateReader, @NonNull CompatUIConfiguration compatUIConfiguration, @NonNull CompatUIShellCommandHandler compatUIShellCommandHandler, @NonNull AccessibilityManager accessibilityManager, @NonNull CompatUIStatusManager compatUIStatusManager, @NonNull Optional<DesktopUserRepositories> desktopUserRepositories)211 public CompatUIController(@NonNull Context context, 212 @NonNull ShellInit shellInit, 213 @NonNull ShellController shellController, 214 @NonNull DisplayController displayController, 215 @NonNull DisplayInsetsController displayInsetsController, 216 @NonNull DisplayImeController imeController, 217 @NonNull SyncTransactionQueue syncQueue, 218 @NonNull ShellExecutor mainExecutor, 219 @NonNull Lazy<Transitions> transitionsLazy, 220 @NonNull DockStateReader dockStateReader, 221 @NonNull CompatUIConfiguration compatUIConfiguration, 222 @NonNull CompatUIShellCommandHandler compatUIShellCommandHandler, 223 @NonNull AccessibilityManager accessibilityManager, 224 @NonNull CompatUIStatusManager compatUIStatusManager, 225 @NonNull Optional<DesktopUserRepositories> desktopUserRepositories) { 226 mContext = context; 227 mShellController = shellController; 228 mDisplayController = displayController; 229 mDisplayInsetsController = displayInsetsController; 230 mImeController = imeController; 231 mSyncQueue = syncQueue; 232 mMainExecutor = mainExecutor; 233 mTransitionsLazy = transitionsLazy; 234 mCompatUIHintsState = new CompatUIHintsState(); 235 mDockStateReader = dockStateReader; 236 mCompatUIConfiguration = compatUIConfiguration; 237 mCompatUIShellCommandHandler = compatUIShellCommandHandler; 238 mDisappearTimeSupplier = flags -> accessibilityManager.getRecommendedTimeoutMillis( 239 DISAPPEAR_DELAY_MS, flags); 240 mCompatUIStatusManager = compatUIStatusManager; 241 mDesktopUserRepositories = desktopUserRepositories; 242 shellInit.addInitCallback(this::onInit, this); 243 } 244 onInit()245 private void onInit() { 246 mShellController.addKeyguardChangeListener(this); 247 mDisplayController.addDisplayWindowListener(this); 248 mImeController.addPositionProcessor(this); 249 mCompatUIShellCommandHandler.onInit(); 250 } 251 252 /** Sets the callback for UI interactions. */ 253 @Override setCallback(@ullable Consumer<CompatUIEvent> callback)254 public void setCallback(@Nullable Consumer<CompatUIEvent> callback) { 255 mCallback = callback; 256 } 257 258 @Override sendCompatUIRequest(CompatUIRequest compatUIRequest)259 public void sendCompatUIRequest(CompatUIRequest compatUIRequest) { 260 switch(compatUIRequest.getRequestId()) { 261 case DISPLAY_COMPAT_SHOW_RESTART_DIALOG: 262 handleDisplayCompatShowRestartDialog(compatUIRequest.asType()); 263 break; 264 default: 265 } 266 } 267 handleDisplayCompatShowRestartDialog( CompatUIRequests.DisplayCompatShowRestartDialog request)268 private void handleDisplayCompatShowRestartDialog( 269 CompatUIRequests.DisplayCompatShowRestartDialog request) { 270 final CompatUIInfo compatUIInfo = mTaskIdToCompatUIInfoMap.get(request.getTaskId()); 271 if (compatUIInfo == null) { 272 return; 273 } 274 onRestartButtonClicked(new Pair<>(compatUIInfo.getTaskInfo(), compatUIInfo.getListener())); 275 } 276 277 /** 278 * Called when the Task info changed. Creates and updates the compat UI if there is an 279 * activity in size compat, or removes the UI if there is no size compat activity. 280 * 281 * @param compatUIInfo {@link CompatUIInfo} encapsulates information about the task and listener 282 */ onCompatInfoChanged(@onNull CompatUIInfo compatUIInfo)283 public void onCompatInfoChanged(@NonNull CompatUIInfo compatUIInfo) { 284 final TaskInfo taskInfo = compatUIInfo.getTaskInfo(); 285 final ShellTaskOrganizer.TaskListener taskListener = compatUIInfo.getListener(); 286 if (taskListener == null) { 287 mTaskIdToCompatUIInfoMap.delete(taskInfo.taskId); 288 } else { 289 mTaskIdToCompatUIInfoMap.put(taskInfo.taskId, compatUIInfo); 290 } 291 final boolean isInDisplayCompatMode = 292 taskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove(); 293 if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat() 294 && !isInDisplayCompatMode) { 295 mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId); 296 } 297 mIsInDesktopMode = isInDesktopMode(taskInfo); 298 // We close all the Compat UI educations in case TaskInfo has no configuration or 299 // TaskListener or in desktop mode. 300 if (taskInfo.configuration == null || taskListener == null 301 || (mIsInDesktopMode && !isInDisplayCompatMode)) { 302 // Null token means the current foreground activity is not in compatibility mode. 303 removeLayouts(taskInfo.taskId); 304 return; 305 } 306 if (taskInfo != null && taskListener != null) { 307 updateActiveTaskInfo(taskInfo); 308 } 309 310 // We're showing the first reachability education so we ignore incoming TaskInfo 311 // until the education flow has completed or we double tap. The double-tap 312 // basically cancel all the onboarding flow. We don't have to ignore events in case 313 // the app is in size compat mode. 314 if (mIsFirstReachabilityEducationRunning) { 315 if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap() 316 && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()) { 317 return; 318 } 319 mIsFirstReachabilityEducationRunning = false; 320 } 321 if (taskInfo.appCompatTaskInfo.isTopActivityLetterboxed()) { 322 if (taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled()) { 323 createOrUpdateLetterboxEduLayout(taskInfo, taskListener); 324 } else if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap()) { 325 // In this case the app is letterboxed and the letterbox education 326 // is disabled. In this case we need to understand if it's the first 327 // time we show the reachability education. When this is happening 328 // we need to ignore all the incoming TaskInfo until the education 329 // completes. If we come from a double tap we follow the normal flow. 330 final boolean topActivityPillarboxed = 331 taskInfo.appCompatTaskInfo.isTopActivityPillarboxShaped(); 332 final boolean isFirstTimeHorizontalReachabilityEdu = topActivityPillarboxed 333 && !mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(taskInfo); 334 final boolean isFirstTimeVerticalReachabilityEdu = !topActivityPillarboxed 335 && !mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(taskInfo); 336 if (isFirstTimeHorizontalReachabilityEdu || isFirstTimeVerticalReachabilityEdu) { 337 mCompatUIConfiguration.setSeenLetterboxEducation(taskInfo.userId); 338 // We activate the first reachability education if the double-tap is enabled. 339 // If the double tap is not enabled (e.g. thin letterbox) we just set the value 340 // of the education being seen. 341 if (taskInfo.appCompatTaskInfo.isLetterboxDoubleTapEnabled()) { 342 mIsFirstReachabilityEducationRunning = true; 343 createOrUpdateReachabilityEduLayout(taskInfo, taskListener); 344 return; 345 } 346 } 347 } 348 } 349 createOrUpdateCompatLayout(taskInfo, taskListener); 350 createOrUpdateRestartDialogLayout(taskInfo, taskListener); 351 if (mCompatUIConfiguration.getHasSeenLetterboxEducation(taskInfo.userId)) { 352 if (taskInfo.appCompatTaskInfo.isLetterboxDoubleTapEnabled()) { 353 createOrUpdateReachabilityEduLayout(taskInfo, taskListener); 354 } 355 // The user aspect ratio button should not be handled when a new TaskInfo is 356 // sent because of a double tap or when in multi-window mode. 357 if (taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { 358 if (mUserAspectRatioSettingsLayout != null) { 359 mUserAspectRatioSettingsLayout.release(); 360 mUserAspectRatioSettingsLayout = null; 361 } 362 return; 363 } 364 if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap()) { 365 createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener); 366 } 367 } 368 } 369 370 @Override onDisplayAdded(int displayId)371 public void onDisplayAdded(int displayId) { 372 addOnInsetsChangedListener(displayId); 373 } 374 375 @Override onDisplayRemoved(int displayId)376 public void onDisplayRemoved(int displayId) { 377 mDisplayContextCache.remove(displayId); 378 removeOnInsetsChangedListener(displayId); 379 380 // Remove all compat UIs on the removed display. 381 final List<Integer> toRemoveTaskIds = new ArrayList<>(); 382 forAllLayoutsOnDisplay(displayId, layout -> toRemoveTaskIds.add(layout.getTaskId())); 383 for (int i = toRemoveTaskIds.size() - 1; i >= 0; i--) { 384 removeLayouts(toRemoveTaskIds.get(i)); 385 } 386 } 387 addOnInsetsChangedListener(int displayId)388 private void addOnInsetsChangedListener(int displayId) { 389 PerDisplayOnInsetsChangedListener listener = new PerDisplayOnInsetsChangedListener( 390 displayId); 391 listener.register(); 392 mOnInsetsChangedListeners.put(displayId, listener); 393 } 394 removeOnInsetsChangedListener(int displayId)395 private void removeOnInsetsChangedListener(int displayId) { 396 PerDisplayOnInsetsChangedListener listener = mOnInsetsChangedListeners.get(displayId); 397 if (listener == null) { 398 return; 399 } 400 listener.unregister(); 401 mOnInsetsChangedListeners.remove(displayId); 402 } 403 404 @Override onDisplayConfigurationChanged(int displayId, Configuration newConfig)405 public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { 406 updateDisplayLayout(displayId); 407 } 408 updateDisplayLayout(int displayId)409 private void updateDisplayLayout(int displayId) { 410 final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(displayId); 411 forAllLayoutsOnDisplay(displayId, layout -> layout.updateDisplayLayout(displayLayout)); 412 } 413 414 @Override onImeVisibilityChanged(int displayId, boolean isShowing)415 public void onImeVisibilityChanged(int displayId, boolean isShowing) { 416 if (isShowing) { 417 mDisplaysWithIme.add(displayId); 418 } else { 419 mDisplaysWithIme.remove(displayId); 420 } 421 422 // Hide the compat UIs when input method is showing. 423 forAllLayoutsOnDisplay(displayId, 424 layout -> layout.updateVisibility(showOnDisplay(displayId))); 425 } 426 427 @Override onKeyguardVisibilityChanged(boolean visible, boolean occluded, boolean animatingDismiss)428 public void onKeyguardVisibilityChanged(boolean visible, boolean occluded, 429 boolean animatingDismiss) { 430 mKeyguardShowing = visible; 431 // Hide the compat UIs when keyguard is showing. 432 forAllLayouts(layout -> layout.updateVisibility(showOnDisplay(layout.getDisplayId()))); 433 } 434 435 /** 436 * Invoked when a new task is created or the info of an existing task has changed. Updates the 437 * shown status of the user aspect ratio settings button and the task id it relates to. 438 */ updateActiveTaskInfo(@onNull TaskInfo taskInfo)439 void updateActiveTaskInfo(@NonNull TaskInfo taskInfo) { 440 // If the activity belongs to the task we are currently tracking, don't update any variables 441 // as they are still relevant. Else, if the activity is visible and focused (the one the 442 // user can see and is using), the user aspect ratio button can potentially be displayed so 443 // start tracking the buttons visibility for this task. 444 if (mTopActivityTaskId != taskInfo.taskId 445 && !taskInfo.isTopActivityTransparent 446 && taskInfo.isVisible && taskInfo.isFocused) { 447 mTopActivityTaskId = taskInfo.taskId; 448 setHasShownUserAspectRatioSettingsButton(false); 449 } 450 } 451 452 /** 453 * Informs the system that the user aspect ratio button has been displayed for the application 454 * associated with the task id in {@link CompatUIController#mTopActivityTaskId}. 455 */ setHasShownUserAspectRatioSettingsButton(boolean state)456 void setHasShownUserAspectRatioSettingsButton(boolean state) { 457 mHasShownUserAspectRatioSettingsButton = state; 458 } 459 460 /** 461 * Returns whether the user aspect ratio settings button has been show for the application 462 * associated with the task id in {@link CompatUIController#mTopActivityTaskId}. 463 */ hasShownUserAspectRatioSettingsButton()464 boolean hasShownUserAspectRatioSettingsButton() { 465 return mHasShownUserAspectRatioSettingsButton; 466 } 467 468 /** 469 * Returns the task id of the application we are currently attempting to show, of have most 470 * recently shown, the user aspect ratio settings button for. 471 */ getTopActivityTaskId()472 int getTopActivityTaskId() { 473 return mTopActivityTaskId; 474 } 475 showOnDisplay(int displayId)476 private boolean showOnDisplay(int displayId) { 477 return !mKeyguardShowing && !isImeShowingOnDisplay(displayId); 478 } 479 isImeShowingOnDisplay(int displayId)480 private boolean isImeShowingOnDisplay(int displayId) { 481 return mDisplaysWithIme.contains(displayId); 482 } 483 createOrUpdateCompatLayout(@onNull TaskInfo taskInfo, @Nullable ShellTaskOrganizer.TaskListener taskListener)484 private void createOrUpdateCompatLayout(@NonNull TaskInfo taskInfo, 485 @Nullable ShellTaskOrganizer.TaskListener taskListener) { 486 CompatUIWindowManager layout = mActiveCompatLayouts.get(taskInfo.taskId); 487 if (layout != null) { 488 if (layout.needsToBeRecreated(taskInfo, taskListener) || mIsInDesktopMode) { 489 mActiveCompatLayouts.remove(taskInfo.taskId); 490 layout.release(); 491 } else { 492 // UI already exists, update the UI layout. 493 if (!layout.updateCompatInfo(taskInfo, taskListener, 494 showOnDisplay(layout.getDisplayId()))) { 495 // The layout is no longer eligible to be shown, remove from active layouts. 496 mActiveCompatLayouts.remove(taskInfo.taskId); 497 } 498 return; 499 } 500 } 501 if (mIsInDesktopMode) { 502 // Return if in desktop mode. 503 return; 504 } 505 // Create a new UI layout. 506 final Context context = getOrCreateDisplayContext(taskInfo.displayId); 507 if (context == null) { 508 return; 509 } 510 layout = createCompatUiWindowManager(context, taskInfo, taskListener); 511 if (layout.createLayout(showOnDisplay(taskInfo.displayId))) { 512 // The new layout is eligible to be shown, add it the active layouts. 513 mActiveCompatLayouts.put(taskInfo.taskId, layout); 514 } 515 } 516 517 @VisibleForTesting createCompatUiWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener)518 CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo, 519 ShellTaskOrganizer.TaskListener taskListener) { 520 return new CompatUIWindowManager(context, 521 taskInfo, mSyncQueue, mCallback, taskListener, 522 mDisplayController.getDisplayLayout(taskInfo.displayId), mCompatUIHintsState, 523 mCompatUIConfiguration, this::onRestartButtonClicked); 524 } 525 onRestartButtonClicked( Pair<TaskInfo, ShellTaskOrganizer.TaskListener> taskInfoState)526 private void onRestartButtonClicked( 527 Pair<TaskInfo, ShellTaskOrganizer.TaskListener> taskInfoState) { 528 if (mCompatUIConfiguration.isRestartDialogEnabled() 529 && mCompatUIConfiguration.shouldShowRestartDialogAgain( 530 taskInfoState.first)) { 531 // We need to show the dialog 532 mSetOfTaskIdsShowingRestartDialog.add(taskInfoState.first.taskId); 533 onCompatInfoChanged(new CompatUIInfo(taskInfoState.first, taskInfoState.second)); 534 } else { 535 mCallback.accept(new SizeCompatRestartButtonClicked(taskInfoState.first.taskId)); 536 } 537 } 538 createOrUpdateLetterboxEduLayout(@onNull TaskInfo taskInfo, @Nullable ShellTaskOrganizer.TaskListener taskListener)539 private void createOrUpdateLetterboxEduLayout(@NonNull TaskInfo taskInfo, 540 @Nullable ShellTaskOrganizer.TaskListener taskListener) { 541 if (mActiveLetterboxEduLayout != null) { 542 if (mActiveLetterboxEduLayout.needsToBeRecreated(taskInfo, taskListener) 543 || mIsInDesktopMode) { 544 mActiveLetterboxEduLayout.release(); 545 mActiveLetterboxEduLayout = null; 546 } else { 547 if (!mActiveLetterboxEduLayout.updateCompatInfo(taskInfo, taskListener, 548 showOnDisplay(mActiveLetterboxEduLayout.getDisplayId()))) { 549 // The layout is no longer eligible to be shown, clear active layout. 550 mActiveLetterboxEduLayout.release(); 551 mActiveLetterboxEduLayout = null; 552 } 553 return; 554 } 555 } 556 if (mIsInDesktopMode) { 557 // Return if in desktop mode. 558 return; 559 } 560 // Create a new UI layout. 561 final Context context = getOrCreateDisplayContext(taskInfo.displayId); 562 if (context == null) { 563 return; 564 } 565 LetterboxEduWindowManager newLayout = createLetterboxEduWindowManager(context, taskInfo, 566 taskListener); 567 if (newLayout.createLayout(showOnDisplay(taskInfo.displayId))) { 568 // The new layout is eligible to be shown, make it the active layout. 569 if (mActiveLetterboxEduLayout != null) { 570 // Release the previous layout since at most one can be active. 571 // Since letterbox education is only shown once to the user, releasing the previous 572 // layout is only a precaution. 573 mActiveLetterboxEduLayout.release(); 574 } 575 mActiveLetterboxEduLayout = newLayout; 576 } 577 } 578 579 @VisibleForTesting createLetterboxEduWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener)580 LetterboxEduWindowManager createLetterboxEduWindowManager(Context context, TaskInfo taskInfo, 581 ShellTaskOrganizer.TaskListener taskListener) { 582 return new LetterboxEduWindowManager(context, taskInfo, 583 mSyncQueue, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), 584 mTransitionsLazy.get(), 585 stateInfo -> createOrUpdateReachabilityEduLayout(stateInfo.first, stateInfo.second), 586 mDockStateReader, mCompatUIConfiguration, mCompatUIStatusManager); 587 } 588 createOrUpdateRestartDialogLayout(@onNull TaskInfo taskInfo, @Nullable ShellTaskOrganizer.TaskListener taskListener)589 private void createOrUpdateRestartDialogLayout(@NonNull TaskInfo taskInfo, 590 @Nullable ShellTaskOrganizer.TaskListener taskListener) { 591 RestartDialogWindowManager layout = 592 mTaskIdToRestartDialogWindowManagerMap.get(taskInfo.taskId); 593 final boolean isInNonDisplayCompatDesktopMode = mIsInDesktopMode 594 && !taskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove(); 595 if (layout != null) { 596 if (layout.needsToBeRecreated(taskInfo, taskListener) 597 || isInNonDisplayCompatDesktopMode) { 598 mTaskIdToRestartDialogWindowManagerMap.remove(taskInfo.taskId); 599 layout.release(); 600 } else { 601 layout.setRequestRestartDialog( 602 mSetOfTaskIdsShowingRestartDialog.contains(taskInfo.taskId)); 603 // UI already exists, update the UI layout. 604 if (!layout.updateCompatInfo(taskInfo, taskListener, 605 showOnDisplay(layout.getDisplayId()))) { 606 // The layout is no longer eligible to be shown, remove from active layouts. 607 mTaskIdToRestartDialogWindowManagerMap.remove(taskInfo.taskId); 608 } 609 return; 610 } 611 } 612 if (isInNonDisplayCompatDesktopMode) { 613 // No restart dialog can be shown in desktop mode unless the task is in display compat 614 // mode. 615 return; 616 } 617 // Create a new UI layout. 618 final Context context = getOrCreateDisplayContext(taskInfo.displayId); 619 if (context == null) { 620 return; 621 } 622 layout = createRestartDialogWindowManager(context, taskInfo, taskListener); 623 layout.setRequestRestartDialog( 624 mSetOfTaskIdsShowingRestartDialog.contains(taskInfo.taskId)); 625 if (layout.createLayout(showOnDisplay(taskInfo.displayId))) { 626 // The new layout is eligible to be shown, add it the active layouts. 627 mTaskIdToRestartDialogWindowManagerMap.put(taskInfo.taskId, layout); 628 } 629 } 630 631 @VisibleForTesting createRestartDialogWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener)632 RestartDialogWindowManager createRestartDialogWindowManager(Context context, TaskInfo taskInfo, 633 ShellTaskOrganizer.TaskListener taskListener) { 634 return new RestartDialogWindowManager(context, taskInfo, mSyncQueue, taskListener, 635 mDisplayController.getDisplayLayout(taskInfo.displayId), mTransitionsLazy.get(), 636 this::onRestartDialogCallback, this::onRestartDialogDismissCallback, 637 mCompatUIConfiguration); 638 } 639 onRestartDialogCallback( Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo)640 private void onRestartDialogCallback( 641 Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) { 642 mTaskIdToRestartDialogWindowManagerMap.remove(stateInfo.first.taskId); 643 mCallback.accept(new SizeCompatRestartButtonClicked(stateInfo.first.taskId)); 644 } 645 onRestartDialogDismissCallback( Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo)646 private void onRestartDialogDismissCallback( 647 Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) { 648 mSetOfTaskIdsShowingRestartDialog.remove(stateInfo.first.taskId); 649 onCompatInfoChanged(new CompatUIInfo(stateInfo.first, stateInfo.second)); 650 } 651 createOrUpdateReachabilityEduLayout(@onNull TaskInfo taskInfo, @Nullable ShellTaskOrganizer.TaskListener taskListener)652 private void createOrUpdateReachabilityEduLayout(@NonNull TaskInfo taskInfo, 653 @Nullable ShellTaskOrganizer.TaskListener taskListener) { 654 if (mActiveReachabilityEduLayout != null) { 655 if (mActiveReachabilityEduLayout.needsToBeRecreated(taskInfo, taskListener) 656 || mIsInDesktopMode) { 657 mActiveReachabilityEduLayout.release(); 658 mActiveReachabilityEduLayout = null; 659 } else { 660 // UI already exists, update the UI layout. 661 if (!mActiveReachabilityEduLayout.updateCompatInfo(taskInfo, taskListener, 662 showOnDisplay(mActiveReachabilityEduLayout.getDisplayId()))) { 663 // The layout is no longer eligible to be shown, remove from active layouts. 664 mActiveReachabilityEduLayout.release(); 665 mActiveReachabilityEduLayout = null; 666 } 667 return; 668 } 669 } 670 if (mIsInDesktopMode) { 671 // Return if in desktop mode. 672 return; 673 } 674 // Create a new UI layout. 675 final Context context = getOrCreateDisplayContext(taskInfo.displayId); 676 if (context == null) { 677 return; 678 } 679 ReachabilityEduWindowManager newLayout = createReachabilityEduWindowManager(context, 680 taskInfo, taskListener); 681 if (newLayout.createLayout(showOnDisplay(taskInfo.displayId))) { 682 // The new layout is eligible to be shown, make it the active layout. 683 if (mActiveReachabilityEduLayout != null) { 684 // Release the previous layout since at most one can be active. 685 // Since letterbox reachability education is only shown once to the user, 686 // releasing the previous layout is only a precaution. 687 mActiveReachabilityEduLayout.release(); 688 } 689 mActiveReachabilityEduLayout = newLayout; 690 } 691 } 692 693 @VisibleForTesting createReachabilityEduWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener)694 ReachabilityEduWindowManager createReachabilityEduWindowManager(Context context, 695 TaskInfo taskInfo, 696 ShellTaskOrganizer.TaskListener taskListener) { 697 return new ReachabilityEduWindowManager(context, taskInfo, mSyncQueue, 698 taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), 699 mCompatUIConfiguration, mMainExecutor, this::onInitialReachabilityEduDismissed, 700 mDisappearTimeSupplier); 701 } 702 onInitialReachabilityEduDismissed(@onNull TaskInfo taskInfo, @NonNull ShellTaskOrganizer.TaskListener taskListener)703 private void onInitialReachabilityEduDismissed(@NonNull TaskInfo taskInfo, 704 @NonNull ShellTaskOrganizer.TaskListener taskListener) { 705 // We need to update the UI otherwise it will not be shown until the user relaunches the app 706 mIsFirstReachabilityEducationRunning = false; 707 createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener); 708 } 709 createOrUpdateUserAspectRatioSettingsLayout(@onNull TaskInfo taskInfo, @Nullable ShellTaskOrganizer.TaskListener taskListener)710 private void createOrUpdateUserAspectRatioSettingsLayout(@NonNull TaskInfo taskInfo, 711 @Nullable ShellTaskOrganizer.TaskListener taskListener) { 712 boolean overridesShowAppHandle = DesktopModeStatus.overridesShowAppHandle(mContext); 713 if (mUserAspectRatioSettingsLayout != null) { 714 if (mUserAspectRatioSettingsLayout.needsToBeRecreated(taskInfo, taskListener) 715 || mIsInDesktopMode || overridesShowAppHandle) { 716 mUserAspectRatioSettingsLayout.release(); 717 mUserAspectRatioSettingsLayout = null; 718 } else { 719 // UI already exists, update the UI layout. 720 if (!mUserAspectRatioSettingsLayout.updateCompatInfo(taskInfo, taskListener, 721 showOnDisplay(mUserAspectRatioSettingsLayout.getDisplayId()))) { 722 mUserAspectRatioSettingsLayout.release(); 723 mUserAspectRatioSettingsLayout = null; 724 } 725 return; 726 } 727 } 728 if (mIsInDesktopMode || overridesShowAppHandle) { 729 // Return if in desktop mode or app handle menu is already showing change aspect ratio 730 // option. 731 return; 732 } 733 // Create a new UI layout. 734 final Context context = getOrCreateDisplayContext(taskInfo.displayId); 735 if (context == null) { 736 return; 737 } 738 final UserAspectRatioSettingsWindowManager newLayout = 739 createUserAspectRatioSettingsWindowManager(context, taskInfo, taskListener); 740 if (newLayout.createLayout(showOnDisplay(taskInfo.displayId))) { 741 // The new layout is eligible to be shown, add it the active layouts. 742 mUserAspectRatioSettingsLayout = newLayout; 743 } 744 } 745 746 @VisibleForTesting 747 @NonNull createUserAspectRatioSettingsWindowManager( @onNull Context context, @NonNull TaskInfo taskInfo, @Nullable ShellTaskOrganizer.TaskListener taskListener)748 UserAspectRatioSettingsWindowManager createUserAspectRatioSettingsWindowManager( 749 @NonNull Context context, @NonNull TaskInfo taskInfo, 750 @Nullable ShellTaskOrganizer.TaskListener taskListener) { 751 return new UserAspectRatioSettingsWindowManager(context, taskInfo, mSyncQueue, 752 taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), 753 mCompatUIHintsState, this::launchUserAspectRatioSettings, mMainExecutor, 754 mDisappearTimeSupplier, this::hasShownUserAspectRatioSettingsButton, 755 this::setHasShownUserAspectRatioSettingsButton); 756 } 757 launchUserAspectRatioSettings( @onNull TaskInfo taskInfo, @NonNull ShellTaskOrganizer.TaskListener taskListener)758 private void launchUserAspectRatioSettings( 759 @NonNull TaskInfo taskInfo, @NonNull ShellTaskOrganizer.TaskListener taskListener) { 760 launchUserAspectRatioSettings(mContext, taskInfo); 761 } 762 763 /** Launch the user aspect ratio settings for the package of the given task. */ launchUserAspectRatioSettings( @onNull Context context, @NonNull TaskInfo taskInfo)764 public static void launchUserAspectRatioSettings( 765 @NonNull Context context, @NonNull TaskInfo taskInfo) { 766 final Intent intent = new Intent(Settings.ACTION_MANAGE_USER_ASPECT_RATIO_SETTINGS); 767 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 768 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); 769 final ComponentName appComponent = taskInfo.topActivity; 770 if (appComponent != null) { 771 final Uri packageUri = Uri.parse("package:" + appComponent.getPackageName()); 772 intent.setData(packageUri); 773 } 774 final UserHandle userHandle = UserHandle.of(taskInfo.userId); 775 context.startActivityAsUser(intent, userHandle); 776 } 777 778 @VisibleForTesting removeLayouts(int taskId)779 void removeLayouts(int taskId) { 780 final CompatUIWindowManager compatLayout = mActiveCompatLayouts.get(taskId); 781 if (compatLayout != null) { 782 compatLayout.release(); 783 mActiveCompatLayouts.remove(taskId); 784 } 785 786 if (mActiveLetterboxEduLayout != null && mActiveLetterboxEduLayout.getTaskId() == taskId) { 787 mActiveLetterboxEduLayout.release(); 788 mActiveLetterboxEduLayout = null; 789 } 790 791 final RestartDialogWindowManager restartLayout = 792 mTaskIdToRestartDialogWindowManagerMap.get(taskId); 793 if (restartLayout != null) { 794 restartLayout.release(); 795 mTaskIdToRestartDialogWindowManagerMap.remove(taskId); 796 mSetOfTaskIdsShowingRestartDialog.remove(taskId); 797 } 798 if (mActiveReachabilityEduLayout != null 799 && mActiveReachabilityEduLayout.getTaskId() == taskId) { 800 mActiveReachabilityEduLayout.release(); 801 mActiveReachabilityEduLayout = null; 802 } 803 804 if (mUserAspectRatioSettingsLayout != null 805 && mUserAspectRatioSettingsLayout.getTaskId() == taskId) { 806 mUserAspectRatioSettingsLayout.release(); 807 mUserAspectRatioSettingsLayout = null; 808 } 809 } 810 getOrCreateDisplayContext(int displayId)811 private Context getOrCreateDisplayContext(int displayId) { 812 if (displayId == Display.DEFAULT_DISPLAY) { 813 return mContext; 814 } 815 Context context = null; 816 final WeakReference<Context> ref = mDisplayContextCache.get(displayId); 817 if (ref != null) { 818 context = ref.get(); 819 } 820 if (context == null) { 821 Display display = mContext.getSystemService(DisplayManager.class).getDisplay(displayId); 822 if (display != null) { 823 context = mContext.createDisplayContext(display); 824 mDisplayContextCache.put(displayId, new WeakReference<>(context)); 825 } else { 826 Log.e(TAG, "Cannot get context for display " + displayId); 827 } 828 } 829 return context; 830 } 831 forAllLayoutsOnDisplay(int displayId, Consumer<CompatUIWindowManagerAbstract> callback)832 private void forAllLayoutsOnDisplay(int displayId, 833 Consumer<CompatUIWindowManagerAbstract> callback) { 834 forAllLayouts(layout -> layout.getDisplayId() == displayId, callback); 835 } 836 forAllLayouts(Consumer<CompatUIWindowManagerAbstract> callback)837 private void forAllLayouts(Consumer<CompatUIWindowManagerAbstract> callback) { 838 forAllLayouts(layout -> true, callback); 839 } 840 forAllLayouts(Predicate<CompatUIWindowManagerAbstract> condition, Consumer<CompatUIWindowManagerAbstract> callback)841 private void forAllLayouts(Predicate<CompatUIWindowManagerAbstract> condition, 842 Consumer<CompatUIWindowManagerAbstract> callback) { 843 for (int i = 0; i < mActiveCompatLayouts.size(); i++) { 844 final int taskId = mActiveCompatLayouts.keyAt(i); 845 final CompatUIWindowManager layout = mActiveCompatLayouts.get(taskId); 846 if (layout != null && condition.test(layout)) { 847 callback.accept(layout); 848 } 849 } 850 if (mActiveLetterboxEduLayout != null && condition.test(mActiveLetterboxEduLayout)) { 851 callback.accept(mActiveLetterboxEduLayout); 852 } 853 for (int i = 0; i < mTaskIdToRestartDialogWindowManagerMap.size(); i++) { 854 final int taskId = mTaskIdToRestartDialogWindowManagerMap.keyAt(i); 855 final RestartDialogWindowManager layout = 856 mTaskIdToRestartDialogWindowManagerMap.get(taskId); 857 if (layout != null && condition.test(layout)) { 858 callback.accept(layout); 859 } 860 } 861 if (mActiveReachabilityEduLayout != null && condition.test(mActiveReachabilityEduLayout)) { 862 callback.accept(mActiveReachabilityEduLayout); 863 } 864 if (mUserAspectRatioSettingsLayout != null && condition.test( 865 mUserAspectRatioSettingsLayout)) { 866 callback.accept(mUserAspectRatioSettingsLayout); 867 } 868 } 869 870 /** An implementation of {@link OnInsetsChangedListener} for a given display id. */ 871 private class PerDisplayOnInsetsChangedListener implements OnInsetsChangedListener { 872 final int mDisplayId; 873 final InsetsState mInsetsState = new InsetsState(); 874 PerDisplayOnInsetsChangedListener(int displayId)875 PerDisplayOnInsetsChangedListener(int displayId) { 876 mDisplayId = displayId; 877 } 878 register()879 void register() { 880 mDisplayInsetsController.addInsetsChangedListener(mDisplayId, this); 881 } 882 unregister()883 void unregister() { 884 mDisplayInsetsController.removeInsetsChangedListener(mDisplayId, this); 885 } 886 887 @Override insetsChanged(InsetsState insetsState)888 public void insetsChanged(InsetsState insetsState) { 889 if (mInsetsState.equals(insetsState)) { 890 return; 891 } 892 mInsetsState.set(insetsState); 893 updateDisplayLayout(mDisplayId); 894 } 895 896 @Override insetsControlChanged(InsetsState insetsState, InsetsSourceControl[] activeControls)897 public void insetsControlChanged(InsetsState insetsState, 898 InsetsSourceControl[] activeControls) { 899 insetsChanged(insetsState); 900 } 901 } 902 903 /** 904 * A class holding the state of the compat UI hints, which is shared between all compat UI 905 * window managers. 906 */ 907 static class CompatUIHintsState { 908 boolean mHasShownSizeCompatHint; 909 boolean mHasShownUserAspectRatioSettingsButtonHint; 910 } 911 isInDesktopMode(@ullable TaskInfo taskInfo)912 private boolean isInDesktopMode(@Nullable TaskInfo taskInfo) { 913 if (mDesktopUserRepositories.isEmpty() || taskInfo == null) { 914 return false; 915 } 916 boolean isDesktopModeShowing = mDesktopUserRepositories.get().getCurrent() 917 .isAnyDeskActive(taskInfo.displayId); 918 return DesktopModeFlags.ENABLE_DESKTOP_SKIP_COMPAT_UI_EDUCATION_IN_DESKTOP_MODE_BUGFIX 919 .isTrue() && isDesktopModeShowing; 920 } 921 } 922