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