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.TaskInfo; 24 import android.app.TaskInfo.CameraCompatControlState; 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 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)191 public CompatUIController(@NonNull Context context, 192 @NonNull ShellInit shellInit, 193 @NonNull ShellController shellController, 194 @NonNull DisplayController displayController, 195 @NonNull DisplayInsetsController displayInsetsController, 196 @NonNull DisplayImeController imeController, 197 @NonNull SyncTransactionQueue syncQueue, 198 @NonNull ShellExecutor mainExecutor, 199 @NonNull Lazy<Transitions> transitionsLazy, 200 @NonNull DockStateReader dockStateReader, 201 @NonNull CompatUIConfiguration compatUIConfiguration, 202 @NonNull CompatUIShellCommandHandler compatUIShellCommandHandler, 203 @NonNull AccessibilityManager accessibilityManager) { 204 mContext = context; 205 mShellController = shellController; 206 mDisplayController = displayController; 207 mDisplayInsetsController = displayInsetsController; 208 mImeController = imeController; 209 mSyncQueue = syncQueue; 210 mMainExecutor = mainExecutor; 211 mTransitionsLazy = transitionsLazy; 212 mCompatUIHintsState = new CompatUIHintsState(); 213 mDockStateReader = dockStateReader; 214 mCompatUIConfiguration = compatUIConfiguration; 215 mCompatUIShellCommandHandler = compatUIShellCommandHandler; 216 mDisappearTimeSupplier = flags -> accessibilityManager.getRecommendedTimeoutMillis( 217 DISAPPEAR_DELAY_MS, flags); 218 shellInit.addInitCallback(this::onInit, this); 219 } 220 onInit()221 private void onInit() { 222 mShellController.addKeyguardChangeListener(this); 223 mDisplayController.addDisplayWindowListener(this); 224 mImeController.addPositionProcessor(this); 225 mCompatUIShellCommandHandler.onInit(); 226 } 227 228 /** Sets the callback for Compat UI interactions. */ setCompatUICallback(@onNull CompatUICallback compatUiCallback)229 public void setCompatUICallback(@NonNull CompatUICallback compatUiCallback) { 230 mCompatUICallback = compatUiCallback; 231 } 232 233 /** 234 * Called when the Task info changed. Creates and updates the compat UI if there is an 235 * activity in size compat, or removes the UI if there is no size compat activity. 236 * 237 * @param taskInfo {@link TaskInfo} task the activity is in. 238 * @param taskListener listener to handle the Task Surface placement. 239 */ onCompatInfoChanged(@onNull TaskInfo taskInfo, @Nullable ShellTaskOrganizer.TaskListener taskListener)240 public void onCompatInfoChanged(@NonNull TaskInfo taskInfo, 241 @Nullable ShellTaskOrganizer.TaskListener taskListener) { 242 if (taskInfo != null && !taskInfo.topActivityInSizeCompat) { 243 mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId); 244 } 245 246 if (taskInfo != null && taskListener != null) { 247 updateActiveTaskInfo(taskInfo); 248 } 249 250 if (taskInfo.configuration == null || taskListener == null) { 251 // Null token means the current foreground activity is not in compatibility mode. 252 removeLayouts(taskInfo.taskId); 253 return; 254 } 255 256 createOrUpdateCompatLayout(taskInfo, taskListener); 257 createOrUpdateLetterboxEduLayout(taskInfo, taskListener); 258 createOrUpdateRestartDialogLayout(taskInfo, taskListener); 259 if (mCompatUIConfiguration.getHasSeenLetterboxEducation(taskInfo.userId)) { 260 createOrUpdateReachabilityEduLayout(taskInfo, taskListener); 261 // The user aspect ratio button should not be handled when a new TaskInfo is 262 // sent because of a double tap or when in multi-window mode. 263 if (taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { 264 if (mUserAspectRatioSettingsLayout != null) { 265 mUserAspectRatioSettingsLayout.release(); 266 mUserAspectRatioSettingsLayout = null; 267 } 268 return; 269 } 270 if (!taskInfo.isFromLetterboxDoubleTap) { 271 createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener); 272 } 273 } 274 } 275 276 @Override onDisplayAdded(int displayId)277 public void onDisplayAdded(int displayId) { 278 addOnInsetsChangedListener(displayId); 279 } 280 281 @Override onDisplayRemoved(int displayId)282 public void onDisplayRemoved(int displayId) { 283 mDisplayContextCache.remove(displayId); 284 removeOnInsetsChangedListener(displayId); 285 286 // Remove all compat UIs on the removed display. 287 final List<Integer> toRemoveTaskIds = new ArrayList<>(); 288 forAllLayoutsOnDisplay(displayId, layout -> toRemoveTaskIds.add(layout.getTaskId())); 289 for (int i = toRemoveTaskIds.size() - 1; i >= 0; i--) { 290 removeLayouts(toRemoveTaskIds.get(i)); 291 } 292 } 293 addOnInsetsChangedListener(int displayId)294 private void addOnInsetsChangedListener(int displayId) { 295 PerDisplayOnInsetsChangedListener listener = new PerDisplayOnInsetsChangedListener( 296 displayId); 297 listener.register(); 298 mOnInsetsChangedListeners.put(displayId, listener); 299 } 300 removeOnInsetsChangedListener(int displayId)301 private void removeOnInsetsChangedListener(int displayId) { 302 PerDisplayOnInsetsChangedListener listener = mOnInsetsChangedListeners.get(displayId); 303 if (listener == null) { 304 return; 305 } 306 listener.unregister(); 307 mOnInsetsChangedListeners.remove(displayId); 308 } 309 310 311 @Override onDisplayConfigurationChanged(int displayId, Configuration newConfig)312 public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { 313 updateDisplayLayout(displayId); 314 } 315 updateDisplayLayout(int displayId)316 private void updateDisplayLayout(int displayId) { 317 final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(displayId); 318 forAllLayoutsOnDisplay(displayId, layout -> layout.updateDisplayLayout(displayLayout)); 319 } 320 321 @Override onImeVisibilityChanged(int displayId, boolean isShowing)322 public void onImeVisibilityChanged(int displayId, boolean isShowing) { 323 if (isShowing) { 324 mDisplaysWithIme.add(displayId); 325 } else { 326 mDisplaysWithIme.remove(displayId); 327 } 328 329 // Hide the compat UIs when input method is showing. 330 forAllLayoutsOnDisplay(displayId, 331 layout -> layout.updateVisibility(showOnDisplay(displayId))); 332 } 333 334 @Override onKeyguardVisibilityChanged(boolean visible, boolean occluded, boolean animatingDismiss)335 public void onKeyguardVisibilityChanged(boolean visible, boolean occluded, 336 boolean animatingDismiss) { 337 mKeyguardShowing = visible; 338 // Hide the compat UIs when keyguard is showing. 339 forAllLayouts(layout -> layout.updateVisibility(showOnDisplay(layout.getDisplayId()))); 340 } 341 342 /** 343 * Invoked when a new task is created or the info of an existing task has changed. Updates the 344 * shown status of the user aspect ratio settings button and the task id it relates to. 345 */ updateActiveTaskInfo(@onNull TaskInfo taskInfo)346 void updateActiveTaskInfo(@NonNull TaskInfo taskInfo) { 347 // If the activity belongs to the task we are currently tracking, don't update any variables 348 // as they are still relevant. Else, if the activity is visible and focused (the one the 349 // user can see and is using), the user aspect ratio button can potentially be displayed so 350 // start tracking the buttons visibility for this task. 351 if (mTopActivityTaskId != taskInfo.taskId && !taskInfo.isTopActivityTransparent 352 && taskInfo.isVisible && taskInfo.isFocused) { 353 mTopActivityTaskId = taskInfo.taskId; 354 setHasShownUserAspectRatioSettingsButton(false); 355 } 356 } 357 358 /** 359 * Informs the system that the user aspect ratio button has been displayed for the application 360 * associated with the task id in {@link CompatUIController#mTopActivityTaskId}. 361 */ setHasShownUserAspectRatioSettingsButton(boolean state)362 void setHasShownUserAspectRatioSettingsButton(boolean state) { 363 mHasShownUserAspectRatioSettingsButton = state; 364 } 365 366 /** 367 * Returns whether the user aspect ratio settings button has been show for the application 368 * associated with the task id in {@link CompatUIController#mTopActivityTaskId}. 369 */ hasShownUserAspectRatioSettingsButton()370 boolean hasShownUserAspectRatioSettingsButton() { 371 return mHasShownUserAspectRatioSettingsButton; 372 } 373 374 /** 375 * Returns the task id of the application we are currently attempting to show, of have most 376 * recently shown, the user aspect ratio settings button for. 377 */ getTopActivityTaskId()378 int getTopActivityTaskId() { 379 return mTopActivityTaskId; 380 } 381 showOnDisplay(int displayId)382 private boolean showOnDisplay(int displayId) { 383 return !mKeyguardShowing && !isImeShowingOnDisplay(displayId); 384 } 385 isImeShowingOnDisplay(int displayId)386 private boolean isImeShowingOnDisplay(int displayId) { 387 return mDisplaysWithIme.contains(displayId); 388 } 389 createOrUpdateCompatLayout(@onNull TaskInfo taskInfo, @Nullable ShellTaskOrganizer.TaskListener taskListener)390 private void createOrUpdateCompatLayout(@NonNull TaskInfo taskInfo, 391 @Nullable ShellTaskOrganizer.TaskListener taskListener) { 392 CompatUIWindowManager layout = mActiveCompatLayouts.get(taskInfo.taskId); 393 if (layout != null) { 394 if (layout.needsToBeRecreated(taskInfo, taskListener)) { 395 mActiveCompatLayouts.remove(taskInfo.taskId); 396 layout.release(); 397 } else { 398 // UI already exists, update the UI layout. 399 if (!layout.updateCompatInfo(taskInfo, taskListener, 400 showOnDisplay(layout.getDisplayId()))) { 401 // The layout is no longer eligible to be shown, remove from active layouts. 402 mActiveCompatLayouts.remove(taskInfo.taskId); 403 } 404 return; 405 } 406 } 407 408 // Create a new UI layout. 409 final Context context = getOrCreateDisplayContext(taskInfo.displayId); 410 if (context == null) { 411 return; 412 } 413 layout = createCompatUiWindowManager(context, taskInfo, taskListener); 414 if (layout.createLayout(showOnDisplay(taskInfo.displayId))) { 415 // The new layout is eligible to be shown, add it the active layouts. 416 mActiveCompatLayouts.put(taskInfo.taskId, layout); 417 } 418 } 419 420 @VisibleForTesting createCompatUiWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener)421 CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo, 422 ShellTaskOrganizer.TaskListener taskListener) { 423 return new CompatUIWindowManager(context, 424 taskInfo, mSyncQueue, mCompatUICallback, taskListener, 425 mDisplayController.getDisplayLayout(taskInfo.displayId), mCompatUIHintsState, 426 mCompatUIConfiguration, this::onRestartButtonClicked); 427 } 428 onRestartButtonClicked( Pair<TaskInfo, ShellTaskOrganizer.TaskListener> taskInfoState)429 private void onRestartButtonClicked( 430 Pair<TaskInfo, ShellTaskOrganizer.TaskListener> taskInfoState) { 431 if (mCompatUIConfiguration.isRestartDialogEnabled() 432 && mCompatUIConfiguration.shouldShowRestartDialogAgain( 433 taskInfoState.first)) { 434 // We need to show the dialog 435 mSetOfTaskIdsShowingRestartDialog.add(taskInfoState.first.taskId); 436 onCompatInfoChanged(taskInfoState.first, taskInfoState.second); 437 } else { 438 mCompatUICallback.onSizeCompatRestartButtonClicked(taskInfoState.first.taskId); 439 } 440 } 441 createOrUpdateLetterboxEduLayout(@onNull TaskInfo taskInfo, @Nullable ShellTaskOrganizer.TaskListener taskListener)442 private void createOrUpdateLetterboxEduLayout(@NonNull TaskInfo taskInfo, 443 @Nullable ShellTaskOrganizer.TaskListener taskListener) { 444 if (mActiveLetterboxEduLayout != null) { 445 if (mActiveLetterboxEduLayout.needsToBeRecreated(taskInfo, taskListener)) { 446 mActiveLetterboxEduLayout.release(); 447 mActiveLetterboxEduLayout = null; 448 } else { 449 if (!mActiveLetterboxEduLayout.updateCompatInfo(taskInfo, taskListener, 450 showOnDisplay(mActiveLetterboxEduLayout.getDisplayId()))) { 451 // The layout is no longer eligible to be shown, clear active layout. 452 mActiveLetterboxEduLayout.release(); 453 mActiveLetterboxEduLayout = null; 454 } 455 return; 456 } 457 } 458 // Create a new UI layout. 459 final Context context = getOrCreateDisplayContext(taskInfo.displayId); 460 if (context == null) { 461 return; 462 } 463 LetterboxEduWindowManager newLayout = createLetterboxEduWindowManager(context, taskInfo, 464 taskListener); 465 if (newLayout.createLayout(showOnDisplay(taskInfo.displayId))) { 466 // The new layout is eligible to be shown, make it the active layout. 467 if (mActiveLetterboxEduLayout != null) { 468 // Release the previous layout since at most one can be active. 469 // Since letterbox education is only shown once to the user, releasing the previous 470 // layout is only a precaution. 471 mActiveLetterboxEduLayout.release(); 472 } 473 mActiveLetterboxEduLayout = newLayout; 474 } 475 } 476 477 @VisibleForTesting createLetterboxEduWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener)478 LetterboxEduWindowManager createLetterboxEduWindowManager(Context context, TaskInfo taskInfo, 479 ShellTaskOrganizer.TaskListener taskListener) { 480 return new LetterboxEduWindowManager(context, taskInfo, 481 mSyncQueue, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), 482 mTransitionsLazy.get(), 483 stateInfo -> createOrUpdateReachabilityEduLayout(stateInfo.first, stateInfo.second), 484 mDockStateReader, mCompatUIConfiguration); 485 } 486 createOrUpdateRestartDialogLayout(@onNull TaskInfo taskInfo, @Nullable ShellTaskOrganizer.TaskListener taskListener)487 private void createOrUpdateRestartDialogLayout(@NonNull TaskInfo taskInfo, 488 @Nullable ShellTaskOrganizer.TaskListener taskListener) { 489 RestartDialogWindowManager layout = 490 mTaskIdToRestartDialogWindowManagerMap.get(taskInfo.taskId); 491 if (layout != null) { 492 if (layout.needsToBeRecreated(taskInfo, taskListener)) { 493 mTaskIdToRestartDialogWindowManagerMap.remove(taskInfo.taskId); 494 layout.release(); 495 } else { 496 layout.setRequestRestartDialog( 497 mSetOfTaskIdsShowingRestartDialog.contains(taskInfo.taskId)); 498 // UI already exists, update the UI layout. 499 if (!layout.updateCompatInfo(taskInfo, taskListener, 500 showOnDisplay(layout.getDisplayId()))) { 501 // The layout is no longer eligible to be shown, remove from active layouts. 502 mTaskIdToRestartDialogWindowManagerMap.remove(taskInfo.taskId); 503 } 504 return; 505 } 506 } 507 // Create a new UI layout. 508 final Context context = getOrCreateDisplayContext(taskInfo.displayId); 509 if (context == null) { 510 return; 511 } 512 layout = createRestartDialogWindowManager(context, taskInfo, taskListener); 513 layout.setRequestRestartDialog( 514 mSetOfTaskIdsShowingRestartDialog.contains(taskInfo.taskId)); 515 if (layout.createLayout(showOnDisplay(taskInfo.displayId))) { 516 // The new layout is eligible to be shown, add it the active layouts. 517 mTaskIdToRestartDialogWindowManagerMap.put(taskInfo.taskId, layout); 518 } 519 } 520 521 @VisibleForTesting createRestartDialogWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener)522 RestartDialogWindowManager createRestartDialogWindowManager(Context context, TaskInfo taskInfo, 523 ShellTaskOrganizer.TaskListener taskListener) { 524 return new RestartDialogWindowManager(context, taskInfo, mSyncQueue, taskListener, 525 mDisplayController.getDisplayLayout(taskInfo.displayId), mTransitionsLazy.get(), 526 this::onRestartDialogCallback, this::onRestartDialogDismissCallback, 527 mCompatUIConfiguration); 528 } 529 onRestartDialogCallback( Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo)530 private void onRestartDialogCallback( 531 Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) { 532 mTaskIdToRestartDialogWindowManagerMap.remove(stateInfo.first.taskId); 533 mCompatUICallback.onSizeCompatRestartButtonClicked(stateInfo.first.taskId); 534 } 535 onRestartDialogDismissCallback( Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo)536 private void onRestartDialogDismissCallback( 537 Pair<TaskInfo, ShellTaskOrganizer.TaskListener> stateInfo) { 538 mSetOfTaskIdsShowingRestartDialog.remove(stateInfo.first.taskId); 539 onCompatInfoChanged(stateInfo.first, stateInfo.second); 540 } 541 createOrUpdateReachabilityEduLayout(@onNull TaskInfo taskInfo, @Nullable ShellTaskOrganizer.TaskListener taskListener)542 private void createOrUpdateReachabilityEduLayout(@NonNull TaskInfo taskInfo, 543 @Nullable ShellTaskOrganizer.TaskListener taskListener) { 544 if (mActiveReachabilityEduLayout != null) { 545 if (mActiveReachabilityEduLayout.needsToBeRecreated(taskInfo, taskListener)) { 546 mActiveReachabilityEduLayout.release(); 547 mActiveReachabilityEduLayout = null; 548 } else { 549 // UI already exists, update the UI layout. 550 if (!mActiveReachabilityEduLayout.updateCompatInfo(taskInfo, taskListener, 551 showOnDisplay(mActiveReachabilityEduLayout.getDisplayId()))) { 552 // The layout is no longer eligible to be shown, remove from active layouts. 553 mActiveReachabilityEduLayout.release(); 554 mActiveReachabilityEduLayout = null; 555 } 556 return; 557 } 558 } 559 // Create a new UI layout. 560 final Context context = getOrCreateDisplayContext(taskInfo.displayId); 561 if (context == null) { 562 return; 563 } 564 ReachabilityEduWindowManager newLayout = createReachabilityEduWindowManager(context, 565 taskInfo, taskListener); 566 if (newLayout.createLayout(showOnDisplay(taskInfo.displayId))) { 567 // The new layout is eligible to be shown, make it the active layout. 568 if (mActiveReachabilityEduLayout != null) { 569 // Release the previous layout since at most one can be active. 570 // Since letterbox reachability education is only shown once to the user, 571 // releasing the previous layout is only a precaution. 572 mActiveReachabilityEduLayout.release(); 573 } 574 mActiveReachabilityEduLayout = newLayout; 575 } 576 } 577 578 @VisibleForTesting createReachabilityEduWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener)579 ReachabilityEduWindowManager createReachabilityEduWindowManager(Context context, 580 TaskInfo taskInfo, 581 ShellTaskOrganizer.TaskListener taskListener) { 582 return new ReachabilityEduWindowManager(context, taskInfo, mSyncQueue, 583 taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), 584 mCompatUIConfiguration, mMainExecutor, this::onInitialReachabilityEduDismissed, 585 mDisappearTimeSupplier); 586 } 587 onInitialReachabilityEduDismissed(@onNull TaskInfo taskInfo, @NonNull ShellTaskOrganizer.TaskListener taskListener)588 private void onInitialReachabilityEduDismissed(@NonNull TaskInfo taskInfo, 589 @NonNull ShellTaskOrganizer.TaskListener taskListener) { 590 // We need to update the UI otherwise it will not be shown until the user relaunches the app 591 createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener); 592 } 593 createOrUpdateUserAspectRatioSettingsLayout(@onNull TaskInfo taskInfo, @Nullable ShellTaskOrganizer.TaskListener taskListener)594 private void createOrUpdateUserAspectRatioSettingsLayout(@NonNull TaskInfo taskInfo, 595 @Nullable ShellTaskOrganizer.TaskListener taskListener) { 596 if (mUserAspectRatioSettingsLayout != null) { 597 if (mUserAspectRatioSettingsLayout.needsToBeRecreated(taskInfo, taskListener)) { 598 mUserAspectRatioSettingsLayout.release(); 599 mUserAspectRatioSettingsLayout = null; 600 } else { 601 // UI already exists, update the UI layout. 602 if (!mUserAspectRatioSettingsLayout.updateCompatInfo(taskInfo, taskListener, 603 showOnDisplay(mUserAspectRatioSettingsLayout.getDisplayId()))) { 604 mUserAspectRatioSettingsLayout.release(); 605 mUserAspectRatioSettingsLayout = null; 606 } 607 return; 608 } 609 } 610 611 // Create a new UI layout. 612 final Context context = getOrCreateDisplayContext(taskInfo.displayId); 613 if (context == null) { 614 return; 615 } 616 final UserAspectRatioSettingsWindowManager newLayout = 617 createUserAspectRatioSettingsWindowManager(context, taskInfo, taskListener); 618 if (newLayout.createLayout(showOnDisplay(taskInfo.displayId))) { 619 // The new layout is eligible to be shown, add it the active layouts. 620 mUserAspectRatioSettingsLayout = newLayout; 621 } 622 } 623 624 @VisibleForTesting 625 @NonNull createUserAspectRatioSettingsWindowManager( @onNull Context context, @NonNull TaskInfo taskInfo, @Nullable ShellTaskOrganizer.TaskListener taskListener)626 UserAspectRatioSettingsWindowManager createUserAspectRatioSettingsWindowManager( 627 @NonNull Context context, @NonNull TaskInfo taskInfo, 628 @Nullable ShellTaskOrganizer.TaskListener taskListener) { 629 return new UserAspectRatioSettingsWindowManager(context, taskInfo, mSyncQueue, 630 taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId), 631 mCompatUIHintsState, this::launchUserAspectRatioSettings, mMainExecutor, 632 mDisappearTimeSupplier, this::hasShownUserAspectRatioSettingsButton, 633 this::setHasShownUserAspectRatioSettingsButton); 634 } 635 launchUserAspectRatioSettings( @onNull TaskInfo taskInfo, @NonNull ShellTaskOrganizer.TaskListener taskListener)636 private void launchUserAspectRatioSettings( 637 @NonNull TaskInfo taskInfo, @NonNull ShellTaskOrganizer.TaskListener taskListener) { 638 final Intent intent = new Intent(Settings.ACTION_MANAGE_USER_ASPECT_RATIO_SETTINGS); 639 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 640 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); 641 final ComponentName appComponent = taskInfo.topActivity; 642 if (appComponent != null) { 643 final Uri packageUri = Uri.parse("package:" + appComponent.getPackageName()); 644 intent.setData(packageUri); 645 } 646 final UserHandle userHandle = UserHandle.of(taskInfo.userId); 647 mContext.startActivityAsUser(intent, userHandle); 648 } 649 removeLayouts(int taskId)650 private void removeLayouts(int taskId) { 651 final CompatUIWindowManager compatLayout = mActiveCompatLayouts.get(taskId); 652 if (compatLayout != null) { 653 compatLayout.release(); 654 mActiveCompatLayouts.remove(taskId); 655 } 656 657 if (mActiveLetterboxEduLayout != null && mActiveLetterboxEduLayout.getTaskId() == taskId) { 658 mActiveLetterboxEduLayout.release(); 659 mActiveLetterboxEduLayout = null; 660 } 661 662 final RestartDialogWindowManager restartLayout = 663 mTaskIdToRestartDialogWindowManagerMap.get(taskId); 664 if (restartLayout != null) { 665 restartLayout.release(); 666 mTaskIdToRestartDialogWindowManagerMap.remove(taskId); 667 mSetOfTaskIdsShowingRestartDialog.remove(taskId); 668 } 669 if (mActiveReachabilityEduLayout != null 670 && mActiveReachabilityEduLayout.getTaskId() == taskId) { 671 mActiveReachabilityEduLayout.release(); 672 mActiveReachabilityEduLayout = null; 673 } 674 675 if (mUserAspectRatioSettingsLayout != null 676 && mUserAspectRatioSettingsLayout.getTaskId() == taskId) { 677 mUserAspectRatioSettingsLayout.release(); 678 mUserAspectRatioSettingsLayout = null; 679 } 680 } 681 getOrCreateDisplayContext(int displayId)682 private Context getOrCreateDisplayContext(int displayId) { 683 if (displayId == Display.DEFAULT_DISPLAY) { 684 return mContext; 685 } 686 Context context = null; 687 final WeakReference<Context> ref = mDisplayContextCache.get(displayId); 688 if (ref != null) { 689 context = ref.get(); 690 } 691 if (context == null) { 692 Display display = mContext.getSystemService(DisplayManager.class).getDisplay(displayId); 693 if (display != null) { 694 context = mContext.createDisplayContext(display); 695 mDisplayContextCache.put(displayId, new WeakReference<>(context)); 696 } else { 697 Log.e(TAG, "Cannot get context for display " + displayId); 698 } 699 } 700 return context; 701 } 702 forAllLayoutsOnDisplay(int displayId, Consumer<CompatUIWindowManagerAbstract> callback)703 private void forAllLayoutsOnDisplay(int displayId, 704 Consumer<CompatUIWindowManagerAbstract> callback) { 705 forAllLayouts(layout -> layout.getDisplayId() == displayId, callback); 706 } 707 forAllLayouts(Consumer<CompatUIWindowManagerAbstract> callback)708 private void forAllLayouts(Consumer<CompatUIWindowManagerAbstract> callback) { 709 forAllLayouts(layout -> true, callback); 710 } 711 forAllLayouts(Predicate<CompatUIWindowManagerAbstract> condition, Consumer<CompatUIWindowManagerAbstract> callback)712 private void forAllLayouts(Predicate<CompatUIWindowManagerAbstract> condition, 713 Consumer<CompatUIWindowManagerAbstract> callback) { 714 for (int i = 0; i < mActiveCompatLayouts.size(); i++) { 715 final int taskId = mActiveCompatLayouts.keyAt(i); 716 final CompatUIWindowManager layout = mActiveCompatLayouts.get(taskId); 717 if (layout != null && condition.test(layout)) { 718 callback.accept(layout); 719 } 720 } 721 if (mActiveLetterboxEduLayout != null && condition.test(mActiveLetterboxEduLayout)) { 722 callback.accept(mActiveLetterboxEduLayout); 723 } 724 for (int i = 0; i < mTaskIdToRestartDialogWindowManagerMap.size(); i++) { 725 final int taskId = mTaskIdToRestartDialogWindowManagerMap.keyAt(i); 726 final RestartDialogWindowManager layout = 727 mTaskIdToRestartDialogWindowManagerMap.get(taskId); 728 if (layout != null && condition.test(layout)) { 729 callback.accept(layout); 730 } 731 } 732 if (mActiveReachabilityEduLayout != null && condition.test(mActiveReachabilityEduLayout)) { 733 callback.accept(mActiveReachabilityEduLayout); 734 } 735 if (mUserAspectRatioSettingsLayout != null && condition.test( 736 mUserAspectRatioSettingsLayout)) { 737 callback.accept(mUserAspectRatioSettingsLayout); 738 } 739 } 740 741 /** An implementation of {@link OnInsetsChangedListener} for a given display id. */ 742 private class PerDisplayOnInsetsChangedListener implements OnInsetsChangedListener { 743 final int mDisplayId; 744 final InsetsState mInsetsState = new InsetsState(); 745 PerDisplayOnInsetsChangedListener(int displayId)746 PerDisplayOnInsetsChangedListener(int displayId) { 747 mDisplayId = displayId; 748 } 749 register()750 void register() { 751 mDisplayInsetsController.addInsetsChangedListener(mDisplayId, this); 752 } 753 unregister()754 void unregister() { 755 mDisplayInsetsController.removeInsetsChangedListener(mDisplayId, this); 756 } 757 758 @Override insetsChanged(InsetsState insetsState)759 public void insetsChanged(InsetsState insetsState) { 760 if (mInsetsState.equals(insetsState)) { 761 return; 762 } 763 mInsetsState.set(insetsState); 764 updateDisplayLayout(mDisplayId); 765 } 766 767 @Override insetsControlChanged(InsetsState insetsState, InsetsSourceControl[] activeControls)768 public void insetsControlChanged(InsetsState insetsState, 769 InsetsSourceControl[] activeControls) { 770 insetsChanged(insetsState); 771 } 772 } 773 774 /** 775 * A class holding the state of the compat UI hints, which is shared between all compat UI 776 * window managers. 777 */ 778 static class CompatUIHintsState { 779 boolean mHasShownSizeCompatHint; 780 boolean mHasShownCameraCompatHint; 781 boolean mHasShownUserAspectRatioSettingsButtonHint; 782 } 783 } 784