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