chromium/ui/android/java/src/org/chromium/ui/modaldialog/PendingDialogContainer.java

// Copyright 2022 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.ui.modaldialog;

import androidx.annotation.Nullable;

import org.chromium.ui.modaldialog.ModalDialogManager.ModalDialogPriority;
import org.chromium.ui.modaldialog.ModalDialogManager.ModalDialogType;
import org.chromium.ui.modelutil.PropertyModel;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

/**
 * A container class to provide basic operations for pending dialogs with attributes {@link
 * ModalDialogType} and {@link ModalDialogPriority}.
 */
class PendingDialogContainer {
    /** A class representing the attributes of a pending dialog. */
    class PendingDialogType {
        public final PropertyModel propertyModel;
        public final @ModalDialogType int dialogType;
        public final @ModalDialogPriority int dialogPriority;

        PendingDialogType(
                PropertyModel model, @ModalDialogType int type, @ModalDialogPriority int priority) {
            propertyModel = model;
            dialogType = type;
            dialogPriority = priority;
        }
    }

    /**
     * Mapping of the lists of pending dialogs based on {@link ModalDialogType} and {@link
     * ModalDialogPriority} of the dialogs.
     *
     * The key of this map is 10 * {@link ModalDialogType} + {@link ModalDialogPriority}.
     * This allows for efficient retrievals of pending dialogs sliced on {@link ModalDialogType}
     * or {@link ModalDialogType}.
     *
     * Iterating over pending dialogs based on {@link ModalDialogType} is useful when suspending
     * dialogs for certain types.
     *
     * Iterating over pending dialogs based on {@link ModalDialogPriority} is useful when
     * choosing the next dialog to be shown.
     *
     * Please note, this only works if the MAX_RANGE of {@link ModalDialogPriority} is <= 9,
     * otherwise we can have situations where two different dialogs on either {@link
     * ModalDialogType} or {@link ModalDialogPriority} can be mapped to the same key.
     *
     * For example:
     * 10 * (ModalDialogType: 2) + (ModalDialogPriority: 9) = 10 * (ModalDialogType: 1) +
     * (ModalDialogPriority: 19) would map to the same key 29.
     */
    private final HashMap<Integer, List<PropertyModel>> mPendingDialogs = new HashMap<>();

    /**
     * Queues the {@link PropertyModel} model in the container based on the {@link
     * ModalDialogPriority} and {@link ModalDialogType}.
     *
     * @param dialogType The {@link ModalDialogType} of the {@link PropertyModel}.
     * @param dialogPriority The {@link ModalDialogPriority} of the {@link PropertyModel}.
     * @param model The {@link PropertyModel} which contains the {@link ModalDialogProperties} to
     *     launch a dialog.
     * @param showAsNext If set to true, the {@link PropertyModel} |model| would be put as first in
     *     its list of dialog.
     */
    void put(
            @ModalDialogType int dialogType,
            @ModalDialogPriority int dialogPriority,
            PropertyModel model,
            boolean showAsNext) {
        Integer key = computeKey(dialogType, dialogPriority);
        List<PropertyModel> dialogs = mPendingDialogs.get(key);
        if (dialogs == null) mPendingDialogs.put(key, dialogs = new ArrayList<>());
        dialogs.add(showAsNext ? 0 : dialogs.size(), model);
    }

    /**
     * @param dialogType The {@link ModalDialogType} of the list of pending dialog to fetch.
     * @param dialogPriority The {@link ModalDialogPriority} of the list of pending dialog to fetch.
     *
     * @return Returns the list of {@link PropertyModel} with matching {@link ModalDialogType} *and*
     *         {@link ModalDialogPriority}, null otherwise.
     */
    @Nullable
    List<PropertyModel> get(
            @ModalDialogType int dialogType, @ModalDialogPriority int dialogPriority) {
        Integer key = computeKey(dialogType, dialogPriority);
        return mPendingDialogs.get(key);
    }

    /**
     * @param model The {@link PropertyModel} model to check if it contains in the list of
     *         pending dialogs.
     *
     * @return Returns true if model found, otherwise false.
     */
    boolean contains(PropertyModel model) {
        for (Map.Entry<Integer, List<PropertyModel>> entry : mPendingDialogs.entrySet()) {
            List<PropertyModel> dialogs = entry.getValue();
            for (int i = 0; i < dialogs.size(); ++i) {
                if (dialogs.get(i) == model) return true;
            }
        }
        return false;
    }

