chromium/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/DialogManager.java

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.password_manager.settings;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;

import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;

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

/**
 * This class manages a {@link DialogFragment}.
 * In particular, it ensures that the dialog stays visible for a minimum time period, so that
 * earlier calls to hide it are delayed appropriately. It also allows to override the delaying for
 * testing purposes.
 */
public final class DialogManager {
    /**
     * Contains the reference to a {@link android.app.DialogFragment} between the call to {@link
     * show} and dismissing the dialog.
     */
    @Nullable private DialogFragment mDialogFragment;

    /**
     * The least amout of time for which {@link mDialogFragment} should stay visible to avoid
     * flickering. It was chosen so that it is enough to read the approx. 3 words on it, but not too
     * long (to avoid the user waiting while Chrome is already idle).
     */
    private static final long MINIMUM_LIFE_SPAN_MILLIS = 1000L;

    /** This is used to post the unblocking signal for hiding the dialog fragment. */
    private CallbackDelayer mDelayer = new TimedCallbackDelayer(MINIMUM_LIFE_SPAN_MILLIS);

    /** Allows to fake the timed delayer. */
    public void replaceCallbackDelayerForTesting(CallbackDelayer newDelayer) {
        mDelayer = newDelayer;
    }

    /**
     * Used to gate hiding of a dialog on two actions: one automatic delayed signal and one manual
     * call to {@link hide}. This is not null between the calls to {@link show} and {@link hide}.
     */
    @Nullable private SingleThreadBarrierClosure mBarrierClosure;

    /** Callback to run after the dialog was hidden. Can be null if no hiding was requested.*/
    @Nullable private Runnable mCallback;

    private boolean mShowingRequested;

    /** Possible actions taken on the dialog during {@link #hide}. */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({HideActions.NO_OP, HideActions.HIDDEN_IMMEDIATELY, HideActions.HIDING_DELAYED})
    public @interface HideActions {
        /** The dialog has not been shown, so it is not being hidden. */
        int NO_OP = 0;

        /** {@link #mBarrierClosure} was signalled so the dialog is hidden now. */
        int HIDDEN_IMMEDIATELY = 1;

        /** The hiding is being delayed until {@link #mBarrierClosure} is signalled further. */
        int HIDING_DELAYED = 2;
    }

    /** Interface to notify, during @{link #hide}, which action was taken. */
    public interface ActionsConsumer {
        void consume(@HideActions int action);
    }

    /** The callback called everytime {@link #hide} is executed. */
    @Nullable private final ActionsConsumer mActionsConsumer;

    /**
     * Constructs a DialogManager, optionally with a callback to report which action was taken on
     * hiding.
     * @param actionsConsumer The callback called everytime {@link #hide} is executed.
     */
    public DialogManager(@Nullable ActionsConsumer actionsConsumer) {
        mActionsConsumer = actionsConsumer;
    }

    /**
     * Shows the dialog after the specified delay.
     *
     * @param dialog to be shown.
     * @param fragmentManager needed to call {@link android.app.DialogFragment#show}.
     * @param delay the delay in ms after which the dialog will be displayed (if not canceled during
     *         this delay).
     */
    public void showWithDelay(DialogFragment dialog, FragmentManager fragmentManager, int delay) {
        mShowingRequested = true;
        new TimedCallbackDelayer(delay)
                .delay(
                        () -> {
                            // hide() might have been called during the delay.
                            if (mShowingRequested) {
                                show(dialog, fragmentManager);
                            }
                        });
    }

    /**
     * Shows the dialog.
     * @param dialog to be shown.
     * @param fragmentManager needed to call {@link android.app.DialogFragment#show}
     */
    public void show(DialogFragment dialog, FragmentManager fragmentManager) {
        mShowingRequested = true;
        mDialogFragment = dialog;
        mDialogFragment.show(fragmentManager, null);
        // Initiate the barrier closure, expecting 2 runs: one automatic but delayed, and one
        // explicit, to hide the dialog.
        mBarrierClosure = new SingleThreadBarrierClosure(2, this::hideImmediately);
        // This is the automatic but delayed signal.
        mDelayer.delay(mBarrierClosure);
    }

    /**
     * Hides the dialog as soon as possible, but not sooner than {@link MINIMUM_LIFE_SPAN_MILLIS}
     * milliseconds after it was shown. Attempts to hide the dialog when none is shown are
     * gracefully ignored but the callback is called in any case.
     * @param callback is asynchronously called as soon as the dialog is no longer visible.
     */
    public void hide(Runnable callback) {
        if (mActionsConsumer != null) {
            @HideActions final int action;
            if (mBarrierClosure == null) {
                action = HideActions.NO_OP;
            } else if (mBarrierClosure.isReady()) {
                action = HideActions.HIDDEN_IMMEDIATELY;
            } else {
                action = HideActions.HIDING_DELAYED;
            }
            mActionsConsumer.consume(action);
        }
        mCallback = callback;
        // The barrier closure is null if the dialog was not shown. In that case don't wait before
        // confirming the hidden state.
        if (mBarrierClosure == null) {
            hideImmediately();
        } else {
            mBarrierClosure.run();
        }
    }

    /**
     * Synchronously hides the dialog without any delay. Attempts to hide the dialog when
     * none is shown are gracefully ignored but |mCallback| is called in any case if present.
     */
    private void hideImmediately() {
        if (mDialogFragment != null) mDialogFragment.dismiss();
        // Post the callback to ensure that it is always run asynchronously, even if hide() took a
        // shortcut for a missing shown().
        if (mCallback != null) PostTask.postTask(TaskTraits.UI_DEFAULT, mCallback);
        reset();
    }

    /** Resets the dialog reference and metadata related to it.*/
    private void reset() {
        mDialogFragment = null;
        mCallback = null;
        mBarrierClosure = null;
        mShowingRequested = false;
    }
}