1 /*
2  * Copyright (C) 2022 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.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING;
20 import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI;
21 
22 import android.annotation.Nullable;
23 import android.app.TaskInfo;
24 import android.content.Context;
25 import android.graphics.Rect;
26 import android.provider.Settings;
27 import android.util.Pair;
28 import android.view.LayoutInflater;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.WindowManager;
32 import android.view.accessibility.AccessibilityEvent;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.wm.shell.R;
36 import com.android.wm.shell.ShellTaskOrganizer;
37 import com.android.wm.shell.common.DisplayLayout;
38 import com.android.wm.shell.common.SyncTransactionQueue;
39 import com.android.wm.shell.transition.Transitions;
40 
41 import java.util.function.Consumer;
42 
43 /**
44  * Window manager for the Restart Dialog.
45  *
46  * TODO(b/263484314): Create abstraction of RestartDialogWindowManager and LetterboxEduWindowManager
47  */
48 class RestartDialogWindowManager extends CompatUIWindowManagerAbstract {
49 
50     private final DialogAnimationController<RestartDialogLayout> mAnimationController;
51 
52     private final Transitions mTransitions;
53 
54     // Remember the last reported state in case visibility changes due to keyguard or IME updates.
55     private boolean mRequestRestartDialog;
56 
57     private final Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnDismissCallback;
58 
59     private final Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartCallback;
60 
61     private final CompatUIConfiguration mCompatUIConfiguration;
62 
63     /**
64      * The vertical margin between the dialog container and the task stable bounds (excluding
65      * insets).
66      */
67     private final int mDialogVerticalMargin;
68 
69     @Nullable
70     @VisibleForTesting
71     RestartDialogLayout mLayout;
72 
RestartDialogWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, Transitions transitions, Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartCallback, Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback, CompatUIConfiguration compatUIConfiguration)73     RestartDialogWindowManager(Context context, TaskInfo taskInfo,
74             SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
75             DisplayLayout displayLayout, Transitions transitions,
76             Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartCallback,
77             Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback,
78             CompatUIConfiguration compatUIConfiguration) {
79         this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions,
80                 onRestartCallback, onDismissCallback,
81                 new DialogAnimationController<>(context, "RestartDialogWindowManager"),
82                 compatUIConfiguration);
83     }
84 
85     @VisibleForTesting
RestartDialogWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, Transitions transitions, Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartCallback, Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback, DialogAnimationController<RestartDialogLayout> animationController, CompatUIConfiguration compatUIConfiguration)86     RestartDialogWindowManager(Context context, TaskInfo taskInfo,
87             SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
88             DisplayLayout displayLayout, Transitions transitions,
89             Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartCallback,
90             Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onDismissCallback,
91             DialogAnimationController<RestartDialogLayout> animationController,
92             CompatUIConfiguration compatUIConfiguration) {
93         super(context, taskInfo, syncQueue, taskListener, displayLayout);
94         mTransitions = transitions;
95         mOnDismissCallback = onDismissCallback;
96         mOnRestartCallback = onRestartCallback;
97         mAnimationController = animationController;
98         mDialogVerticalMargin = (int) mContext.getResources().getDimension(
99                 R.dimen.letterbox_restart_dialog_margin);
100         mCompatUIConfiguration = compatUIConfiguration;
101     }
102 
103     @Override
getZOrder()104     protected int getZOrder() {
105         return TASK_CHILD_LAYER_COMPAT_UI + 2;
106     }
107 
108     @Override
109     @Nullable
getLayout()110     protected  View getLayout() {
111         return mLayout;
112     }
113 
114     @Override
removeLayout()115     protected void removeLayout() {
116         mLayout = null;
117     }
118 
119     @Override
eligibleToShowLayout()120     protected boolean eligibleToShowLayout() {
121         // We don't show this dialog if the user has explicitly selected so clicking on a checkbox.
122         return mRequestRestartDialog && !isTaskbarEduShowing() && (mLayout != null
123                 || mCompatUIConfiguration.shouldShowRestartDialogAgain(getLastTaskInfo()));
124     }
125 
126     @Override
createLayout()127     protected View createLayout() {
128         mLayout = inflateLayout();
129         updateDialogMargins();
130 
131         // startEnterAnimation will be called immediately if shell-transitions are disabled.
132         mTransitions.runOnIdle(this::startEnterAnimation);
133 
134         return mLayout;
135     }
136 
setRequestRestartDialog(boolean enabled)137     void setRequestRestartDialog(boolean enabled) {
138         mRequestRestartDialog = enabled;
139     }
140 
updateDialogMargins()141     private void updateDialogMargins() {
142         if (mLayout == null) {
143             return;
144         }
145         final View dialogContainer = mLayout.getDialogContainerView();
146         ViewGroup.MarginLayoutParams marginParams =
147                 (ViewGroup.MarginLayoutParams) dialogContainer.getLayoutParams();
148 
149         final Rect taskBounds = getTaskBounds();
150         final Rect taskStableBounds = getTaskStableBounds();
151         // only update margins based on taskbar insets
152         marginParams.topMargin = mDialogVerticalMargin;
153         marginParams.bottomMargin = taskBounds.bottom - taskStableBounds.bottom
154                 + mDialogVerticalMargin;
155         dialogContainer.setLayoutParams(marginParams);
156     }
157 
inflateLayout()158     private RestartDialogLayout inflateLayout() {
159         return (RestartDialogLayout) LayoutInflater.from(mContext).inflate(
160                 R.layout.letterbox_restart_dialog_layout, null);
161     }
162 
startEnterAnimation()163     private void startEnterAnimation() {
164         if (mLayout == null) {
165             // Dialog has already been released.
166             return;
167         }
168         mAnimationController.startEnterAnimation(mLayout, /* endCallback= */
169                 this::onDialogEnterAnimationEnded);
170     }
171 
onDialogEnterAnimationEnded()172     private void onDialogEnterAnimationEnded() {
173         if (mLayout == null) {
174             // Dialog has already been released.
175             return;
176         }
177         final TaskInfo lastTaskInfo = getLastTaskInfo();
178         mLayout.setDismissOnClickListener(this::onDismiss);
179         mLayout.setRestartOnClickListener(dontShowAgain -> {
180             if (mLayout != null) {
181                 mLayout.setDismissOnClickListener(null);
182                 mAnimationController.startExitAnimation(mLayout, () -> {
183                     release();
184                 });
185             }
186             if (dontShowAgain) {
187                 mCompatUIConfiguration.setDontShowRestartDialogAgain(lastTaskInfo);
188             }
189             mOnRestartCallback.accept(Pair.create(lastTaskInfo, getTaskListener()));
190         });
191         // Focus on the dialog title for accessibility.
192         mLayout.getDialogTitle().sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
193     }
194 
onDismiss()195     private void onDismiss() {
196         if (mLayout == null) {
197             return;
198         }
199 
200         mLayout.setDismissOnClickListener(null);
201         mAnimationController.startExitAnimation(mLayout, () -> {
202             release();
203             mOnDismissCallback.accept(Pair.create(getLastTaskInfo(), getTaskListener()));
204         });
205     }
206 
207     @Override
release()208     public void release() {
209         mAnimationController.cancelAnimation();
210         super.release();
211     }
212 
213     @Override
onParentBoundsChanged()214     protected void onParentBoundsChanged() {
215         if (mLayout == null) {
216             return;
217         }
218         // Both the layout dimensions and dialog margins depend on the parent bounds.
219         WindowManager.LayoutParams windowLayoutParams = getWindowLayoutParams();
220         mLayout.setLayoutParams(windowLayoutParams);
221         updateDialogMargins();
222         relayout(windowLayoutParams);
223     }
224 
225     @Override
updateSurfacePosition()226     protected void updateSurfacePosition() {
227         // Nothing to do, since the position of the surface is fixed to the top left corner (0,0)
228         // of the task (parent surface), which is the default position of a surface.
229     }
230 
231     @Override
getWindowLayoutParams()232     protected WindowManager.LayoutParams getWindowLayoutParams() {
233         final Rect taskBounds = getTaskBounds();
234         return getWindowLayoutParams(/* width= */ taskBounds.width(), /* height= */
235                 taskBounds.height());
236     }
237 
238     @VisibleForTesting
isTaskbarEduShowing()239     boolean isTaskbarEduShowing() {
240         return Settings.Secure.getInt(mContext.getContentResolver(),
241                 LAUNCHER_TASKBAR_EDUCATION_SHOWING, /* def= */ 0) == 1;
242     }
243 }
244