blob: 8dd62b77e70bc80f77f14f1210f83e9174ad2b36 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.ui.modaldialog;
import android.util.SparseArray;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.chromium.base.Callback;
import org.chromium.base.CommandLine;
import org.chromium.base.ObserverList;
import org.chromium.base.VisibleForTesting;
import org.chromium.ui.UiSwitches;
import org.chromium.ui.modelutil.PropertyModel;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Manager for managing the display of a queue of {@link PropertyModel}s.
*/
public class ModalDialogManager {
/**
* An observer of the ModalDialogManager intended to broadcast notifications about any dialog
* being shown. Observers will know if something is overlaying the screen.
*/
public interface ModalDialogManagerObserver {
/**
* A notification that the manager started showing a modal dialog.
* @param model The model that describes the dialog that was shown.
*/
void onDialogShown(PropertyModel model);
/**
* A notification that the manager hid a modal dialog.
* @param model The model that describes the dialog that was hidden.
*/
void onDialogHidden(PropertyModel model);
}
/**
* Present a {@link PropertyModel} in a container.
*/
public static abstract class Presenter {
private Callback<Integer> mDismissCallback;
private PropertyModel mDialogModel;
/**
* @param model The dialog model that's currently showing in this presenter.
* If null, no dialog is currently showing.
*/
private void setDialogModel(
@Nullable PropertyModel model, @Nullable Callback<Integer> dismissCallback) {
if (model == null) {
removeDialogView(mDialogModel);
mDialogModel = null;
mDismissCallback = null;
} else {
assert mDialogModel
== null : "Should call setDialogModel(null) before setting a dialog model.";
mDialogModel = model;
mDismissCallback = dismissCallback;
addDialogView(model);
}
}
/**
* Run the cached cancel callback and reset the cached callback.
*/
public final void dismissCurrentDialog(@DialogDismissalCause int dismissalCause) {
if (mDismissCallback == null) return;
// Set #mCancelCallback to null before calling the callback to avoid it being
// updated during the callback.
Callback<Integer> callback = mDismissCallback;
mDismissCallback = null;
callback.onResult(dismissalCause);
}
/**
* @return The dialog model that this presenter is showing.
*/
public final PropertyModel getDialogModel() {
return mDialogModel;
}
/**
* @param model The dialog model from which the properties should be obtained.
* @return The property value for {@link ModalDialogProperties#CONTENT_DESCRIPTION}, or a
* fallback content description if it is not set.
*/
protected static String getContentDescription(PropertyModel model) {
String description = model.get(ModalDialogProperties.CONTENT_DESCRIPTION);
if (description == null) description = model.get(ModalDialogProperties.TITLE);
return description;
}
/**
* Creates a view for the specified dialog model and puts the view in a container.
* @param model The dialog model that needs to be shown.
*/
protected abstract void addDialogView(PropertyModel model);
/**
* Removes the view created for the specified model from a container.
* @param model The dialog model that needs to be removed.
*/
protected abstract void removeDialogView(PropertyModel model);
}
@IntDef({ModalDialogType.APP, ModalDialogType.TAB})
@Retention(RetentionPolicy.SOURCE)
public @interface ModalDialogType {
// The integer assigned to each type represents its priority. A smaller number represents a
// higher priority type of dialog.
int APP = 0;
int TAB = 1;
}
/** Mapping of the {@link Presenter}s and the type of dialogs they are showing. */
private final SparseArray<Presenter> mPresenters = new SparseArray<>();
/** Mapping of the lists of pending dialogs and the type of the dialogs. */
private final SparseArray<List<PropertyModel>> mPendingDialogs = new SparseArray<>();
/**
* The list of suspended types of dialogs. The dialogs of types in the list will be suspended
* from showing and will only be shown after {@link #resumeType(int)} is called.
*/
private final Set<Integer> mSuspendedTypes = new HashSet<>();
/** The default presenter to be used if a specified type is not supported. */
private final Presenter mDefaultPresenter;
/**
* The presenter of the type of the dialog that is currently showing. Note that if there is no
* matching {@link Presenter} for {@link #mCurrentType}, this will be the default presenter.
*/
private Presenter mCurrentPresenter;
/**
* The type of the current dialog. This can be different from the type of the current
* {@link Presenter} if there is no registered presenter for this type.
*/
private @ModalDialogType int mCurrentType;
/**
* True if the current dialog is in the process of being dismissed.
*/
private boolean mDismissingCurrentDialog;
/** Observers of this manager. */
private final ObserverList<ModalDialogManagerObserver> mObserverList = new ObserverList<>();
/**
* Constructor for initializing default {@link Presenter}.
* @param defaultPresenter The default presenter to be used when no presenter specified.
* @param defaultType The dialog type of the default presenter.
*/
public ModalDialogManager(
@NonNull Presenter defaultPresenter, @ModalDialogType int defaultType) {
mDefaultPresenter = defaultPresenter;
registerPresenter(defaultPresenter, defaultType);
}
/** Clears any dependencies on the showing or pending dialogs. */
public void destroy() {
dismissAllDialogs(DialogDismissalCause.ACTIVITY_DESTROYED);
mObserverList.clear();
}
/**
* Add an observer to this manager.
* @param observer The observer to add.
*/
public void addObserver(ModalDialogManagerObserver observer) {
mObserverList.addObserver(observer);
}
/**
* Remove an observer of this manager.
* @param observer The observer to remove.
*/
public void removeObserver(ModalDialogManagerObserver observer) {
mObserverList.removeObserver(observer);
}
/**
* Register a {@link Presenter} that shows a specific type of dialog. Note that only one
* presenter of each type can be registered.
* @param presenter The {@link Presenter} to be registered.
* @param dialogType The type of the dialog shown by the specified presenter.
*/
public void registerPresenter(Presenter presenter, @ModalDialogType int dialogType) {
assert mPresenters.get(dialogType)
== null : "Only one presenter can be registered for each type.";
mPresenters.put(dialogType, presenter);
}
/**
* @return Whether a dialog is currently showing.
*/
public boolean isShowing() {
return mCurrentPresenter != null;
}
/**
* Show the specified dialog. If another dialog is currently showing, the specified dialog will
* be added to the end of the pending dialog list of the specified type.
* @param model The dialog model to be shown or added to pending list.
* @param dialogType The type of the dialog to be shown.
*/
public void showDialog(PropertyModel model, @ModalDialogType int dialogType) {
showDialog(model, dialogType, false);
}
/**
* Show the specified dialog. If another dialog is currently showing, the specified dialog will
* be added to the pending dialog list. If showNext is set to true, the dialog will be added
* to the top of the pending list of its type, otherwise it will be added to the end.
* @param model The dialog model to be shown or added to pending list.
* @param dialogType The type of the dialog to be shown.
* @param showAsNext Whether the specified dialog should be set highest priority of its type.
*/
public void showDialog(
PropertyModel model, @ModalDialogType int dialogType, boolean showAsNext) {
if (CommandLine.getInstance().hasSwitch(UiSwitches.ENABLE_SCREENSHOT_UI_MODE)) {
return;
}
List<PropertyModel> dialogs = mPendingDialogs.get(dialogType);
if (dialogs == null) mPendingDialogs.put(dialogType, dialogs = new ArrayList<>());
// Put the new dialog in pending list if the dialog type is suspended or the current dialog
// is of higher priority.
if (mSuspendedTypes.contains(dialogType) || (isShowing() && mCurrentType <= dialogType)) {
dialogs.add(showAsNext ? 0 : dialogs.size(), model);
return;
}
if (isShowing()) suspendCurrentDialog();
assert !isShowing();
mCurrentType = dialogType;
mCurrentPresenter = mPresenters.get(dialogType, mDefaultPresenter);
mCurrentPresenter.setDialogModel(
model, (dismissalCause) -> dismissDialog(model, dismissalCause));
for (ModalDialogManagerObserver o : mObserverList) o.onDialogShown(model);
}
/**
* Dismiss the specified dialog. If the dialog is not currently showing, it will be removed from
* the pending dialog list. If the dialog is currently being dismissed this function does
* nothing.
* @param model The dialog model to be dismissed or removed from pending list.
* @param dismissalCause The {@link DialogDismissalCause} that describes why the dialog is
* dismissed.
*/
public void dismissDialog(PropertyModel model, @DialogDismissalCause int dismissalCause) {
if (model == null) return;
if (mCurrentPresenter == null || model != mCurrentPresenter.getDialogModel()) {
for (int i = 0; i < mPendingDialogs.size(); ++i) {
List<PropertyModel> dialogs = mPendingDialogs.valueAt(i);
for (int j = 0; j < dialogs.size(); ++j) {
if (dialogs.get(j) == model) {
dialogs.remove(j)
.get(ModalDialogProperties.CONTROLLER)
.onDismiss(model, dismissalCause);
return;
}
}
}
// If the specified dialog is not found, return without any callbacks.
return;
}
if (!isShowing()) return;
assert model == mCurrentPresenter.getDialogModel();
if (mDismissingCurrentDialog) return;
mDismissingCurrentDialog = true;
model.get(ModalDialogProperties.CONTROLLER).onDismiss(model, dismissalCause);
for (ModalDialogManagerObserver o : mObserverList) o.onDialogHidden(model);
mCurrentPresenter.setDialogModel(null, null);
mCurrentPresenter = null;
mDismissingCurrentDialog = false;
showNextDialog();
}
/**
* Dismiss the dialog currently shown and remove all pending dialogs.
* @param dismissalCause The {@link DialogDismissalCause} that describes why the dialogs are
* dismissed.
*/
public void dismissAllDialogs(@DialogDismissalCause int dismissalCause) {
for (int i = 0; i < mPendingDialogs.size(); ++i) {
dismissPendingDialogsOfType(mPendingDialogs.keyAt(i), dismissalCause);
}
if (isShowing()) dismissDialog(mCurrentPresenter.getDialogModel(), dismissalCause);
}
/**
* Dismiss the dialog currently shown and remove all pending dialogs of the specified type.
* @param dialogType The specified type of dialog.
* @param dismissalCause The {@link DialogDismissalCause} that describes why the dialogs are
* dismissed.
*/
public void dismissDialogsOfType(
@ModalDialogType int dialogType, @DialogDismissalCause int dismissalCause) {
dismissPendingDialogsOfType(dialogType, dismissalCause);
if (isShowing() && dialogType == mCurrentType) {
dismissDialog(mCurrentPresenter.getDialogModel(), dismissalCause);
}
}
/** Helper method to dismiss pending dialogs of the specified type. */
private void dismissPendingDialogsOfType(
@ModalDialogType int dialogType, @DialogDismissalCause int dismissalCause) {
List<PropertyModel> dialogs = mPendingDialogs.get(dialogType);
if (dialogs == null) return;
while (!dialogs.isEmpty()) {
PropertyModel model = dialogs.remove(0);
ModalDialogProperties.Controller controller =
model.get(ModalDialogProperties.CONTROLLER);
controller.onDismiss(model, dismissalCause);
}
}
/**
* Suspend all dialogs of the specified type, including the one currently shown. These dialogs
* will be prevented from showing unless {@link #resumeType(int)} is called after the
* suspension. If the current dialog is suspended, it will be moved back to the first dialog
* in the pending list. Any dialogs of the specified type in the pending list will be skipped.
* @param dialogType The specified type of dialogs to be suspended.
*/
public void suspendType(@ModalDialogType int dialogType) {
mSuspendedTypes.add(dialogType);
if (isShowing() && dialogType == mCurrentType) {
suspendCurrentDialog();
showNextDialog();
}
}
/**
* Resume the specified type of dialogs after suspension.
* @param dialogType The specified type of dialogs to be resumed.
*/
public void resumeType(@ModalDialogType int dialogType) {
mSuspendedTypes.remove(dialogType);
if (!isShowing()) showNextDialog();
}
/** Hide the current dialog and put it back to the front of the pending list. */
private void suspendCurrentDialog() {
assert isShowing();
PropertyModel dialogView = mCurrentPresenter.getDialogModel();
mCurrentPresenter.setDialogModel(null, null);
mCurrentPresenter = null;
mPendingDialogs.get(mCurrentType).add(0, dialogView);
}
/** Helper method for showing the next available dialog in the pending dialog list. */
private void showNextDialog() {
assert !isShowing();
// Show the next dialog of highest priority that its type is not suspended.
for (int i = 0; i < mPendingDialogs.size(); ++i) {
int dialogType = mPendingDialogs.keyAt(i);
if (mSuspendedTypes.contains(dialogType)) continue;
List<PropertyModel> dialogs = mPendingDialogs.valueAt(i);
if (!dialogs.isEmpty()) {
showDialog(dialogs.remove(0), dialogType);
return;
}
}
}
@VisibleForTesting
public PropertyModel getCurrentDialogForTest() {
return mCurrentPresenter == null ? null : mCurrentPresenter.getDialogModel();
}
@VisibleForTesting
public List<PropertyModel> getPendingDialogsForTest(@ModalDialogType int dialogType) {
return mPendingDialogs.get(dialogType);
}
@VisibleForTesting
public Presenter getPresenterForTest(@ModalDialogType int dialogType) {
return mPresenters.get(dialogType);
}
@VisibleForTesting
public Presenter getCurrentPresenterForTest() {
return mCurrentPresenter;
}
}