chromium/chrome/android/features/keyboard_accessory/internal/java/src/org/chromium/chrome/browser/keyboard_accessory/bar_component/KeyboardAccessoryCoordinator.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.keyboard_accessory.bar_component;

import static org.chromium.chrome.browser.autofill.AutofillUiUtils.getCardIcon;
import static org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryProperties.SKIP_CLOSING_ANIMATION;
import static org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryProperties.VISIBLE;

import android.content.Context;

import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.VisibleForTesting;
import androidx.viewpager.widget.ViewPager;

import org.chromium.base.TraceEvent;
import org.chromium.chrome.browser.autofill.AutofillUiUtils;
import org.chromium.chrome.browser.autofill.PersonalDataManager;
import org.chromium.chrome.browser.autofill.PersonalDataManagerFactory;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.keyboard_accessory.AccessoryTabType;
import org.chromium.chrome.browser.keyboard_accessory.R;
import org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryProperties.BarItem;
import org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryViewBinder.BarItemViewHolder;
import org.chromium.chrome.browser.keyboard_accessory.button_group_component.KeyboardAccessoryButtonGroupCoordinator;
import org.chromium.chrome.browser.keyboard_accessory.data.KeyboardAccessoryData;
import org.chromium.chrome.browser.keyboard_accessory.data.Provider;
import org.chromium.chrome.browser.keyboard_accessory.sheet_component.AccessorySheetCoordinator;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.components.autofill.AutofillDelegate;
import org.chromium.components.autofill.AutofillSuggestion;
import org.chromium.ui.AsyncViewProvider;
import org.chromium.ui.AsyncViewStub;
import org.chromium.ui.ViewProvider;
import org.chromium.ui.modelutil.LazyConstructionPropertyMcp;
import org.chromium.ui.modelutil.ListModel;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.RecyclerViewAdapter;

import java.util.List;

/**
 * Creates and owns all elements which are part of the keyboard accessory component. It's part of
 * the controller but will mainly forward events (like adding a tab, or showing the accessory) to
 * the {@link KeyboardAccessoryMediator}.
 */
public class KeyboardAccessoryCoordinator {
    private final KeyboardAccessoryMediator mMediator;
    private final KeyboardAccessoryButtonGroupCoordinator mButtonGroup;
    private final PropertyModel mModel;
    private KeyboardAccessoryView mView;

    /**
     * The interface to notify consumers about keyboard accessories visibility. E.g: the animation
     * end. The actual implementation isn't relevant for this component. Therefore, a class
     * implementing this interface takes that responsibility, i.e. ManualFillingCoordinator.
     */
    public interface BarVisibilityDelegate {
        /**
         * Signals that the accessory bar has completed the fade-in. This may be relevant to the
         * keyboard extensions state to adjust the scroll position.
         */
        void onBarFadeInAnimationEnd();
    }

    /**
     * Describes a delegate manages all known tabs and is responsible to determine the active tab.
     */
    public interface TabSwitchingDelegate {
        /**
         * A {@link KeyboardAccessoryData.Tab} passed into this function will be represented as item
         * at the start of the tab layout. It is meant to trigger various bottom sheets.
         * @param tab The tab which contains representation data of a bottom sheet.
         */
        void addTab(KeyboardAccessoryData.Tab tab);

        /**
         * The {@link KeyboardAccessoryData.Tab} passed into this function will be completely
         * removed from the tab layout.
         * @param tab The tab to be removed.
         */
        void removeTab(KeyboardAccessoryData.Tab tab);

        /**
         * Clears all currently known tabs and adds the given tabs as replacement.
         * @param tabs An array of {@link KeyboardAccessoryData.Tab}s.
         */
        void setTabs(KeyboardAccessoryData.Tab[] tabs);

        /** Closes any active tab so that {@link #getActiveTab} returns null again. */
        void closeActiveTab();

        /**
         * Set the currently active tab to the given tabType.
         * @param tabType The type of the tab that should be selected.
         */
        void setActiveTab(@AccessoryTabType int tabType);

        /**
         * Returns whether active tab or null if no tab is currently active. The returned property
         * reflects the latest change while the view might still be in progress of being updated.
         * @return The active {@link KeyboardAccessoryData.Tab}, null otherwise.
         */
        @Nullable
        KeyboardAccessoryData.Tab getActiveTab();

