1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.ui.modaldialog;
6 
7 import android.util.SparseArray;
8 
9 import androidx.annotation.IntDef;
10 import androidx.annotation.NonNull;
11 import androidx.annotation.Nullable;
12 import androidx.annotation.VisibleForTesting;
13 
14 import org.chromium.base.Callback;
15 import org.chromium.base.CommandLine;
16 import org.chromium.base.ObserverList;
17 import org.chromium.ui.UiSwitches;
18 import org.chromium.ui.modelutil.PropertyModel;
19 import org.chromium.ui.util.TokenHolder;
20 
21 import java.lang.annotation.Retention;
22 import java.lang.annotation.RetentionPolicy;
23 import java.util.ArrayList;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Set;
29 
30 /**
31  * Manager for managing the display of a queue of {@link PropertyModel}s.
32  */
33 public class ModalDialogManager {
34     /**
35      * An observer of the ModalDialogManager intended to broadcast notifications about any dialog
36      * being shown. Observers will know if something is overlaying the screen.
37      */
38     public interface ModalDialogManagerObserver {
39         /**
40          * A notification that the manager queues a dialog to be shown.
41          * @param model The model that describes the dialog that was added.
42          */
onDialogAdded(PropertyModel model)43         default void onDialogAdded(PropertyModel model) {}
44 
45         /**
46          * A notification that the manager dismisses a modal dialog.
47          * @param model The model that describes the dialog that was dismissed.
48          */
onDialogDismissed(PropertyModel model)49         default void onDialogDismissed(PropertyModel model) {}
50 
51         /** A notification that the manager has dismissed all queued modal dialog. */
onLastDialogDismissed()52         default void onLastDialogDismissed() {}
53     }
54 
55     /**
56      * Present a {@link PropertyModel} in a container.
57      */
58     public abstract static class Presenter {
59         private Callback<Integer> mDismissCallback;
60         private PropertyModel mDialogModel;
61 
62         /**
63          * @param model The dialog model that's currently showing in this presenter.
64          *              If null, no dialog is currently showing.
65          */
setDialogModel( @ullable PropertyModel model, @Nullable Callback<Integer> dismissCallback)66         private void setDialogModel(
67                 @Nullable PropertyModel model, @Nullable Callback<Integer> dismissCallback) {
68             if (model == null) {
69                 removeDialogView(mDialogModel);
70                 mDialogModel = null;
71                 mDismissCallback = null;
72             } else {
73                 assert mDialogModel
74                         == null : "Should call setDialogModel(null) before setting a dialog model.";
75                 mDialogModel = model;
76                 mDismissCallback = dismissCallback;
77                 addDialogView(model);
78             }
79         }
80 
81         /**
82          * Run the cached cancel callback and reset the cached callback.
83          */
dismissCurrentDialog(@ialogDismissalCause int dismissalCause)84         public final void dismissCurrentDialog(@DialogDismissalCause int dismissalCause) {
85             if (mDismissCallback == null) return;
86 
87             // Set #mCancelCallback to null before calling the callback to avoid it being
88             // updated during the callback.
89             Callback<Integer> callback = mDismissCallback;
90             mDismissCallback = null;
91             callback.onResult(dismissalCause);
92         }
93 
94         /**
95          * @return The dialog model that this presenter is showing.
96          */
getDialogModel()97         public final PropertyModel getDialogModel() {
98             return mDialogModel;
99         }
100 
101         /**
102          * @param model The dialog model from which the properties should be obtained.
103          * @return The property value for {@link ModalDialogProperties#CONTENT_DESCRIPTION}, or a
104          *         fallback content description if it is not set.
105          */
getContentDescription(PropertyModel model)106         protected static String getContentDescription(PropertyModel model) {
107             String description = model.get(ModalDialogProperties.CONTENT_DESCRIPTION);
108             if (description == null) description = model.get(ModalDialogProperties.TITLE);
109             return description;
110         }
111 
112         /**
113          * Creates a view for the specified dialog model and puts the view in a container.
114          * @param model The dialog model that needs to be shown.
115          */
addDialogView(PropertyModel model)116         protected abstract void addDialogView(PropertyModel model);
117 
118         /**
119          * Removes the view created for the specified model from a container.
120          * @param model The dialog model that needs to be removed.
121          */
removeDialogView(PropertyModel model)122         protected abstract void removeDialogView(PropertyModel model);
123     }
124 
125     @IntDef({ModalDialogType.APP, ModalDialogType.TAB})
126     @Retention(RetentionPolicy.SOURCE)
127     public @interface ModalDialogType {
128         // The integer assigned to each type represents its priority. A smaller number represents a
129         // higher priority type of dialog.
130         int APP = 0;
131         int TAB = 1;
132     }
133 
134     /** Mapping of the {@link Presenter}s and the type of dialogs they are showing. */
135     private final SparseArray<Presenter> mPresenters = new SparseArray<>();
136 
137     /** Mapping of the lists of pending dialogs and the type of the dialogs. */
138     private final SparseArray<List<PropertyModel>> mPendingDialogs = new SparseArray<>();
139 
140     /**
141      * The list of suspended types of dialogs. The dialogs of types in the list will be suspended
142      * from showing and will only be shown after {@link #resumeType(int)} is called.
143      */
144     private final Set<Integer> mSuspendedTypes = new HashSet<>();
145 
146     /** The default presenter to be used if a specified type is not supported. */
147     private final Presenter mDefaultPresenter;
148 
149     /**
150      * The presenter of the type of the dialog that is currently showing. Note that if there is no
151      * matching {@link Presenter} for {@link #mCurrentType}, this will be the default presenter.
152      */
153     private Presenter mCurrentPresenter;
154 
155     /**
156      * The type of the current dialog. This can be different from the type of the current
157      * {@link Presenter} if there is no registered presenter for this type.
158      */
159     private @ModalDialogType int mCurrentType;
160 
161     /**
162      * True if the current dialog is in the process of being dismissed.
163      */
164     private boolean mDismissingCurrentDialog;
165 
166     /** Observers of this manager. */
167     private final ObserverList<ModalDialogManagerObserver> mObserverList = new ObserverList<>();
168 
169     /** Tokens for features temporarily suppressing dialogs. */
170     private final Map<Integer, TokenHolder> mTokenHolders = new HashMap<>();
171 
172     /**
173      * Constructor for initializing default {@link Presenter}.
174      * @param defaultPresenter The default presenter to be used when no presenter specified.
175      * @param defaultType The dialog type of the default presenter.
176      */
ModalDialogManager( @onNull Presenter defaultPresenter, @ModalDialogType int defaultType)177     public ModalDialogManager(
178             @NonNull Presenter defaultPresenter, @ModalDialogType int defaultType) {
179         mDefaultPresenter = defaultPresenter;
180         registerPresenter(defaultPresenter, defaultType);
181 
182         mTokenHolders.put(ModalDialogType.APP,
183                 new TokenHolder(() -> resumeTypeInternal(ModalDialogType.APP)));
184         mTokenHolders.put(ModalDialogType.TAB,
185                 new TokenHolder(() -> resumeTypeInternal(ModalDialogType.TAB)));
186     }
187 
188     /** Clears any dependencies on the showing or pending dialogs. */
destroy()189     public void destroy() {
190         dismissAllDialogs(DialogDismissalCause.ACTIVITY_DESTROYED);
191         mObserverList.clear();
192     }
193 
194     /**
195      * Add an observer to this manager.
196      * @param observer The observer to add.
197      */
addObserver(ModalDialogManagerObserver observer)198     public void addObserver(ModalDialogManagerObserver observer) {
199         mObserverList.addObserver(observer);
200     }
201 
202     /**
203      * Remove an observer of this manager.
204      * @param observer The observer to remove.
205      */
removeObserver(ModalDialogManagerObserver observer)206     public void removeObserver(ModalDialogManagerObserver observer) {
207         mObserverList.removeObserver(observer);
208     }
209 
210     /**
211      * Register a {@link Presenter} that shows a specific type of dialog. Note that only one
212      * presenter of each type can be registered.
213      * @param presenter The {@link Presenter} to be registered.
214      * @param dialogType The type of the dialog shown by the specified presenter.
215      */
registerPresenter(Presenter presenter, @ModalDialogType int dialogType)216     public void registerPresenter(Presenter presenter, @ModalDialogType int dialogType) {
217         assert mPresenters.get(dialogType)
218                 == null : "Only one presenter can be registered for each type.";
219         mPresenters.put(dialogType, presenter);
220     }
221 
222     /**
223      * @return Whether a dialog is currently showing.
224      */
isShowing()225     public boolean isShowing() {
226         return mCurrentPresenter != null;
227     }
228 
229     /**
230      * @return The type of dialog showing, or last type that was shown.
231      */
getCurrentType()232     public @ModalDialogType int getCurrentType() {
233         return mCurrentType;
234     }
235 
236     /**
237      * Show the specified dialog. If another dialog is currently showing, the specified dialog will
238      * be added to the end of the pending dialog list of the specified type.
239      * @param model The dialog model to be shown or added to pending list.
240      * @param dialogType The type of the dialog to be shown.
241      */
showDialog(PropertyModel model, @ModalDialogType int dialogType)242     public void showDialog(PropertyModel model, @ModalDialogType int dialogType) {
243         showDialog(model, dialogType, false);
244     }
245 
246     /**
247      * Show the specified dialog. If another dialog is currently showing, the specified dialog will
248      * be added to the pending dialog list. If showNext is set to true, the dialog will be added
249      * to the top of the pending list of its type, otherwise it will be added to the end.
250      * @param model The dialog model to be shown or added to pending list.
251      * @param dialogType The type of the dialog to be shown.
252      * @param showAsNext Whether the specified dialog should be set highest priority of its type.
253      */
showDialog( PropertyModel model, @ModalDialogType int dialogType, boolean showAsNext)254     public void showDialog(
255             PropertyModel model, @ModalDialogType int dialogType, boolean showAsNext) {
256         if (CommandLine.getInstance().hasSwitch(UiSwitches.ENABLE_SCREENSHOT_UI_MODE)) {
257             return;
258         }
259 
260         List<PropertyModel> dialogs = mPendingDialogs.get(dialogType);
261         if (dialogs == null) mPendingDialogs.put(dialogType, dialogs = new ArrayList<>());
262 
263         // Put the new dialog in pending list if the dialog type is suspended or the current dialog
264         // is of higher priority.
265         if (mSuspendedTypes.contains(dialogType) || (isShowing() && mCurrentType <= dialogType)) {
266             dialogs.add(showAsNext ? 0 : dialogs.size(), model);
267             return;
268         }
269 
270         if (isShowing()) suspendCurrentDialog();
271 
272         assert !isShowing();
273         mCurrentType = dialogType;
274         mCurrentPresenter = mPresenters.get(dialogType, mDefaultPresenter);
275         mCurrentPresenter.setDialogModel(
276                 model, (dismissalCause) -> dismissDialog(model, dismissalCause));
277         for (ModalDialogManagerObserver o : mObserverList) o.onDialogAdded(model);
278     }
279 
280     /**
281      * Dismiss the specified dialog. If the dialog is not currently showing, it will be removed from
282      * the pending dialog list. If the dialog is currently being dismissed this function does
283      * nothing.
284      * @param model The dialog model to be dismissed or removed from pending list.
285      * @param dismissalCause The {@link DialogDismissalCause} that describes why the dialog is
286      *                       dismissed.
287      */
dismissDialog(PropertyModel model, @DialogDismissalCause int dismissalCause)288     public void dismissDialog(PropertyModel model, @DialogDismissalCause int dismissalCause) {
289         if (model == null) return;
290         if (mCurrentPresenter == null || model != mCurrentPresenter.getDialogModel()) {
291             for (int i = 0; i < mPendingDialogs.size(); ++i) {
292                 List<PropertyModel> dialogs = mPendingDialogs.valueAt(i);
293                 for (int j = 0; j < dialogs.size(); ++j) {
294                     if (dialogs.get(j) == model) {
295                         dialogs.remove(j)
296                                 .get(ModalDialogProperties.CONTROLLER)
297                                 .onDismiss(model, dismissalCause);
298                         for (ModalDialogManagerObserver o : mObserverList) {
299                             o.onDialogDismissed(model);
300                         }
301                         dispatchOnLastDialogDismissedIfEmpty();
302                         return;
303                     }
304                 }
305             }
306             // If the specified dialog is not found, return without any callbacks.
307             return;
308         }
309 
310         if (!isShowing()) return;
311         assert model == mCurrentPresenter.getDialogModel();
312         if (mDismissingCurrentDialog) return;
313         mDismissingCurrentDialog = true;
314         model.get(ModalDialogProperties.CONTROLLER).onDismiss(model, dismissalCause);
315         for (ModalDialogManagerObserver o : mObserverList) o.onDialogDismissed(model);
316         mCurrentPresenter.setDialogModel(null, null);
317         mCurrentPresenter = null;
318         mDismissingCurrentDialog = false;
319         dispatchOnLastDialogDismissedIfEmpty();
320         showNextDialog();
321     }
322 
323     /**
324      * Dismiss the dialog currently shown and remove all pending dialogs.
325      * @param dismissalCause The {@link DialogDismissalCause} that describes why the dialogs are
326      *                       dismissed.
327      */
dismissAllDialogs(@ialogDismissalCause int dismissalCause)328     public void dismissAllDialogs(@DialogDismissalCause int dismissalCause) {
329         for (int i = 0; i < mPendingDialogs.size(); ++i) {
330             dismissPendingDialogsOfType(mPendingDialogs.keyAt(i), dismissalCause);
331         }
332         if (isShowing()) dismissDialog(mCurrentPresenter.getDialogModel(), dismissalCause);
333     }
334 
335     /**
336      * Dismiss the dialog currently shown and remove all pending dialogs of the specified type.
337      * @param dialogType The specified type of dialog.
338      * @param dismissalCause The {@link DialogDismissalCause} that describes why the dialogs are
339      *                       dismissed.
340      */
dismissDialogsOfType( @odalDialogType int dialogType, @DialogDismissalCause int dismissalCause)341     public void dismissDialogsOfType(
342             @ModalDialogType int dialogType, @DialogDismissalCause int dismissalCause) {
343         dismissPendingDialogsOfType(dialogType, dismissalCause);
344         dismissActiveDialogOfType(dialogType, dismissalCause);
345     }
346 
347     /**
348      * Dismiss the dialog currently shown if it is of the specified type.
349      *
350      * Any pending dialogs will then be shown.
351      *
352      * @param dialogType The specified type of dialog.
353      * @param dismissalCause The {@link DialogDismissalCause} that describes why the dialogs are
354      *                       dismissed.
355      * @return true if a dialog was showing and was dismissed.
356      */
dismissActiveDialogOfType( @odalDialogType int dialogType, @DialogDismissalCause int dismissalCause)357     public boolean dismissActiveDialogOfType(
358             @ModalDialogType int dialogType, @DialogDismissalCause int dismissalCause) {
359         if (isShowing() && dialogType == mCurrentType) {
360             dismissDialog(mCurrentPresenter.getDialogModel(), dismissalCause);
361             return true;
362         }
363         return false;
364     }
365 
366     /** Helper method to dismiss pending dialogs of the specified type. */
dismissPendingDialogsOfType( @odalDialogType int dialogType, @DialogDismissalCause int dismissalCause)367     private void dismissPendingDialogsOfType(
368             @ModalDialogType int dialogType, @DialogDismissalCause int dismissalCause) {
369         List<PropertyModel> dialogs = mPendingDialogs.get(dialogType);
370         if (dialogs == null || dialogs.isEmpty()) return;
371         while (!dialogs.isEmpty()) {
372             PropertyModel model = dialogs.remove(0);
373             ModalDialogProperties.Controller controller =
374                     model.get(ModalDialogProperties.CONTROLLER);
375             controller.onDismiss(model, dismissalCause);
376             for (ModalDialogManagerObserver o : mObserverList) o.onDialogDismissed(model);
377         }
378         dispatchOnLastDialogDismissedIfEmpty();
379     }
380 
381     /**
382      * Suspend all dialogs of the specified type, including the one currently shown. These dialogs
383      * will be prevented from showing unless {@link #resumeType(int, int)} is called after the
384      * suspension. If the current dialog is suspended, it will be moved back to the first dialog
385      * in the pending list. Any dialogs of the specified type in the pending list will be skipped.
386      * @param dialogType The specified type of dialogs to be suspended.
387      * @return A token to use when resuming the suspended type.
388      */
suspendType(@odalDialogType int dialogType)389     public int suspendType(@ModalDialogType int dialogType) {
390         mSuspendedTypes.add(dialogType);
391         if (isShowing() && dialogType == mCurrentType) {
392             suspendCurrentDialog();
393             showNextDialog();
394         }
395         return mTokenHolders.get(dialogType).acquireToken();
396     }
397 
398     /**
399      * Resume the specified type of dialogs after suspension. This method does not resume showing
400      * the dialog until after all held tokens are released.
401      * @param dialogType The specified type of dialogs to be resumed.
402      * @param token The token generated from suspending the dialog type.
403      */
resumeType(@odalDialogType int dialogType, int token)404     public void resumeType(@ModalDialogType int dialogType, int token) {
405         mTokenHolders.get(dialogType).releaseToken(token);
406     }
407 
408     /**
409      * Actually resumes showing the type of dialog after all tokens are released.
410      * @param dialogType The specified type of dialogs to be resumed.
411      */
resumeTypeInternal(@odalDialogType int dialogType)412     private void resumeTypeInternal(@ModalDialogType int dialogType) {
413         if (mTokenHolders.get(dialogType).hasTokens()) return;
414         mSuspendedTypes.remove(dialogType);
415         if (!isShowing()) showNextDialog();
416     }
417 
418     /** Hide the current dialog and put it back to the front of the pending list. */
suspendCurrentDialog()419     private void suspendCurrentDialog() {
420         assert isShowing();
421         PropertyModel dialogView = mCurrentPresenter.getDialogModel();
422         mCurrentPresenter.setDialogModel(null, null);
423         mCurrentPresenter = null;
424         mPendingDialogs.get(mCurrentType).add(0, dialogView);
425     }
426 
427     /** Helper method for showing the next available dialog in the pending dialog list. */
showNextDialog()428     private void showNextDialog() {
429         assert !isShowing();
430         // Show the next dialog of highest priority that its type is not suspended.
431         for (int i = 0; i < mPendingDialogs.size(); ++i) {
432             int dialogType = mPendingDialogs.keyAt(i);
433             if (mSuspendedTypes.contains(dialogType)) continue;
434 
435             List<PropertyModel> dialogs = mPendingDialogs.valueAt(i);
436             if (!dialogs.isEmpty()) {
437                 showDialog(dialogs.remove(0), dialogType);
438                 return;
439             }
440         }
441     }
442 
443     /** Helper method for determining if there are any available dialogs */
isPendingDialogsEmpty()444     private boolean isPendingDialogsEmpty() {
445         for (int i = 0; i < mPendingDialogs.size(); ++i) {
446             List<PropertyModel> dialogs = mPendingDialogs.valueAt(i);
447             if (!dialogs.isEmpty()) return false;
448         }
449         return true;
450     }
451 
452     // This calls onLastDialogDismissed() if there are no pending dialogs.
dispatchOnLastDialogDismissedIfEmpty()453     private void dispatchOnLastDialogDismissedIfEmpty() {
454         if (isPendingDialogsEmpty()) {
455             for (ModalDialogManagerObserver o : mObserverList) {
456                 o.onLastDialogDismissed();
457             }
458         }
459     }
460 
461     @VisibleForTesting
getCurrentDialogForTest()462     public PropertyModel getCurrentDialogForTest() {
463         return mCurrentPresenter == null ? null : mCurrentPresenter.getDialogModel();
464     }
465 
466     @VisibleForTesting
getPendingDialogsForTest(@odalDialogType int dialogType)467     public List<PropertyModel> getPendingDialogsForTest(@ModalDialogType int dialogType) {
468         return mPendingDialogs.get(dialogType);
469     }
470 
471     @VisibleForTesting
getPresenterForTest(@odalDialogType int dialogType)472     public Presenter getPresenterForTest(@ModalDialogType int dialogType) {
473         return mPresenters.get(dialogType);
474     }
475 
476     @VisibleForTesting
getCurrentPresenterForTest()477     public Presenter getCurrentPresenterForTest() {
478         return mCurrentPresenter;
479     }
480 }
481