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