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