1 /*
2  * Copyright (C) 2014 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.systemui.statusbar.phone;
18 
19 import android.app.AlertDialog;
20 import android.app.Dialog;
21 import android.content.BroadcastReceiver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.content.res.Configuration;
26 import android.graphics.Insets;
27 import android.graphics.drawable.Drawable;
28 import android.os.Bundle;
29 import android.os.Handler;
30 import android.os.Looper;
31 import android.os.SystemProperties;
32 import android.os.UserHandle;
33 import android.util.TypedValue;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.view.ViewRootImpl;
37 import android.view.Window;
38 import android.view.WindowInsets.Type;
39 import android.view.WindowManager;
40 import android.view.WindowManager.LayoutParams;
41 
42 import androidx.annotation.Nullable;
43 
44 import com.android.systemui.Dependency;
45 import com.android.systemui.R;
46 import com.android.systemui.animation.DialogLaunchAnimator;
47 import com.android.systemui.broadcast.BroadcastDispatcher;
48 import com.android.systemui.flags.FeatureFlags;
49 import com.android.systemui.flags.Flags;
50 import com.android.systemui.model.SysUiState;
51 import com.android.systemui.shared.system.QuickStepContract;
52 import com.android.systemui.util.DialogKt;
53 
54 import java.util.ArrayList;
55 import java.util.List;
56 
57 /**
58  * Base class for dialogs that should appear over panels and keyguard.
59  *
60  * Optionally provide a {@link SystemUIDialogManager} to its constructor to send signals to
61  * listeners on whether this dialog is showing.
62  *
63  * The SystemUIDialog registers a listener for the screen off / close system dialogs broadcast,
64  * and dismisses itself when it receives the broadcast.
65  */
66 public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigChangedCallback {
67     protected static final int DEFAULT_THEME = R.style.Theme_SystemUI_Dialog;
68     // TODO(b/203389579): Remove this once the dialog width on large screens has been agreed on.
69     private static final String FLAG_TABLET_DIALOG_WIDTH =
70             "persist.systemui.flag_tablet_dialog_width";
71     private static final boolean DEFAULT_DISMISS_ON_DEVICE_LOCK = true;
72 
73     private final Context mContext;
74     private final FeatureFlags mFeatureFlags;
75     @Nullable private final DismissReceiver mDismissReceiver;
76     private final Handler mHandler = new Handler();
77     private final SystemUIDialogManager mDialogManager;
78     private final SysUiState mSysUiState;
79 
80     private int mLastWidth = Integer.MIN_VALUE;
81     private int mLastHeight = Integer.MIN_VALUE;
82     private int mLastConfigurationWidthDp = -1;
83     private int mLastConfigurationHeightDp = -1;
84 
85     private List<Runnable> mOnCreateRunnables = new ArrayList<>();
86 
SystemUIDialog(Context context)87     public SystemUIDialog(Context context) {
88         this(context, DEFAULT_THEME, DEFAULT_DISMISS_ON_DEVICE_LOCK);
89     }
90 
SystemUIDialog(Context context, int theme)91     public SystemUIDialog(Context context, int theme) {
92         this(context, theme, DEFAULT_DISMISS_ON_DEVICE_LOCK);
93     }
94 
SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock)95     public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock) {
96         // TODO(b/219008720): Remove those calls to Dependency.get by introducing a
97         // SystemUIDialogFactory and make all other dialogs create a SystemUIDialog to which we set
98         // the content and attach listeners.
99         this(context, theme, dismissOnDeviceLock,
100                 Dependency.get(FeatureFlags.class),
101                 Dependency.get(SystemUIDialogManager.class),
102                 Dependency.get(SysUiState.class),
103                 Dependency.get(BroadcastDispatcher.class),
104                 Dependency.get(DialogLaunchAnimator.class));
105     }
106 
SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock, FeatureFlags featureFlags, SystemUIDialogManager dialogManager, SysUiState sysUiState, BroadcastDispatcher broadcastDispatcher, DialogLaunchAnimator dialogLaunchAnimator)107     public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock,
108             FeatureFlags featureFlags,
109             SystemUIDialogManager dialogManager,
110             SysUiState sysUiState,
111             BroadcastDispatcher broadcastDispatcher,
112             DialogLaunchAnimator dialogLaunchAnimator) {
113         super(context, theme);
114         mContext = context;
115         mFeatureFlags = featureFlags;
116 
117         applyFlags(this);
118         WindowManager.LayoutParams attrs = getWindow().getAttributes();
119         attrs.setTitle(getClass().getSimpleName());
120         getWindow().setAttributes(attrs);
121 
122         mDismissReceiver = dismissOnDeviceLock ? new DismissReceiver(this, broadcastDispatcher,
123                 dialogLaunchAnimator) : null;
124         mDialogManager = dialogManager;
125         mSysUiState = sysUiState;
126     }
127 
128     @Override
onCreate(Bundle savedInstanceState)129     protected void onCreate(Bundle savedInstanceState) {
130         super.onCreate(savedInstanceState);
131 
132         Configuration config = getContext().getResources().getConfiguration();
133         mLastConfigurationWidthDp = config.screenWidthDp;
134         mLastConfigurationHeightDp = config.screenHeightDp;
135         updateWindowSize();
136 
137         for (int i = 0; i < mOnCreateRunnables.size(); i++) {
138             mOnCreateRunnables.get(i).run();
139         }
140         if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM)) {
141             DialogKt.registerAnimationOnBackInvoked(
142                     /* dialog = */ this,
143                     /* targetView = */ getWindow().getDecorView()
144             );
145         }
146     }
147 
updateWindowSize()148     private void updateWindowSize() {
149         // Only the thread that created this dialog can update its window size.
150         if (Looper.myLooper() != mHandler.getLooper()) {
151             mHandler.post(this::updateWindowSize);
152             return;
153         }
154 
155         int width = getWidth();
156         int height = getHeight();
157         if (width == mLastWidth && height == mLastHeight) {
158             return;
159         }
160 
161         mLastWidth = width;
162         mLastHeight = height;
163         getWindow().setLayout(width, height);
164     }
165 
166     @Override
onConfigurationChanged(Configuration configuration)167     public void onConfigurationChanged(Configuration configuration) {
168         if (mLastConfigurationWidthDp != configuration.screenWidthDp
169                 || mLastConfigurationHeightDp != configuration.screenHeightDp) {
170             mLastConfigurationWidthDp = configuration.screenWidthDp;
171             mLastConfigurationHeightDp = configuration.compatScreenWidthDp;
172 
173             updateWindowSize();
174         }
175     }
176 
177     /**
178      * Return this dialog width. This method will be invoked when this dialog is created and when
179      * the device configuration changes, and the result will be used to resize this dialog window.
180      */
getWidth()181     protected int getWidth() {
182         return getDefaultDialogWidth(this);
183     }
184 
185     /**
186      * Return this dialog height. This method will be invoked when this dialog is created and when
187      * the device configuration changes, and the result will be used to resize this dialog window.
188      */
getHeight()189     protected int getHeight() {
190         return getDefaultDialogHeight();
191     }
192 
193     @Override
onStart()194     protected final void onStart() {
195         super.onStart();
196 
197         if (mDismissReceiver != null) {
198             mDismissReceiver.register();
199         }
200 
201         // Listen for configuration changes to resize this dialog window. This is mostly necessary
202         // for foldables that often go from large <=> small screen when folding/unfolding.
203         ViewRootImpl.addConfigCallback(this);
204         mDialogManager.setShowing(this, true);
205         mSysUiState.setFlag(QuickStepContract.SYSUI_STATE_DIALOG_SHOWING, true)
206                 .commitUpdate(mContext.getDisplayId());
207 
208         start();
209     }
210 
211     /**
212      * Called when {@link #onStart} is called. Subclasses wishing to override {@link #onStart()}
213      * should override this method instead.
214      */
start()215     protected void start() {}
216 
217     @Override
onStop()218     protected final void onStop() {
219         super.onStop();
220 
221         if (mDismissReceiver != null) {
222             mDismissReceiver.unregister();
223         }
224 
225         ViewRootImpl.removeConfigCallback(this);
226         mDialogManager.setShowing(this, false);
227         mSysUiState.setFlag(QuickStepContract.SYSUI_STATE_DIALOG_SHOWING, false)
228                 .commitUpdate(mContext.getDisplayId());
229 
230         stop();
231     }
232 
233     /**
234      * Called when {@link #onStop} is called. Subclasses wishing to override {@link #onStop()}
235      * should override this method instead.
236      */
stop()237     protected void stop() {}
238 
setShowForAllUsers(boolean show)239     public void setShowForAllUsers(boolean show) {
240         setShowForAllUsers(this, show);
241     }
242 
setMessage(int resId)243     public void setMessage(int resId) {
244         setMessage(mContext.getString(resId));
245     }
246 
247     /**
248      * Set a listener to be invoked when the positive button of the dialog is pressed. The dialog
249      * will automatically be dismissed when the button is clicked.
250      */
setPositiveButton(int resId, OnClickListener onClick)251     public void setPositiveButton(int resId, OnClickListener onClick) {
252         setPositiveButton(resId, onClick, true /* dismissOnClick */);
253     }
254 
255     /**
256      * Set a listener to be invoked when the positive button of the dialog is pressed. The dialog
257      * will be dismissed when the button is clicked iff {@code dismissOnClick} is true.
258      */
setPositiveButton(int resId, OnClickListener onClick, boolean dismissOnClick)259     public void setPositiveButton(int resId, OnClickListener onClick, boolean dismissOnClick) {
260         setButton(BUTTON_POSITIVE, resId, onClick, dismissOnClick);
261     }
262 
263     /**
264      * Set a listener to be invoked when the negative button of the dialog is pressed. The dialog
265      * will automatically be dismissed when the button is clicked.
266      */
setNegativeButton(int resId, OnClickListener onClick)267     public void setNegativeButton(int resId, OnClickListener onClick) {
268         setNegativeButton(resId, onClick, true /* dismissOnClick */);
269     }
270 
271     /**
272      * Set a listener to be invoked when the negative button of the dialog is pressed. The dialog
273      * will be dismissed when the button is clicked iff {@code dismissOnClick} is true.
274      */
setNegativeButton(int resId, OnClickListener onClick, boolean dismissOnClick)275     public void setNegativeButton(int resId, OnClickListener onClick, boolean dismissOnClick) {
276         setButton(BUTTON_NEGATIVE, resId, onClick, dismissOnClick);
277     }
278 
279     /**
280      * Set a listener to be invoked when the neutral button of the dialog is pressed. The dialog
281      * will automatically be dismissed when the button is clicked.
282      */
setNeutralButton(int resId, OnClickListener onClick)283     public void setNeutralButton(int resId, OnClickListener onClick) {
284         setNeutralButton(resId, onClick, true /* dismissOnClick */);
285     }
286 
287     /**
288      * Set a listener to be invoked when the neutral button of the dialog is pressed. The dialog
289      * will be dismissed when the button is clicked iff {@code dismissOnClick} is true.
290      */
setNeutralButton(int resId, OnClickListener onClick, boolean dismissOnClick)291     public void setNeutralButton(int resId, OnClickListener onClick, boolean dismissOnClick) {
292         setButton(BUTTON_NEUTRAL, resId, onClick, dismissOnClick);
293     }
294 
setButton(int whichButton, int resId, OnClickListener onClick, boolean dismissOnClick)295     private void setButton(int whichButton, int resId, OnClickListener onClick,
296             boolean dismissOnClick) {
297         if (dismissOnClick) {
298             setButton(whichButton, mContext.getString(resId), onClick);
299         } else {
300             // Set a null OnClickListener to make sure the button is still created and shown.
301             setButton(whichButton, mContext.getString(resId), (OnClickListener) null);
302 
303             // When the dialog is created, set the click listener but don't dismiss the dialog when
304             // it is clicked.
305             mOnCreateRunnables.add(() -> getButton(whichButton).setOnClickListener(
306                     view -> onClick.onClick(this, whichButton)));
307         }
308     }
309 
setShowForAllUsers(Dialog dialog, boolean show)310     public static void setShowForAllUsers(Dialog dialog, boolean show) {
311         if (show) {
312             dialog.getWindow().getAttributes().privateFlags |=
313                     WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
314         } else {
315             dialog.getWindow().getAttributes().privateFlags &=
316                     ~WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
317         }
318     }
319 
320     /**
321      * Ensure the window type is set properly to show over all other screens
322      */
setWindowOnTop(Dialog dialog, boolean isKeyguardShowing)323     public static void setWindowOnTop(Dialog dialog, boolean isKeyguardShowing) {
324         final Window window = dialog.getWindow();
325         window.setType(LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
326         if (isKeyguardShowing) {
327             window.getAttributes().setFitInsetsTypes(
328                     window.getAttributes().getFitInsetsTypes() & ~Type.statusBars());
329         }
330     }
331 
applyFlags(AlertDialog dialog)332     public static AlertDialog applyFlags(AlertDialog dialog) {
333         final Window window = dialog.getWindow();
334         window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
335         window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
336                 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
337         window.getAttributes().setFitInsetsTypes(
338                 window.getAttributes().getFitInsetsTypes() & ~Type.statusBars());
339         return dialog;
340     }
341 
342     /**
343      * Registers a listener that dismisses the given dialog when it receives
344      * the screen off / close system dialogs broadcast.
345      * <p>
346      * <strong>Note:</strong> Don't call dialog.setOnDismissListener() after
347      * calling this because it causes a leak of BroadcastReceiver. Instead, call the version that
348      * takes an extra Runnable as a parameter.
349      *
350      * @param dialog The dialog to be associated with the listener.
351      */
registerDismissListener(Dialog dialog)352     public static void registerDismissListener(Dialog dialog) {
353         registerDismissListener(dialog, null);
354     }
355 
356 
357     /**
358      * Registers a listener that dismisses the given dialog when it receives
359      * the screen off / close system dialogs broadcast.
360      * <p>
361      * <strong>Note:</strong> Don't call dialog.setOnDismissListener() after
362      * calling this because it causes a leak of BroadcastReceiver.
363      *
364      * @param dialog The dialog to be associated with the listener.
365      * @param dismissAction An action to run when the dialog is dismissed.
366      */
registerDismissListener(Dialog dialog, @Nullable Runnable dismissAction)367     public static void registerDismissListener(Dialog dialog, @Nullable Runnable dismissAction) {
368         // TODO(b/219008720): Remove those calls to Dependency.get.
369         DismissReceiver dismissReceiver = new DismissReceiver(dialog,
370                 Dependency.get(BroadcastDispatcher.class),
371                 Dependency.get(DialogLaunchAnimator.class));
372         dialog.setOnDismissListener(d -> {
373             dismissReceiver.unregister();
374             if (dismissAction != null) dismissAction.run();
375         });
376         dismissReceiver.register();
377     }
378 
379     /** Set an appropriate size to {@code dialog} depending on the current configuration. */
setDialogSize(Dialog dialog)380     public static void setDialogSize(Dialog dialog) {
381         // We need to create the dialog first, otherwise the size will be overridden when it is
382         // created.
383         dialog.create();
384         dialog.getWindow().setLayout(getDefaultDialogWidth(dialog), getDefaultDialogHeight());
385     }
386 
getDefaultDialogWidth(Dialog dialog)387     private static int getDefaultDialogWidth(Dialog dialog) {
388         Context context = dialog.getContext();
389         int flagValue = SystemProperties.getInt(FLAG_TABLET_DIALOG_WIDTH, 0);
390         if (flagValue == -1) {
391             // The width of bottom sheets (624dp).
392             return calculateDialogWidthWithInsets(dialog, 624);
393         } else if (flagValue == -2) {
394             // The suggested small width for all dialogs (348dp)
395             return calculateDialogWidthWithInsets(dialog, 348);
396         } else if (flagValue > 0) {
397             // Any given width.
398             return calculateDialogWidthWithInsets(dialog, flagValue);
399         } else {
400             // By default we use the same width as the notification shade in portrait mode.
401             int width = context.getResources().getDimensionPixelSize(R.dimen.large_dialog_width);
402             if (width > 0) {
403                 // If we are neither WRAP_CONTENT or MATCH_PARENT, add the background insets so that
404                 // the dialog is the desired width.
405                 width += getHorizontalInsets(dialog);
406             }
407             return width;
408         }
409     }
410 
411     /**
412      * Return the pixel width {@param dialog} should be so that it is {@param widthInDp} wide,
413      * taking its background insets into consideration.
414      */
calculateDialogWidthWithInsets(Dialog dialog, int widthInDp)415     private static int calculateDialogWidthWithInsets(Dialog dialog, int widthInDp) {
416         float widthInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, widthInDp,
417                 dialog.getContext().getResources().getDisplayMetrics());
418         return Math.round(widthInPixels + getHorizontalInsets(dialog));
419     }
420 
getHorizontalInsets(Dialog dialog)421     private static int getHorizontalInsets(Dialog dialog) {
422         View decorView = dialog.getWindow().getDecorView();
423         if (decorView == null) {
424             return 0;
425         }
426 
427         // We first look for the background on the dialogContentWithBackground added by
428         // DialogLaunchAnimator. If it's not there, we use the background of the DecorView.
429         View viewWithBackground = decorView.findViewByPredicate(
430                 view -> view.getTag(R.id.tag_dialog_background) != null);
431         Drawable background = viewWithBackground != null ? viewWithBackground.getBackground()
432                 : decorView.getBackground();
433         Insets insets = background != null ? background.getOpticalInsets() : Insets.NONE;
434         return insets.left + insets.right;
435     }
436 
getDefaultDialogHeight()437     private static int getDefaultDialogHeight() {
438         return ViewGroup.LayoutParams.WRAP_CONTENT;
439     }
440 
441     private static class DismissReceiver extends BroadcastReceiver {
442         private static final IntentFilter INTENT_FILTER = new IntentFilter();
443         static {
444             INTENT_FILTER.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
445             INTENT_FILTER.addAction(Intent.ACTION_SCREEN_OFF);
446         }
447 
448         private final Dialog mDialog;
449         private boolean mRegistered;
450         private final BroadcastDispatcher mBroadcastDispatcher;
451         private final DialogLaunchAnimator mDialogLaunchAnimator;
452 
DismissReceiver(Dialog dialog, BroadcastDispatcher broadcastDispatcher, DialogLaunchAnimator dialogLaunchAnimator)453         DismissReceiver(Dialog dialog, BroadcastDispatcher broadcastDispatcher,
454                 DialogLaunchAnimator dialogLaunchAnimator) {
455             mDialog = dialog;
456             mBroadcastDispatcher = broadcastDispatcher;
457             mDialogLaunchAnimator = dialogLaunchAnimator;
458         }
459 
register()460         void register() {
461             mBroadcastDispatcher.registerReceiver(this, INTENT_FILTER, null, UserHandle.CURRENT);
462             mRegistered = true;
463         }
464 
unregister()465         void unregister() {
466             if (mRegistered) {
467                 mBroadcastDispatcher.unregisterReceiver(this);
468                 mRegistered = false;
469             }
470         }
471 
472         @Override
onReceive(Context context, Intent intent)473         public void onReceive(Context context, Intent intent) {
474             // These broadcast are usually received when locking the device, swiping up to home
475             // (which collapses the shade), etc. In those cases, we usually don't want to animate
476             // back into the view.
477             mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations();
478             mDialog.dismiss();
479         }
480     }
481 
482 }
483