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