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