| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.systemui.statusbar.phone; |
| |
| import android.app.AlertDialog; |
| import android.app.Dialog; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.res.Configuration; |
| import android.graphics.Insets; |
| import android.graphics.drawable.Drawable; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.SystemProperties; |
| import android.os.UserHandle; |
| import android.util.TypedValue; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewRootImpl; |
| import android.view.Window; |
| import android.view.WindowInsets.Type; |
| import android.view.WindowManager; |
| import android.view.WindowManager.LayoutParams; |
| |
| import androidx.annotation.Nullable; |
| |
| import com.android.systemui.Dependency; |
| import com.android.systemui.R; |
| import com.android.systemui.animation.DialogLaunchAnimator; |
| import com.android.systemui.broadcast.BroadcastDispatcher; |
| import com.android.systemui.model.SysUiState; |
| import com.android.systemui.shared.system.QuickStepContract; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Base class for dialogs that should appear over panels and keyguard. |
| * |
| * Optionally provide a {@link SystemUIDialogManager} to its constructor to send signals to |
| * listeners on whether this dialog is showing. |
| * |
| * The SystemUIDialog registers a listener for the screen off / close system dialogs broadcast, |
| * and dismisses itself when it receives the broadcast. |
| */ |
| public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigChangedCallback { |
| protected static final int DEFAULT_THEME = R.style.Theme_SystemUI_Dialog; |
| // TODO(b/203389579): Remove this once the dialog width on large screens has been agreed on. |
| private static final String FLAG_TABLET_DIALOG_WIDTH = |
| "persist.systemui.flag_tablet_dialog_width"; |
| private static final boolean DEFAULT_DISMISS_ON_DEVICE_LOCK = true; |
| |
| private final Context mContext; |
| @Nullable private final DismissReceiver mDismissReceiver; |
| private final Handler mHandler = new Handler(); |
| private final SystemUIDialogManager mDialogManager; |
| private final SysUiState mSysUiState; |
| |
| private int mLastWidth = Integer.MIN_VALUE; |
| private int mLastHeight = Integer.MIN_VALUE; |
| private int mLastConfigurationWidthDp = -1; |
| private int mLastConfigurationHeightDp = -1; |
| |
| private List<Runnable> mOnCreateRunnables = new ArrayList<>(); |
| |
| public SystemUIDialog(Context context) { |
| this(context, DEFAULT_THEME, DEFAULT_DISMISS_ON_DEVICE_LOCK); |
| } |
| |
| public SystemUIDialog(Context context, int theme) { |
| this(context, theme, DEFAULT_DISMISS_ON_DEVICE_LOCK); |
| } |
| |
| public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock) { |
| super(context, theme); |
| mContext = context; |
| |
| applyFlags(this); |
| WindowManager.LayoutParams attrs = getWindow().getAttributes(); |
| attrs.setTitle(getClass().getSimpleName()); |
| getWindow().setAttributes(attrs); |
| |
| mDismissReceiver = dismissOnDeviceLock ? new DismissReceiver(this) : null; |
| |
| // TODO(b/219008720): Remove those calls to Dependency.get by introducing a |
| // SystemUIDialogFactory and make all other dialogs create a SystemUIDialog to which we set |
| // the content and attach listeners. |
| mDialogManager = Dependency.get(SystemUIDialogManager.class); |
| mSysUiState = Dependency.get(SysUiState.class); |
| } |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| Configuration config = getContext().getResources().getConfiguration(); |
| mLastConfigurationWidthDp = config.screenWidthDp; |
| mLastConfigurationHeightDp = config.screenHeightDp; |
| updateWindowSize(); |
| |
| for (int i = 0; i < mOnCreateRunnables.size(); i++) { |
| mOnCreateRunnables.get(i).run(); |
| } |
| } |
| |
| private void updateWindowSize() { |
| // Only the thread that created this dialog can update its window size. |
| if (Looper.myLooper() != mHandler.getLooper()) { |
| mHandler.post(this::updateWindowSize); |
| return; |
| } |
| |
| int width = getWidth(); |
| int height = getHeight(); |
| if (width == mLastWidth && height == mLastHeight) { |
| return; |
| } |
| |
| mLastWidth = width; |
| mLastHeight = height; |
| getWindow().setLayout(width, height); |
| } |
| |
| @Override |
| public void onConfigurationChanged(Configuration configuration) { |
| if (mLastConfigurationWidthDp != configuration.screenWidthDp |
| || mLastConfigurationHeightDp != configuration.screenHeightDp) { |
| mLastConfigurationWidthDp = configuration.screenWidthDp; |
| mLastConfigurationHeightDp = configuration.compatScreenWidthDp; |
| |
| updateWindowSize(); |
| } |
| } |
| |
| /** |
| * Return this dialog width. This method will be invoked when this dialog is created and when |
| * the device configuration changes, and the result will be used to resize this dialog window. |
| */ |
| protected int getWidth() { |
| return getDefaultDialogWidth(this); |
| } |
| |
| /** |
| * Return this dialog height. This method will be invoked when this dialog is created and when |
| * the device configuration changes, and the result will be used to resize this dialog window. |
| */ |
| protected int getHeight() { |
| return getDefaultDialogHeight(); |
| } |
| |
| @Override |
| protected void onStart() { |
| super.onStart(); |
| |
| if (mDismissReceiver != null) { |
| mDismissReceiver.register(); |
| } |
| |
| // Listen for configuration changes to resize this dialog window. This is mostly necessary |
| // for foldables that often go from large <=> small screen when folding/unfolding. |
| ViewRootImpl.addConfigCallback(this); |
| mDialogManager.setShowing(this, true); |
| mSysUiState.setFlag(QuickStepContract.SYSUI_STATE_DIALOG_SHOWING, true); |
| } |
| |
| @Override |
| protected void onStop() { |
| super.onStop(); |
| |
| if (mDismissReceiver != null) { |
| mDismissReceiver.unregister(); |
| } |
| |
| ViewRootImpl.removeConfigCallback(this); |
| mDialogManager.setShowing(this, false); |
| mSysUiState.setFlag(QuickStepContract.SYSUI_STATE_DIALOG_SHOWING, false); |
| } |
| |
| public void setShowForAllUsers(boolean show) { |
| setShowForAllUsers(this, show); |
| } |
| |
| public void setMessage(int resId) { |
| setMessage(mContext.getString(resId)); |
| } |
| |
| /** |
| * Set a listener to be invoked when the positive button of the dialog is pressed. The dialog |
| * will automatically be dismissed when the button is clicked. |
| */ |
| public void setPositiveButton(int resId, OnClickListener onClick) { |
| setPositiveButton(resId, onClick, true /* dismissOnClick */); |
| } |
| |
| /** |
| * Set a listener to be invoked when the positive button of the dialog is pressed. The dialog |
| * will be dismissed when the button is clicked iff {@code dismissOnClick} is true. |
| */ |
| public void setPositiveButton(int resId, OnClickListener onClick, boolean dismissOnClick) { |
| setButton(BUTTON_POSITIVE, resId, onClick, dismissOnClick); |
| } |
| |
| /** |
| * Set a listener to be invoked when the negative button of the dialog is pressed. The dialog |
| * will automatically be dismissed when the button is clicked. |
| */ |
| public void setNegativeButton(int resId, OnClickListener onClick) { |
| setNegativeButton(resId, onClick, true /* dismissOnClick */); |
| } |
| |
| /** |
| * Set a listener to be invoked when the negative button of the dialog is pressed. The dialog |
| * will be dismissed when the button is clicked iff {@code dismissOnClick} is true. |
| */ |
| public void setNegativeButton(int resId, OnClickListener onClick, boolean dismissOnClick) { |
| setButton(BUTTON_NEGATIVE, resId, onClick, dismissOnClick); |
| } |
| |
| /** |
| * Set a listener to be invoked when the neutral button of the dialog is pressed. The dialog |
| * will automatically be dismissed when the button is clicked. |
| */ |
| public void setNeutralButton(int resId, OnClickListener onClick) { |
| setNeutralButton(resId, onClick, true /* dismissOnClick */); |
| } |
| |
| /** |
| * Set a listener to be invoked when the neutral button of the dialog is pressed. The dialog |
| * will be dismissed when the button is clicked iff {@code dismissOnClick} is true. |
| */ |
| public void setNeutralButton(int resId, OnClickListener onClick, boolean dismissOnClick) { |
| setButton(BUTTON_NEUTRAL, resId, onClick, dismissOnClick); |
| } |
| |
| private void setButton(int whichButton, int resId, OnClickListener onClick, |
| boolean dismissOnClick) { |
| if (dismissOnClick) { |
| setButton(whichButton, mContext.getString(resId), onClick); |
| } else { |
| // Set a null OnClickListener to make sure the button is still created and shown. |
| setButton(whichButton, mContext.getString(resId), (OnClickListener) null); |
| |
| // When the dialog is created, set the click listener but don't dismiss the dialog when |
| // it is clicked. |
| mOnCreateRunnables.add(() -> getButton(whichButton).setOnClickListener( |
| view -> onClick.onClick(this, whichButton))); |
| } |
| } |
| |
| public static void setShowForAllUsers(Dialog dialog, boolean show) { |
| if (show) { |
| dialog.getWindow().getAttributes().privateFlags |= |
| WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; |
| } else { |
| dialog.getWindow().getAttributes().privateFlags &= |
| ~WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; |
| } |
| } |
| |
| /** |
| * Ensure the window type is set properly to show over all other screens |
| */ |
| public static void setWindowOnTop(Dialog dialog, boolean isKeyguardShowing) { |
| final Window window = dialog.getWindow(); |
| window.setType(LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); |
| if (isKeyguardShowing) { |
| window.getAttributes().setFitInsetsTypes( |
| window.getAttributes().getFitInsetsTypes() & ~Type.statusBars()); |
| } |
| } |
| |
| public static AlertDialog applyFlags(AlertDialog dialog) { |
| final Window window = dialog.getWindow(); |
| window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); |
| window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM |
| | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); |
| window.getAttributes().setFitInsetsTypes( |
| window.getAttributes().getFitInsetsTypes() & ~Type.statusBars()); |
| return dialog; |
| } |
| |
| /** |
| * Registers a listener that dismisses the given dialog when it receives |
| * the screen off / close system dialogs broadcast. |
| * <p> |
| * <strong>Note:</strong> Don't call dialog.setOnDismissListener() after |
| * calling this because it causes a leak of BroadcastReceiver. Instead, call the version that |
| * takes an extra Runnable as a parameter. |
| * |
| * @param dialog The dialog to be associated with the listener. |
| */ |
| public static void registerDismissListener(Dialog dialog) { |
| registerDismissListener(dialog, null); |
| } |
| |
| |
| /** |
| * Registers a listener that dismisses the given dialog when it receives |
| * the screen off / close system dialogs broadcast. |
| * <p> |
| * <strong>Note:</strong> Don't call dialog.setOnDismissListener() after |
| * calling this because it causes a leak of BroadcastReceiver. |
| * |
| * @param dialog The dialog to be associated with the listener. |
| * @param dismissAction An action to run when the dialog is dismissed. |
| */ |
| public static void registerDismissListener(Dialog dialog, @Nullable Runnable dismissAction) { |
| DismissReceiver dismissReceiver = new DismissReceiver(dialog); |
| dialog.setOnDismissListener(d -> { |
| dismissReceiver.unregister(); |
| if (dismissAction != null) dismissAction.run(); |
| }); |
| dismissReceiver.register(); |
| } |
| |
| /** Set an appropriate size to {@code dialog} depending on the current configuration. */ |
| public static void setDialogSize(Dialog dialog) { |
| // We need to create the dialog first, otherwise the size will be overridden when it is |
| // created. |
| dialog.create(); |
| dialog.getWindow().setLayout(getDefaultDialogWidth(dialog), getDefaultDialogHeight()); |
| } |
| |
| private static int getDefaultDialogWidth(Dialog dialog) { |
| Context context = dialog.getContext(); |
| int flagValue = SystemProperties.getInt(FLAG_TABLET_DIALOG_WIDTH, 0); |
| if (flagValue == -1) { |
| // The width of bottom sheets (624dp). |
| return calculateDialogWidthWithInsets(dialog, 624); |
| } else if (flagValue == -2) { |
| // The suggested small width for all dialogs (348dp) |
| return calculateDialogWidthWithInsets(dialog, 348); |
| } else if (flagValue > 0) { |
| // Any given width. |
| return calculateDialogWidthWithInsets(dialog, flagValue); |
| } else { |
| // By default we use the same width as the notification shade in portrait mode. |
| int width = context.getResources().getDimensionPixelSize(R.dimen.large_dialog_width); |
| if (width > 0) { |
| // If we are neither WRAP_CONTENT or MATCH_PARENT, add the background insets so that |
| // the dialog is the desired width. |
| width += getHorizontalInsets(dialog); |
| } |
| return width; |
| } |
| } |
| |
| /** |
| * Return the pixel width {@param dialog} should be so that it is {@param widthInDp} wide, |
| * taking its background insets into consideration. |
| */ |
| private static int calculateDialogWidthWithInsets(Dialog dialog, int widthInDp) { |
| float widthInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, widthInDp, |
| dialog.getContext().getResources().getDisplayMetrics()); |
| return Math.round(widthInPixels + getHorizontalInsets(dialog)); |
| } |
| |
| private static int getHorizontalInsets(Dialog dialog) { |
| View decorView = dialog.getWindow().getDecorView(); |
| if (decorView == null) { |
| return 0; |
| } |
| |
| // We first look for the background on the dialogContentWithBackground added by |
| // DialogLaunchAnimator. If it's not there, we use the background of the DecorView. |
| View viewWithBackground = decorView.findViewByPredicate( |
| view -> view.getTag(R.id.tag_dialog_background) != null); |
| Drawable background = viewWithBackground != null ? viewWithBackground.getBackground() |
| : decorView.getBackground(); |
| Insets insets = background != null ? background.getOpticalInsets() : Insets.NONE; |
| return insets.left + insets.right; |
| } |
| |
| private static int getDefaultDialogHeight() { |
| return ViewGroup.LayoutParams.WRAP_CONTENT; |
| } |
| |
| private static class DismissReceiver extends BroadcastReceiver { |
| private static final IntentFilter INTENT_FILTER = new IntentFilter(); |
| static { |
| INTENT_FILTER.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); |
| INTENT_FILTER.addAction(Intent.ACTION_SCREEN_OFF); |
| } |
| |
| private final Dialog mDialog; |
| private boolean mRegistered; |
| private final BroadcastDispatcher mBroadcastDispatcher; |
| private final DialogLaunchAnimator mDialogLaunchAnimator; |
| |
| DismissReceiver(Dialog dialog) { |
| mDialog = dialog; |
| // TODO(b/219008720): Remove those calls to Dependency.get. |
| mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class); |
| mDialogLaunchAnimator = Dependency.get(DialogLaunchAnimator.class); |
| } |
| |
| void register() { |
| mBroadcastDispatcher.registerReceiver(this, INTENT_FILTER, null, UserHandle.CURRENT); |
| mRegistered = true; |
| } |
| |
| void unregister() { |
| if (mRegistered) { |
| mBroadcastDispatcher.unregisterReceiver(this); |
| mRegistered = false; |
| } |
| } |
| |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| // These broadcast are usually received when locking the device, swiping up to home |
| // (which collapses the shade), etc. In those cases, we usually don't want to animate |
| // back into the view. |
| mDialogLaunchAnimator.disableAllCurrentDialogsExitAnimations(); |
| mDialog.dismiss(); |
| } |
| } |
| |
| } |