chromium/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/ActionConfirmationManager.java

// Copyright 2024 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.tasks.tab_management;

import android.content.Context;
import android.content.res.Resources;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.util.Function;

import org.chromium.base.Callback;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
import org.chromium.chrome.browser.sync.SyncServiceFactory;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.browser.tasks.tab_management.ActionConfirmationDialog.ConfirmationDialogResult;
import org.chromium.chrome.tab_ui.R;
import org.chromium.components.prefs.PrefService;
import org.chromium.components.signin.base.CoreAccountInfo;
import org.chromium.components.signin.identitymanager.ConsentLevel;
import org.chromium.components.signin.identitymanager.IdentityManager;
import org.chromium.components.sync.DataType;
import org.chromium.components.sync.SyncService;
import org.chromium.components.user_prefs.UserPrefs;
import org.chromium.ui.modaldialog.ModalDialogManager;

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

/**
 * Many tab actions can now cause deletion of tab groups. This class helps orchestrates flows where
 * we might want to warn the user that they're about to delete a tab group.
 */
public class ActionConfirmationManager {
    private static final String TAB_GROUP_CONFIRMATION = "TabGroupConfirmation.";
    private static final String DELETE_GROUP_USER_ACTION = TAB_GROUP_CONFIRMATION + "DeleteGroup.";
    private static final String DELETE_SHARED_GROUP_USER_ACTION =
            TAB_GROUP_CONFIRMATION + "DeleteSharedGroup.";
    private static final String UNGROUP_USER_ACTION = TAB_GROUP_CONFIRMATION + "Ungroup.";
    private static final String REMOVE_TAB_USER_ACTION = TAB_GROUP_CONFIRMATION + "RemoveTab.";
    private static final String REMOVE_TAB_FULL_GROUP_USER_ACTION =
            TAB_GROUP_CONFIRMATION + "RemoveTabFullGroup.";
    private static final String CLOSE_TAB_USER_ACTION = TAB_GROUP_CONFIRMATION + "CloseTab.";
    private static final String CLOSE_TAB_FULL_GROUP_USER_ACTION =
            TAB_GROUP_CONFIRMATION + "CloseTabFullGroup.";
    private static final String LEAVE_GROUP_USER_ACTION = TAB_GROUP_CONFIRMATION + "LeaveGroup.";

