Skip to content

Commit

Permalink
[UPM] Expose GMS Loading dialog status
Browse files Browse the repository at this point in the history
It indicates if the dialog was cancelled by the user action or due to
loading operation finished. If the used dismisses the dialog manually,
the pending operation should be cancelled.

dismissDialog() takes no cause to not expose implementation details
and keep coordinator trivial.

Bug: 1318895
Change-Id: I3e4a9a9e850465908a7acd030c24d4bb59d049ac
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3620437
Reviewed-by: Friedrich Horschig <fhorschig@chromium.org>
Commit-Queue: Maxim Anufriev <maxan@google.com>
Cr-Commit-Position: refs/heads/main@{#998784}
  • Loading branch information
Maxim Anufriev authored and Chromium LUCI CQ committed May 3, 2022
1 parent 1c6c795 commit 785f7ba
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@
import android.view.LayoutInflater;
import android.widget.RelativeLayout;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;

import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.ui.modaldialog.DialogDismissalCause;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modelutil.PropertyModel;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
* Coordinator class for displaying the loading modal dialog.
* It proxies the communication to the {@link LoadingModalDialogMediator}.
Expand All @@ -24,6 +27,26 @@ public class LoadingModalDialogCoordinator {
private final LoadingModalDialogMediator mMediator;
private final RelativeLayout mCustomView;

// Used to indicate the current loading dialog state.
@IntDef({State.READY, State.LOADING_DELAYED, State.LOADING_SHOWN, State.FINISHED_SHOWN,
State.FINISHED, State.CANCELLED})
@Retention(RetentionPolicy.SOURCE)
public @interface State {
/** Loading is not started, the dialog is not shown. */
int READY = 0;
/** Loading in progress, the dialog is delayed. */
int LOADING_DELAYED = 1;
/** Loading in progress, the dialog is visible. */
int LOADING_SHOWN = 2;
/** Loading finished, the dialog dismissal is delayed. */
int FINISHED_SHOWN = 3;
/** Loading finished, the dialog is dismissed. */
int FINISHED = 4;
/** User dismissed the dialog before the loading finished. */
int CANCELLED = 5;
int NUM_ENTRIES = 6;
}

/**
* Creates the {@link LoadingModalDialogCoordinator}.
*
Expand Down Expand Up @@ -62,12 +85,15 @@ public void show() {
mMediator.showDialog(dialogModel);
}

/** Dismisses the loading modal dialog. */
public void dismiss() {
mMediator.dismissDialog();
}

/**
* Dismisses the loading modal dialog.
*
* @param dismissalCause {@link DialogDismissalCause} indicating the dismissal cause
* Indicates the current dialog state.
*/
public void dismiss(@DialogDismissalCause int dismissalCause) {
mMediator.dismissDialog(dismissalCause);
public @State int getState() {
return mMediator.getState();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,26 @@ class LoadingModalDialogMediator
private ModalDialogManager mDialogManager;
private PropertyModel mModel;

/**
* Tracks whether the Dialog should be displayed when {@link #showDialogImmediately()} is run.
* Android doesn't always cancel a Runnable when requested, meaning that the Dialog could be
* hidden before it even has a chance to be shown.
*/
private boolean mShouldShow;
private Long mShownAtMs;
private long mShownAtMs;

private @LoadingModalDialogCoordinator.State int mState;

/** ModalDialogProperties.Controller implementation */
@Override
public void onClick(PropertyModel model, @ButtonType int buttonType) {}
public void onClick(PropertyModel model, @ButtonType int buttonType) {
// TODO(crbug.com/1311674): Dismiss as follows after button is added
// dismissDialogImmediately(DialogDismissalCause.NEGATIVE_BUTTON_CLICKED);
}

@Override
public void onDismiss(PropertyModel model, @DialogDismissalCause int dismissalCause) {
mDialogManager.removeObserver(this);
mHandler.removeCallbacksAndMessages(null);
if (dismissalCause != DialogDismissalCause.ACTION_ON_DIALOG_COMPLETED) {
mState = LoadingModalDialogCoordinator.State.CANCELLED;
} else {
mState = LoadingModalDialogCoordinator.State.FINISHED;
}
}

/**
Expand All @@ -60,12 +65,14 @@ public void onDismiss(PropertyModel model, @DialogDismissalCause int dismissalCa
@Override
public void onDialogAdded(PropertyModel model) {
if (model != mModel) return;
mState = LoadingModalDialogCoordinator.State.LOADING_SHOWN;
mShownAtMs = Long.valueOf(SystemClock.elapsedRealtime());
}

LoadingModalDialogMediator(ObservableSupplier<ModalDialogManager> dialogManagerSupplier) {
assert dialogManagerSupplier != null;
mDialogManagerSupplier = dialogManagerSupplier;
mState = LoadingModalDialogCoordinator.State.READY;
}

/**
Expand All @@ -76,56 +83,73 @@ public void onDialogAdded(PropertyModel model) {
*
*/
void showDialog(PropertyModel model) {
assert mModel == null : "dialog is already visible or pending";
assert mState == LoadingModalDialogCoordinator.State.READY;

ModalDialogManager dialogManager = mDialogManagerSupplier.get();
if (dialogManager == null) return;

mDialogManager = dialogManager;
mModel = model;
mShouldShow = true;
mState = LoadingModalDialogCoordinator.State.LOADING_DELAYED;
mHandler.postDelayed(this::showDialogImmediately, SHOW_DELAY_TIME_MS);
}

/**
* Dismisses the currently visible dialog or cancelling the pending dialog if it is not visible
* yet. If dialog is already visible for at least {@link #MINIMUM_SHOW_TIME_MS}, it will be
* dismissed immediately. Otherwise it will be dismissed after being visible for that period of
* time.
*
* @param dismissalCause The {@link DialogDismissalCause} that describes why the dialog is
* dismissed.
* time. This method should be called when the loading finishes.
*/
void dismissDialog(@DialogDismissalCause int dismissalCause) {
if (mModel == null) return;
mShouldShow = false;
void dismissDialog() {
if (mState != LoadingModalDialogCoordinator.State.LOADING_DELAYED
&& mState != LoadingModalDialogCoordinator.State.LOADING_SHOWN) {
return;
}

mHandler.removeCallbacksAndMessages(null);

final long currentTimeMs = SystemClock.elapsedRealtime();
System.out.println("currentTimeMs = " + currentTimeMs);

if (mShownAtMs != null && mShownAtMs.longValue() + MINIMUM_SHOW_TIME_MS > currentTimeMs) {
if (mState == LoadingModalDialogCoordinator.State.LOADING_SHOWN
&& mShownAtMs + MINIMUM_SHOW_TIME_MS > currentTimeMs) {
// Dialog dismiss should be postponed to prevent UI flicker.
Runnable dismissRunnable = () -> dismissDialogImmediately(dismissalCause);
mState = LoadingModalDialogCoordinator.State.FINISHED_SHOWN;
Runnable dismissRunnable =
() -> dismissDialogImmediately(DialogDismissalCause.ACTION_ON_DIALOG_COMPLETED);
mHandler.postDelayed(
dismissRunnable, mShownAtMs.longValue() + MINIMUM_SHOW_TIME_MS - currentTimeMs);
dismissRunnable, mShownAtMs + MINIMUM_SHOW_TIME_MS - currentTimeMs);
} else {
// Dialog is not yet shown or has been visible long enough.
dismissDialogImmediately(dismissalCause);
dismissDialogImmediately(DialogDismissalCause.ACTION_ON_DIALOG_COMPLETED);
}
}

/**
* Indicates the current dialog state.
*/
@LoadingModalDialogCoordinator.State
int getState() {
return mState;
}

/** Immediately shows the dialog. */
private void showDialogImmediately() {
if (!mShouldShow) return;
if (mState != LoadingModalDialogCoordinator.State.LOADING_DELAYED) return;
mDialogManager.addObserver(this);
mDialogManager.showDialog(mModel, ModalDialogManager.ModalDialogType.TAB);
}

/** Immediately dismisses the dialog. */
/**
* Immediately dismisses the dialog.
*
* @param dismissalCause The {@link DialogDismissalCause} that describes why the dialog is
* dismissed.
*/
private void dismissDialogImmediately(@DialogDismissalCause int dismissalCause) {
if (mShouldShow) return;
if (mState != LoadingModalDialogCoordinator.State.FINISHED_SHOWN
&& mState != LoadingModalDialogCoordinator.State.LOADING_SHOWN
&& mState != LoadingModalDialogCoordinator.State.LOADING_DELAYED) {
return;
}
mDialogManager.dismissDialog(mModel, dismissalCause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package org.chromium.chrome.browser.loading_modal;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
Expand Down Expand Up @@ -71,10 +72,14 @@ public void testLoadedBeforeDelay() {
verify(mModalDialogManager, never())
.showDialog(mModel, ModalDialogManager.ModalDialogType.TAB);

mMediator.dismissDialog(DialogDismissalCause.ACTION_ON_DIALOG_COMPLETED);
mMediator.dismissDialog();
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
verify(mModalDialogManager, never())
.showDialog(mModel, ModalDialogManager.ModalDialogType.TAB);
verify(mModalDialogManager, times(1))
.dismissDialog(mModel, DialogDismissalCause.ACTION_ON_DIALOG_COMPLETED);
mMediator.onDismiss(mModel, DialogDismissalCause.ACTION_ON_DIALOG_COMPLETED);
assertEquals(mMediator.getState(), LoadingModalDialogCoordinator.State.FINISHED);
}

@Test
Expand All @@ -86,9 +91,11 @@ public void testLoadedAfterDelayBeforeDialog() {
verify(mModalDialogManager, times(1))
.showDialog(mModel, ModalDialogManager.ModalDialogType.TAB);

mMediator.dismissDialog(DialogDismissalCause.ACTION_ON_DIALOG_COMPLETED);
mMediator.dismissDialog();
verify(mModalDialogManager, times(1))
.dismissDialog(mModel, DialogDismissalCause.ACTION_ON_DIALOG_COMPLETED);
mMediator.onDismiss(mModel, DialogDismissalCause.ACTION_ON_DIALOG_COMPLETED);
assertEquals(mMediator.getState(), LoadingModalDialogCoordinator.State.FINISHED);
}

@Test
Expand All @@ -104,7 +111,7 @@ public void testLoadedAfterDelayAndPostponed() {
ShadowLooper.idleMainLooper(1000, TimeUnit.MILLISECONDS);
mMediator.onDialogAdded(mModel);

mMediator.dismissDialog(DialogDismissalCause.ACTION_ON_DIALOG_COMPLETED);
mMediator.dismissDialog();
// Dialog was sent 1400 ms ago, but is visible for 400 ms only, so it should not be
// dismissed yet.
ShadowLooper.idleMainLooper(400, TimeUnit.MILLISECONDS);
Expand All @@ -115,5 +122,59 @@ public void testLoadedAfterDelayAndPostponed() {
ShadowLooper.idleMainLooper(100, TimeUnit.MILLISECONDS);
verify(mModalDialogManager, times(1))
.dismissDialog(mModel, DialogDismissalCause.ACTION_ON_DIALOG_COMPLETED);
mMediator.onDismiss(mModel, DialogDismissalCause.ACTION_ON_DIALOG_COMPLETED);
assertEquals(mMediator.getState(), LoadingModalDialogCoordinator.State.FINISHED);
}

@Test
public void testIsCancelledWhenSystemBackUsed() {
// Tests that dialog status updates correctly when cancelled.
mMediator.showDialog(mModel);
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
mMediator.onDialogAdded(mModel);
mMediator.onDismiss(mModel, DialogDismissalCause.NAVIGATE_BACK_OR_TOUCH_OUTSIDE);
assertEquals(mMediator.getState(), LoadingModalDialogCoordinator.State.CANCELLED);
}

@Test
public void testStatusIsUpdatedToFinished() {
// Tests that dialog status updates correctly up to finished.
assertEquals(mMediator.getState(), LoadingModalDialogCoordinator.State.READY);

mMediator.showDialog(mModel);
assertEquals(mMediator.getState(), LoadingModalDialogCoordinator.State.LOADING_DELAYED);

ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
verify(mModalDialogManager, times(1))
.showDialog(mModel, ModalDialogManager.ModalDialogType.TAB);
assertEquals(mMediator.getState(), LoadingModalDialogCoordinator.State.LOADING_DELAYED);

mMediator.onDialogAdded(mModel);
assertEquals(mMediator.getState(), LoadingModalDialogCoordinator.State.LOADING_SHOWN);

mMediator.dismissDialog();
assertEquals(mMediator.getState(), LoadingModalDialogCoordinator.State.FINISHED_SHOWN);

ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
verify(mModalDialogManager, times(1))
.dismissDialog(mModel, DialogDismissalCause.ACTION_ON_DIALOG_COMPLETED);
assertEquals(mMediator.getState(), LoadingModalDialogCoordinator.State.FINISHED_SHOWN);

mMediator.onDismiss(mModel, DialogDismissalCause.ACTION_ON_DIALOG_COMPLETED);
assertEquals(mMediator.getState(), LoadingModalDialogCoordinator.State.FINISHED);
}

@Test
public void testStatusIsUpdatedToCancelledWhenFinished() {
// Tests that dialog status updates correctly when the user dismisses the dialog after
// the loading finished, but when the dialog is still visible to prevent flickering.
mMediator.showDialog(mModel);
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
mMediator.onDialogAdded(mModel);
mMediator.dismissDialog();
assertEquals(mMediator.getState(), LoadingModalDialogCoordinator.State.FINISHED_SHOWN);

mMediator.onDismiss(mModel, DialogDismissalCause.NAVIGATE_BACK_OR_TOUCH_OUTSIDE);
assertEquals(mMediator.getState(), LoadingModalDialogCoordinator.State.CANCELLED);
}
}

0 comments on commit 785f7ba

Please sign in to comment.