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