        /**
         * Returns whether the model holds any tabs.
         * @return True if there is at least one tab, false otherwise.
         */
        boolean hasTabs();
    }

    /**
     * Initializes the component as soon as the native library is loaded by e.g. starting to listen
     * to keyboard visibility events.
     *
     * @param profile The {@link Profile} associated with the data.
     * @param barVisibilityDelegate A {@link BarVisibilityDelegate} for delegating the bar
     *     visibility changes.
     * @param sheetVisibilityDelegate A {@link AccessorySheetCoordinator.SheetVisibilityDelegate}
     *     for delegating the sheet visibility changes.
     * @param barStub A {@link AsyncViewStub} for the accessory bar layout.
     */
    public KeyboardAccessoryCoordinator(
            Profile profile,
            BarVisibilityDelegate barVisibilityDelegate,
            AccessorySheetCoordinator.SheetVisibilityDelegate sheetVisibilityDelegate,
            AsyncViewStub barStub) {
        this(
                barStub.getContext(),
                profile,
                new KeyboardAccessoryButtonGroupCoordinator(),
                barVisibilityDelegate,
                sheetVisibilityDelegate,
                AsyncViewProvider.of(barStub, R.id.keyboard_accessory));
    }

    /**
     * Constructor that allows to mock the {@link AsyncViewProvider}.
     *
     * @param context The {@link Context} associated with the current UI context.
     * @param profile The {@link Profile} associated with the data.
     * @param viewProvider A provider for the accessory.
     */
    @VisibleForTesting
    public KeyboardAccessoryCoordinator(
            Context context,
            Profile profile,
            KeyboardAccessoryButtonGroupCoordinator buttonGroup,
            BarVisibilityDelegate barVisibilityDelegate,
            AccessorySheetCoordinator.SheetVisibilityDelegate sheetVisibilityDelegate,
            ViewProvider<KeyboardAccessoryView> viewProvider) {
        mButtonGroup = buttonGroup;
        mModel = KeyboardAccessoryProperties.defaultModelBuilder().build();

        mMediator =
                new KeyboardAccessoryMediator(
                        mModel,
                        barVisibilityDelegate,
                        sheetVisibilityDelegate,
                        mButtonGroup.getTabSwitchingDelegate(),
                        mButtonGroup.getSheetOpenerCallbacks());
        viewProvider.whenLoaded(
                view -> {
                    mView = view;
                    mView.setBarItemsAdapter(
                            createBarItemsAdapter(
                                    mModel.get(KeyboardAccessoryProperties.BAR_ITEMS),
                                    mView,
                                    createUiConfiguration(
                                            context,
                                            PersonalDataManagerFactory.getForProfile(profile))));
                    mView.setFeatureEngagementTracker(TrackerFactory.getTrackerForProfile(profile));
                });

        mButtonGroup.setTabObserver(mMediator);
        LazyConstructionPropertyMcp.create(
                mModel, VISIBLE, viewProvider, KeyboardAccessoryViewBinder::bind);
        KeyboardAccessoryMetricsRecorder.registerKeyboardAccessoryModelMetricsObserver(mModel);
    }

    @VisibleForTesting
    static KeyboardAccessoryViewBinder.UiConfiguration createUiConfiguration(
            Context context, PersonalDataManager personalDataManager) {
        KeyboardAccessoryViewBinder.UiConfiguration uiConfiguration =
                new KeyboardAccessoryViewBinder.UiConfiguration();
        uiConfiguration.suggestionDrawableFunction =
                (suggestion) ->
                        getCardIcon(
                                context,
                                personalDataManager,
                                suggestion.getCustomIconUrl(),
                                suggestion.getIconId(),
                                AutofillUiUtils.CardIconSize.SMALL,
                                /* showCustomIcon= */ true);
        return uiConfiguration;
    }

