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