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.os.Bundle;
27 import android.os.Handler;
28 import android.os.Looper;
29 import android.os.SystemProperties;
30 import android.os.UserHandle;
31 import android.util.TypedValue;
32 import android.view.ViewGroup;
33 import android.view.ViewRootImpl;
34 import android.view.Window;
35 import android.view.WindowInsets.Type;
36 import android.view.WindowManager;
37 import android.view.WindowManager.LayoutParams;
38 
39 import androidx.annotation.Nullable;
40 
41 import com.android.systemui.Dependency;
42 import com.android.systemui.R;
43 import com.android.systemui.broadcast.BroadcastDispatcher;
44 import com.android.systemui.statusbar.policy.KeyguardStateController;
45 
46 /**
47  * Base class for dialogs that should appear over panels and keyguard.
48  *
49  * Optionally provide a {@link SystemUIDialogManager} to its constructor to send signals to
50  * listeners on whether this dialog is showing.
51  *
52  * The SystemUIDialog registers a listener for the screen off / close system dialogs broadcast,
53  * and dismisses itself when it receives the broadcast.
54  */
55 public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigChangedCallback {
56     // TODO(b/203389579): Remove this once the dialog width on large screens has been agreed on.
57     private static final String FLAG_TABLET_DIALOG_WIDTH =
58             "persist.systemui.flag_tablet_dialog_width";
59 
60     private final Context mContext;
61     @Nullable private final DismissReceiver mDismissReceiver;
62     private final Handler mHandler = new Handler();
63     @Nullable private final SystemUIDialogManager mDialogManager;
64 
65     private int mLastWidth = Integer.MIN_VALUE;
66     private int mLastHeight = Integer.MIN_VALUE;
67     private int mLastConfigurationWidthDp = -1;
68     private int mLastConfigurationHeightDp = -1;
69 
SystemUIDialog(Context context)70     public SystemUIDialog(Context context) {
71         this(context, R.style.Theme_SystemUI_Dialog);
72     }
73 
SystemUIDialog(Context context, SystemUIDialogManager dialogManager)74     public SystemUIDialog(Context context, SystemUIDialogManager dialogManager) {
75         this(context, R.style.Theme_SystemUI_Dialog, true, dialogManager);
76     }
77 
SystemUIDialog(Context context, int theme)78     public SystemUIDialog(Context context, int theme) {
79         this(context, theme, true /* dismissOnDeviceLock */);
80     }
SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock)81     public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock) {
82         this(context, theme, dismissOnDeviceLock, null);
83     }
84 
85     /**
86      * @param udfpsDialogManager If set, UDFPS will hide if this dialog is showing.
87      */
SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock, SystemUIDialogManager dialogManager)88     public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock,
89             SystemUIDialogManager dialogManager) {
90         super(context, theme);
91         mContext = context;
92 
93         applyFlags(this);
94         WindowManager.LayoutParams attrs = getWindow().getAttributes();
95         attrs.setTitle(getClass().getSimpleName());
96         getWindow().setAttributes(attrs);
97 
98         mDismissReceiver = dismissOnDeviceLock ? new DismissReceiver(this) : null;
99         mDialogManager = dialogManager;
100     }
101 
102     @Override
onCreate(Bundle savedInstanceState)103     protected void onCreate(Bundle savedInstanceState) {
104         super.onCreate(savedInstanceState);
105 
106         Configuration config = getContext().getResources().getConfiguration();
107         mLastConfigurationWidthDp = config.screenWidthDp;
108         mLastConfigurationHeightDp = config.screenHeightDp;
109         updateWindowSize();
110     }
111 
updateWindowSize()112     private void updateWindowSize() {
113         // Only the thread that created this dialog can update its window size.
114         if (Looper.myLooper() != mHandler.getLooper()) {
115             mHandler.post(this::updateWindowSize);
116             return;
117         }
118 
119         int width = getWidth();
120         int height = getHeight();
121         if (width == mLastWidth && height == mLastHeight) {
122             return;
123         }
124 
125         mLastWidth = width;
126         mLastHeight = height;
127         getWindow().setLayout(width, height);
128     }
129 
130     @Override
onConfigurationChanged(Configuration configuration)131     public void onConfigurationChanged(Configuration configuration) {
132         if (mLastConfigurationWidthDp != configuration.screenWidthDp
133                 || mLastConfigurationHeightDp != configuration.screenHeightDp) {
134             mLastConfigurationWidthDp = configuration.screenWidthDp;
135             mLastConfigurationHeightDp = configuration.compatScreenWidthDp;
136 
137             updateWindowSize();
138         }
139     }
140 
141     /**
142      * Return this dialog width. This method will be invoked when this dialog is created and when
143      * the device configuration changes, and the result will be used to resize this dialog window.
144      */
getWidth()145     protected int getWidth() {
146         return getDefaultDialogWidth(mContext);
147     }
148 
149     /**
150      * Return this dialog height. This method will be invoked when this dialog is created and when
151      * the device configuration changes, and the result will be used to resize this dialog window.
152      */
getHeight()153     protected int getHeight() {
154         return getDefaultDialogHeight();
155     }
156 
157     @Override
onStart()158     protected void onStart() {
159         super.onStart();
160 
161         if (mDismissReceiver != null) {
162             mDismissReceiver.register();
163         }
164 
165         if (mDialogManager != null) {
166             mDialogManager.setShowing(this, true);
167         }
168 
169         // Listen for configuration changes to resize this dialog window. This is mostly necessary
170         // for foldables that often go from large <=> small screen when folding/unfolding.
171         ViewRootImpl.addConfigCallback(this);
172     }
173 
174     @Override
onStop()175     protected void onStop() {
176         super.onStop();
177 
178         if (mDismissReceiver != null) {
179             mDismissReceiver.unregister();
180         }
181 
182         if (mDialogManager != null) {
183             mDialogManager.setShowing(this, false);
184         }
185 
186         ViewRootImpl.removeConfigCallback(this);
187     }
188 
setShowForAllUsers(boolean show)189     public void setShowForAllUsers(boolean show) {
190         setShowForAllUsers(this, show);
191     }
192 
setMessage(int resId)193     public void setMessage(int resId) {
194         setMessage(mContext.getString(resId));
195     }
196 
setPositiveButton(int resId, OnClickListener onClick)197     public void setPositiveButton(int resId, OnClickListener onClick) {
198         setButton(BUTTON_POSITIVE, mContext.getString(resId), onClick);
199     }
200 
setNegativeButton(int resId, OnClickListener onClick)201     public void setNegativeButton(int resId, OnClickListener onClick) {
202         setButton(BUTTON_NEGATIVE, mContext.getString(resId), onClick);
203     }
204 
setNeutralButton(int resId, OnClickListener onClick)205     public void setNeutralButton(int resId, OnClickListener onClick) {
206         setButton(BUTTON_NEUTRAL, mContext.getString(resId), onClick);
207     }
208 
setShowForAllUsers(Dialog dialog, boolean show)209     public static void setShowForAllUsers(Dialog dialog, boolean show) {
210         if (show) {
211             dialog.getWindow().getAttributes().privateFlags |=
212                     WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
213         } else {
214             dialog.getWindow().getAttributes().privateFlags &=
215                     ~WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
216         }
217     }
218 
setWindowOnTop(Dialog dialog)219     public static void setWindowOnTop(Dialog dialog) {
220         final Window window = dialog.getWindow();
221         window.setType(LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
222         if (Dependency.get(KeyguardStateController.class).isShowing()) {
223             window.getAttributes().setFitInsetsTypes(
224                     window.getAttributes().getFitInsetsTypes() & ~Type.statusBars());
225         }
226     }
227 
applyFlags(AlertDialog dialog)228     public static AlertDialog applyFlags(AlertDialog dialog) {
229         final Window window = dialog.getWindow();
230         window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
231         window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
232                 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
233         window.getAttributes().setFitInsetsTypes(
234                 window.getAttributes().getFitInsetsTypes() & ~Type.statusBars());
235         return dialog;
236     }
237 
238     /**
239      * Registers a listener that dismisses the given dialog when it receives
240      * the screen off / close system dialogs broadcast.
241      * <p>
242      * <strong>Note:</strong> Don't call dialog.setOnDismissListener() after
243      * calling this because it causes a leak of BroadcastReceiver. Instead, call the version that
244      * takes an extra Runnable as a parameter.
245      *
246      * @param dialog The dialog to be associated with the listener.
247      */
registerDismissListener(Dialog dialog)248     public static void registerDismissListener(Dialog dialog) {
249         registerDismissListener(dialog, null);
250     }
251 
252 
253     /**
254      * Registers a listener that dismisses the given dialog when it receives
255      * the screen off / close system dialogs broadcast.
256      * <p>
257      * <strong>Note:</strong> Don't call dialog.setOnDismissListener() after
258      * calling this because it causes a leak of BroadcastReceiver.
259      *
260      * @param dialog The dialog to be associated with the listener.
261      * @param dismissAction An action to run when the dialog is dismissed.
262      */
registerDismissListener(Dialog dialog, @Nullable Runnable dismissAction)263     public static void registerDismissListener(Dialog dialog, @Nullable Runnable dismissAction) {
264         DismissReceiver dismissReceiver = new DismissReceiver(dialog);
265         dialog.setOnDismissListener(d -> {
266             dismissReceiver.unregister();
267             if (dismissAction != null) dismissAction.run();
268         });
269         dismissReceiver.register();
270     }
271 
272     /** Set an appropriate size to {@code dialog} depending on the current configuration. */
setDialogSize(Dialog dialog)273     public static void setDialogSize(Dialog dialog) {
274         // We need to create the dialog first, otherwise the size will be overridden when it is
275         // created.
276         dialog.create();
277         dialog.getWindow().setLayout(getDefaultDialogWidth(dialog.getContext()),
278                 getDefaultDialogHeight());
279     }
280 
getDefaultDialogWidth(Context context)281     private static int getDefaultDialogWidth(Context context) {
282         boolean isOnTablet = context.getResources().getConfiguration().smallestScreenWidthDp >= 600;
283         if (!isOnTablet) {
284             return ViewGroup.LayoutParams.MATCH_PARENT;
285         }
286 
287         int flagValue = SystemProperties.getInt(FLAG_TABLET_DIALOG_WIDTH, 0);
288         if (flagValue == -1) {
289             // The width of bottom sheets (624dp).
290             return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 624,
291                     context.getResources().getDisplayMetrics()));
292         } else if (flagValue == -2) {
293             // The suggested small width for all dialogs (348dp)
294             return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 348,
295                     context.getResources().getDisplayMetrics()));
296         } else if (flagValue > 0) {
297             // Any given width.
298             return Math.round(
299                     TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, flagValue,
300                             context.getResources().getDisplayMetrics()));
301         } else {
302             // By default we use the same width as the notification shade in portrait mode (504dp).
303             return context.getResources().getDimensionPixelSize(R.dimen.large_dialog_width);
304         }
305     }
306 
getDefaultDialogHeight()307     private static int getDefaultDialogHeight() {
308         return ViewGroup.LayoutParams.WRAP_CONTENT;
309     }
310 
311     private static class DismissReceiver extends BroadcastReceiver {
312         private static final IntentFilter INTENT_FILTER = new IntentFilter();
313         static {
314             INTENT_FILTER.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
315             INTENT_FILTER.addAction(Intent.ACTION_SCREEN_OFF);
316         }
317 
318         private final Dialog mDialog;
319         private boolean mRegistered;
320         private final BroadcastDispatcher mBroadcastDispatcher;
321 
DismissReceiver(Dialog dialog)322         DismissReceiver(Dialog dialog) {
323             mDialog = dialog;
324             mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class);
325         }
326 
register()327         void register() {
328             mBroadcastDispatcher.registerReceiver(this, INTENT_FILTER, null, UserHandle.CURRENT);
329             mRegistered = true;
330         }
331 
unregister()332         void unregister() {
333             if (mRegistered) {
334                 mBroadcastDispatcher.unregisterReceiver(this);
335                 mRegistered = false;
336             }
337         }
338 
339         @Override
onReceive(Context context, Intent intent)340         public void onReceive(Context context, Intent intent) {
341             mDialog.dismiss();
342         }
343     }
344 }
345