    /**
     * Creates an adapter to an {@link BarItemViewHolder} that is wired up to the model change
     * processor which listens to the given item list.
     *
     * @param barItems The list of shown items represented by the adapter.
     * @param view The keyboard accessory view that will display the bar items.
     * @return Returns a fully initialized and wired adapter to an BarItemViewHolder.
     */
    @VisibleForTesting
    static RecyclerViewAdapter<BarItemViewHolder, Void> createBarItemsAdapter(
            ListModel<BarItem> barItems,
            KeyboardAccessoryView view,
            KeyboardAccessoryViewBinder.UiConfiguration uiConfiguration) {
        return new RecyclerViewAdapter<>(
                new KeyboardAccessoryRecyclerViewMcp<>(
                        barItems,
                        BarItem::getViewType,
                        BarItemViewHolder::bind,
                        BarItemViewHolder::recycle),
                (parent, viewType) ->
                        KeyboardAccessoryViewBinder.create(
                                view, uiConfiguration, parent, viewType));
    }

    public void closeActiveTab() {
        mButtonGroup.getTabSwitchingDelegate().closeActiveTab();
    }

    public void setTabs(KeyboardAccessoryData.Tab[] tabs) {
        mButtonGroup.getTabSwitchingDelegate().setTabs(tabs);
    }

    public void setActiveTab(@AccessoryTabType int tabType) {
        mButtonGroup.getTabSwitchingDelegate().setActiveTab(tabType);
    }

    /**
     * Allows any {@link Provider} to communicate with the
     * {@link KeyboardAccessoryMediator} of this component.
     *
     * Note that the provided actions are removed when the accessory is hidden.
     *
     * @param provider The object providing action lists to observers in this component.
     */
    public void registerActionProvider(Provider<KeyboardAccessoryData.Action[]> provider) {
        provider.addObserver(mMediator);
    }

    /**
     * Registers a Provider.Observer to the given Provider. The new observer will render chips into
     * the accessory bar for every new suggestion and call the given {@link AutofillDelegate} when
     * the user interacts with a chip.
     *
     * @param provider A {@link Provider<List<AutofillSuggestion>>}.
     * @param delegate A {@link AutofillDelegate}.
     */
    public void registerAutofillProvider(
            Provider<List<AutofillSuggestion>> provider, AutofillDelegate delegate) {
        provider.addObserver(mMediator.createAutofillSuggestionsObserver(delegate));
    }

    /**
     * Dismisses the accessory by hiding it's view, clearing potentially left over suggestions and
     * hiding the keyboard.
     */
    public void dismiss() {
        mMediator.dismiss();
    }

    /**
     * Sets the offset to the end of the activity - which is usually 0, the height of the keyboard
     * or the height of a bottom sheet.
     * @param bottomOffset The offset in pixels.
     */
    public void setBottomOffset(@Px int bottomOffset) {
        mMediator.setBottomOffset(bottomOffset);
    }

    /** Triggers the accessory to be shown. */
    public void show() {
        TraceEvent.begin("KeyboardAccessoryCoordinator#show");
        mMediator.show();
        TraceEvent.end("KeyboardAccessoryCoordinator#show");
    }

    /** Next time the accessory is closed, don't delay the closing animation. */
    public void skipClosingAnimationOnce() {
        mMediator.skipClosingAnimationOnce();
        // TODO(fhorschig): Consider allow LazyConstructionPropertyMcp to propagate updates once the
        // view exists. Currently it doesn't, so we need this ugly explicit binding.
        if (mView != null) {
            KeyboardAccessoryViewBinder.bind(mModel, mView, SKIP_CLOSING_ANIMATION);
        }
    }

    /**
     * Returns the visibility of the the accessory. The returned property reflects the latest change
     * while the view might still be in progress of being updated accordingly.
     *
     * @return True if the accessory should be visible, false otherwise.
     */
    // TODO(crbug.com/40879203): Hide because it's only used in tests.
    public boolean isShown() {
        return mMediator.isShown();
    }

    /**
     * This method returns whether the accessory has any contents that justify showing it. A single
     * tab, action or suggestion chip would already mean it is not empty.
     * @return False if there is any content to be shown. True otherwise.
     */
    public boolean empty() {
        return mMediator.empty();
    }

    /**
     * Returns whether the active tab is non-null. The returned property reflects the latest change
     * while the view might still be in progress of being updated accordingly.
     * @return True if the accessory is visible and has an active tab, false otherwise.
     */
    public boolean hasActiveTab() {
        return mMediator.hasActiveTab();
    }

    public ViewPager.OnPageChangeListener getOnPageChangeListener() {
        return mButtonGroup.getStablePageChangeListener();
    }

    public KeyboardAccessoryMediator getMediatorForTesting() {
        return mMediator;
    }
}