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