    /**
     * @return True, if there are no pending dialogs, false otherwise.
     */
    boolean isEmpty() {
        for (Map.Entry<Integer, List<PropertyModel>> entry : mPendingDialogs.entrySet()) {
            if (!entry.getValue().isEmpty()) return false;
        }
        return true;
    }

    /**
     * @return Total number of list of pending dialogs.
     */
    int size() {
        return mPendingDialogs.size();
    }

    /**
     * @param model The {@link PropertyModel} model to remove from pending dialogs.
     *
     * @return True, if the |model| was found and removed, false otherwise.
     */
    boolean remove(PropertyModel model) {
        Iterator<Map.Entry<Integer, List<PropertyModel>>> iterator =
                mPendingDialogs.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Integer, List<PropertyModel>> entry = iterator.next();
            List<PropertyModel> dialogs = entry.getValue();
            for (int i = 0; i < dialogs.size(); ++i) {
                if (dialogs.get(i) == model) {
                    dialogs.remove(i);
                    if (dialogs.isEmpty()) {
                        mPendingDialogs.remove(entry.getKey());
                    }
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Removes the pending dialogs matching the |dialogType| and applies the operation provided by
     * |consumer| before removing.
     *
     * @param dialogType The {@link ModalDialogType} of the dialog to be removed from the list
     *         of pending dialogs.
     * @param consumer The {@link Consumer <PropertyModel>} which would be performed before
     *         removing the {@link PropertyModel} matching the |dialogType| from the pending
     *         dialog list.
     *
     * @return True, if any dialogs were removed, false otherwise.
     */
    boolean remove(@ModalDialogType int dialogType, Consumer<PropertyModel> consumer) {
        boolean dialogRemoved = false;
        for (@ModalDialogPriority int priority = ModalDialogPriority.RANGE_MIN;
                priority <= ModalDialogPriority.RANGE_MAX;
                ++priority) {
            Integer key = computeKey(dialogType, priority);
            List<PropertyModel> dialogs = mPendingDialogs.get(key);
            // No matching dialogs of type |dialogType| found with |priority|, continue searching
            // with other priorities.
            if (dialogs == null) continue;

            for (int i = 0; i < dialogs.size(); ++i) {
                dialogRemoved = true;
                consumer.accept(dialogs.get(i));
            }
            mPendingDialogs.remove(key);
        }
        return dialogRemoved;
    }

    /**
     * Returns the next dialog whose {@link ModalDialogType} is not suspended from showing and also
     * removes it from the pending dialogs.
     *
     * @return The {@link PropertyModel}, the {@link ModalDialogType} and the {@link
     *         ModalDialogPriority} of the model associated with the next dialog to be shown,
     *         otherwise null if not found.
     */
    @Nullable
    PendingDialogType getNextPendingDialog(Set<Integer> suspendedTypes) {
        for (@ModalDialogPriority int priority = ModalDialogPriority.RANGE_MAX;
                priority >= ModalDialogPriority.RANGE_MIN;
                --priority) {
            for (@ModalDialogType int type = ModalDialogType.RANGE_MAX;
                    type >= ModalDialogType.RANGE_MIN;
                    --type) {
                if (suspendedTypes.contains(type)) continue;
                Integer key = computeKey(type, priority);
                List<PropertyModel> dialogs = mPendingDialogs.get(key);
                if (dialogs != null && !dialogs.isEmpty()) {
                    PropertyModel model = dialogs.get(0);
                    dialogs.remove(0);
                    if (dialogs.isEmpty()) {
                        mPendingDialogs.remove(key);
                    }
                    return new PendingDialogType(model, type, priority);
                }
            }
        }
        return null;
    }

    // A method for computing the key of the mPendingDialog HashMap.
    private Integer computeKey(
            @ModalDialogType int dialogType, @ModalDialogPriority int dialogPriority) {
        assert dialogPriority >= 1 && dialogPriority <= 9;
        return dialogType * 10 + dialogPriority;
    }
}