    // The result of processing an action.
    @IntDef({
        ConfirmationResult.IMMEDIATE_CONTINUE,
        ConfirmationResult.CONFIRMATION_POSITIVE,
        ConfirmationResult.CONFIRMATION_NEGATIVE
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ConfirmationResult {
        // Did not show any confirmation, the action should immediately continue. Resulting action
        // should likely be undoable.
        int IMMEDIATE_CONTINUE = 0;
        // Confirmation was received from the user to continue the action. Do not make resulting
        // action undoable.
        int CONFIRMATION_POSITIVE = 1;
        // The user wants to cancel the action.
        int CONFIRMATION_NEGATIVE = 2;
    }

    private final Profile mProfile;
    private final Context mContext;
    private final TabGroupModelFilter mTabGroupModelFilter;
    private final ModalDialogManager mModalDialogManager;

    /**
     * @param profile The profile to access shared services with.
     * @param context Used to load android resources.
     * @param tabGroupModelFilter Used to read tab data.
     * @param modalDialogManager Used to show dialogs.
     */
    public ActionConfirmationManager(
            Profile profile,
            Context context,
            TabGroupModelFilter tabGroupModelFilter,
            @NonNull ModalDialogManager modalDialogManager) {
        assert modalDialogManager != null;
        mProfile = profile;
        mContext = context;
        mTabGroupModelFilter = tabGroupModelFilter;
        mModalDialogManager = modalDialogManager;
    }

    /**
     * A close group is an operation on tab group(s), and while it may contain non-grouped tabs, it
     * is not an action on individual tabs within a group.
     */
    public void processDeleteGroupAttempt(Callback<Integer> onResult) {
        processMaybeSyncAndPrefAction(
                DELETE_GROUP_USER_ACTION,
                Pref.STOP_SHOWING_TAB_GROUP_CONFIRMATION_ON_CLOSE,
                R.string.delete_tab_group_dialog_title,
                R.string.delete_tab_group_description,
                R.string.delete_tab_group_no_sync_description,
                R.string.delete_tab_group_action,
                onResult);
    }

    /** Processes deleting a shared group, the user should be the owner. */
    public void processDeleteSharedGroupAttempt(String groupTitle, Callback<Integer> onResult) {
        processGroupNameAction(
                DELETE_SHARED_GROUP_USER_ACTION,
                R.string.delete_tab_group_dialog_title,
                R.string.delete_shared_tab_group_description,
                groupTitle,
                R.string.delete_tab_group_menu_item,
                onResult);
    }

    /** Ungroup is an action taken on tab groups that ungroups every tab within them. */
    public void processUngroupAttempt(Callback<Integer> onResult) {
        processMaybeSyncAndPrefAction(
                UNGROUP_USER_ACTION,
                Pref.STOP_SHOWING_TAB_GROUP_CONFIRMATION_ON_UNGROUP,
                R.string.ungroup_tab_group_dialog_title,
                R.string.ungroup_tab_group_description,
                R.string.ungroup_tab_group_no_sync_description,
                R.string.ungroup_tab_group_action,
                onResult);
    }

    /**
     * Removing tabs either moving to no group or to a different group. The caller needs to ensure
     * this action will delete the group.
     */
    public void processRemoveTabAttempt(Callback<Integer> onResult) {
        processMaybeSyncAndPrefAction(
                REMOVE_TAB_USER_ACTION,
                Pref.STOP_SHOWING_TAB_GROUP_CONFIRMATION_ON_TAB_REMOVE,
                R.string.remove_from_group_dialog_message,
                R.string.remove_from_group_description,
                R.string.delete_tab_group_no_sync_description,
                R.string.delete_tab_group_action,
                onResult);
    }

    /**
     * Removing tabs is ungrouping through the dialog bottom bar, selecting tabs and ungrouping, or
     * by dragging out of the strip. The list of tabs should all be in the same group.
     */
    public void processRemoveTabAttempt(List<Integer> tabIdList, Callback<Integer> onResult) {
        if (isFullGroup(tabIdList)) {
            processMaybeSyncAndPrefAction(
                    REMOVE_TAB_FULL_GROUP_USER_ACTION,
                    Pref.STOP_SHOWING_TAB_GROUP_CONFIRMATION_ON_TAB_REMOVE,
                    R.string.remove_from_group_dialog_message,
                    R.string.remove_from_group_description,
                    R.string.delete_tab_group_no_sync_description,
                    R.string.delete_tab_group_action,
                    onResult);
        } else {
            onResult.onResult(ConfirmationResult.IMMEDIATE_CONTINUE);
        }
    }

    /**
     * This processes closing tabs within groups. The caller needs to ensure this action will delete
     * the group.
     */
    public void processCloseTabAttempt(Callback<Integer> onResult) {
        processMaybeSyncAndPrefAction(
                CLOSE_TAB_USER_ACTION,
                Pref.STOP_SHOWING_TAB_GROUP_CONFIRMATION_ON_TAB_CLOSE,
                R.string.close_from_group_dialog_title,
                R.string.close_from_group_description,
                R.string.delete_tab_group_no_sync_description,
                R.string.delete_tab_group_action,
                onResult);
    }

    /**
     * This processes closing tabs within groups. Warns when the last tab(s) are being closed. The
     * list of tabs should all be in the same group.
     */
    public void processCloseTabAttempt(List<Integer> tabIdList, Callback<Integer> onResult) {
        if (isFullGroup(tabIdList)) {
            processMaybeSyncAndPrefAction(
                    CLOSE_TAB_FULL_GROUP_USER_ACTION,
                    Pref.STOP_SHOWING_TAB_GROUP_CONFIRMATION_ON_TAB_CLOSE,
                    R.string.close_from_group_dialog_title,
                    R.string.close_from_group_description,
                    R.string.delete_tab_group_no_sync_description,
                    R.string.delete_tab_group_action,
                    onResult);
        } else {
            onResult.onResult(ConfirmationResult.IMMEDIATE_CONTINUE);
        }
    }

    /** Processing leaving a shared group. */
    public void processLeaveGroupAttempt(String groupTitle, Callback<Integer> onResult) {
        processGroupNameAction(
                LEAVE_GROUP_USER_ACTION,
                R.string.leave_tab_group_dialog_title,
                R.string.leave_tab_group_description,
                groupTitle,
                R.string.leave_tab_group_menu_item,
                onResult);
    }

    private boolean isFullGroup(List<Integer> tabIdList) {
        assert mTabGroupModelFilter != null : "TabGroupModelFilter has not been set";
        return tabIdList.size() >= mTabGroupModelFilter.getRelatedTabList(tabIdList.get(0)).size();
    }

    private void processMaybeSyncAndPrefAction(
            String userActionBaseString,
            @Nullable String stopShowingPref,
            @StringRes int titleRes,
            @StringRes int withSyncDescriptionRes,
            @StringRes int noSyncDescriptionRes,
            @StringRes int actionRes,
            Callback<Integer> onResult) {

        boolean syncingTabGroups = false;
        @Nullable SyncService syncService = SyncServiceFactory.getForProfile(mProfile);
        if (syncService != null) {
            syncingTabGroups = syncService.getActiveDataTypes().contains(DataType.SAVED_TAB_GROUP);
        }

        @Nullable CoreAccountInfo coreAccountInfo = getCoreAccountInfo();
        final Function<Resources, String> titleResolver = (res) -> res.getString(titleRes);
        final Function<Resources, String> descriptionResolver;
        if (syncingTabGroups && coreAccountInfo != null) {
            descriptionResolver =
                    (resources ->
                            resources.getString(
                                    withSyncDescriptionRes, coreAccountInfo.getEmail()));
        } else {
            descriptionResolver = (resources -> resources.getString(noSyncDescriptionRes));
        }

        PrefService prefService = UserPrefs.get(mProfile);
        if (prefService.getBoolean(stopShowingPref)) {
            onResult.onResult(ConfirmationResult.IMMEDIATE_CONTINUE);
            return;
        }

        ConfirmationDialogResult onDialogResult =
                (takePositiveAction, resultStopShowing) -> {
                    if (resultStopShowing) {
                        RecordUserAction.record(userActionBaseString + "StopShowing");
                        prefService.setBoolean(stopShowingPref, true);
                    }
                    handleDialogResult(takePositiveAction, userActionBaseString, onResult);
                };
        ActionConfirmationDialog dialog =
                new ActionConfirmationDialog(mContext, mModalDialogManager);
        dialog.show(
                titleResolver,
                descriptionResolver,
                actionRes,
                /* supportStopShowing= */ true,
                onDialogResult);
    }

    private @Nullable CoreAccountInfo getCoreAccountInfo() {
        IdentityServicesProvider identityServicesProvider = IdentityServicesProvider.get();
        @Nullable
        IdentityManager identityManager = identityServicesProvider.getIdentityManager(mProfile);
        if (identityManager != null) {
            return identityManager.getPrimaryAccountInfo(ConsentLevel.SIGNIN);
        } else {
            return null;
        }
    }

    private void processGroupNameAction(
            String userActionBaseString,
            @StringRes int titleRes,
            @StringRes int descriptionRes,
            String formatArg,
            @StringRes int actionRes,
            Callback<Integer> onResult) {
        final Function<Resources, String> titleResolver = (res) -> res.getString(titleRes);
        final Function<Resources, String> descriptionResolver =
                (resources -> resources.getString(descriptionRes, formatArg));
        ConfirmationDialogResult onDialogResult =
                (takePositiveAction, resultStopShowing) ->
                        handleDialogResult(takePositiveAction, userActionBaseString, onResult);
        ActionConfirmationDialog dialog =
                new ActionConfirmationDialog(mContext, mModalDialogManager);
        dialog.show(
                titleResolver,
                descriptionResolver,
                actionRes,
                /* supportStopShowing= */ false,
                onDialogResult);
    }

    private void handleDialogResult(
            boolean takePositiveAction, String userActionBaseString, Callback<Integer> onResult) {
        RecordUserAction.record(userActionBaseString + (takePositiveAction ? "Proceed" : "Abort"));
        onResult.onResult(
                takePositiveAction
                        ? ConfirmationResult.CONFIRMATION_POSITIVE
                        : ConfirmationResult.CONFIRMATION_NEGATIVE);
    }

    public static void clearStopShowingPrefsForTesting(PrefService prefService) {
        prefService.clearPref(Pref.STOP_SHOWING_TAB_GROUP_CONFIRMATION_ON_CLOSE);
        prefService.clearPref(Pref.STOP_SHOWING_TAB_GROUP_CONFIRMATION_ON_UNGROUP);
        prefService.clearPref(Pref.STOP_SHOWING_TAB_GROUP_CONFIRMATION_ON_TAB_REMOVE);
        prefService.clearPref(Pref.STOP_SHOWING_TAB_GROUP_CONFIRMATION_ON_TAB_CLOSE);
    }

    public static void setAllStopShowingPrefsForTesting(PrefService prefService) {
        prefService.setBoolean(Pref.STOP_SHOWING_TAB_GROUP_CONFIRMATION_ON_CLOSE, true);
        prefService.setBoolean(Pref.STOP_SHOWING_TAB_GROUP_CONFIRMATION_ON_UNGROUP, true);
        prefService.setBoolean(Pref.STOP_SHOWING_TAB_GROUP_CONFIRMATION_ON_TAB_REMOVE, true);
        prefService.setBoolean(Pref.STOP_SHOWING_TAB_GROUP_CONFIRMATION_ON_TAB_CLOSE, true);
    }
}