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

import static org.chromium.chrome.browser.keyboard_accessory.button_group_component.KeyboardAccessoryButtonGroupProperties.ACTIVE_TAB;
import static org.chromium.chrome.browser.keyboard_accessory.button_group_component.KeyboardAccessoryButtonGroupProperties.BUTTON_SELECTION_CALLBACKS;
import static org.chromium.chrome.browser.keyboard_accessory.button_group_component.KeyboardAccessoryButtonGroupProperties.TABS;

import android.view.View;

import androidx.viewpager.widget.ViewPager;

import org.chromium.chrome.browser.keyboard_accessory.bar_component.KeyboardAccessoryCoordinator;
import org.chromium.ui.modelutil.ListModel;
import org.chromium.ui.modelutil.ListModelChangeProcessor;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;

import java.util.HashMap;

/**
 * This component reflects the state of selected tabs in the keyboard accessory. It can be assigned
 * to multiple {@link KeyboardAccessoryButtonGroupView}s and will keep them in sync.
 */
public class KeyboardAccessoryButtonGroupCoordinator {
    private final PropertyModel mModel =
            new PropertyModel.Builder(TABS, ACTIVE_TAB, BUTTON_SELECTION_CALLBACKS)
                    .with(TABS, new ListModel<>())
                    .with(ACTIVE_TAB, null)
                    .build();
    private final KeyboardAccessoryButtonGroupMediator mMediator;

    private final HashMap<View, TemporarySheetOpenerBindings> mBindings = new HashMap<>();

    private final SheetOpenerCallbacks mSheetOpenerCallbacks =
            new SheetOpenerCallbacks() {
                @Override
                public void onViewBound(View view) {
                    if (!mBindings.containsKey(view)) {
                        mBindings.put(view, new TemporarySheetOpenerBindings(view));
                    }
                }

                @Override
                public void onViewUnbound(View view) {
                    TemporarySheetOpenerBindings binding = mBindings.remove(view);
                    if (binding != null) binding.destroy();
                }
            };

    /** This observer gets notified when a tab get selected, reselected or when any tab changes. */
    public interface AccessoryTabObserver {
        /**
         * Called when the active tab changes.
         * @param activeTab The index of the active tab.
         */
        void onActiveTabChanged(Integer activeTab);
    }

    /**
     * These callbacks are triggered when the view corresponding to the sheet opener buttons or tabs
     * is bound or unbound which provides opportunity to initialize and clean up.
     */
    public interface SheetOpenerCallbacks {
        /**
         * Called when the this item is bound to a view. It's useful for setting up MCPs that
         * need the view for initialization.
         * @param sheetOpenerView The {@link View} representing this item.
         */
        void onViewBound(View sheetOpenerView);

        /**
         * Called right before the view currently representing this item gets recycled.
         * It's useful for cleaning up Adapters and MCPs.
         * @param tabs The {@link View} representing this item.
         */
        void onViewUnbound(View sheetOpenerView);
    }

    private class TemporarySheetOpenerBindings {
        private PropertyModelChangeProcessor mMcp;
        private ViewPager.OnPageChangeListener mOnPageChangeListener;

        TemporarySheetOpenerBindings(View view) {
            mMcp =
                    PropertyModelChangeProcessor.create(
                            mModel,
                            (KeyboardAccessoryButtonGroupView) view,
                            KeyboardAccessoryButtonGroupViewBinder::bind);
            mOnPageChangeListener = new ViewPager.SimpleOnPageChangeListener();
            mMediator.addPageChangeListener(mOnPageChangeListener);
        }

        void destroy() {
            mMediator.removePageChangeListener(mOnPageChangeListener);
            mMcp.destroy();
            mOnPageChangeListener = null;
        }
    }

    /**
     * Creates the {@link KeyboardAccessoryButtonGroupViewBinder} that is linked to the
     * {@link ListModelChangeProcessor} that connects the given
     * {@link KeyboardAccessoryButtonGroupView} to the given tab list.
     * @param model the {@link PropertyModel} with {@link KeyboardAccessoryButtonGroupProperties}.
     * @param inflatedView the {@link KeyboardAccessoryButtonGroupView}.
     * @return Returns a fully initialized and wired {@link KeyboardAccessoryButtonGroupViewBinder}.
     */
    static KeyboardAccessoryButtonGroupViewBinder createButtonGroupViewBinder(
            PropertyModel model, KeyboardAccessoryButtonGroupView inflatedView) {
        KeyboardAccessoryButtonGroupViewBinder buttonGroupViewBinder =
                new KeyboardAccessoryButtonGroupViewBinder();
        model.get(TABS)
                .addObserver(
                        new ListModelChangeProcessor<>(
                                model.get(TABS), inflatedView, buttonGroupViewBinder));
        return buttonGroupViewBinder;
    }

    /** Creates a new Tab Layout component that isn't assigned to any view yet. */
    public KeyboardAccessoryButtonGroupCoordinator() {
        mMediator = new KeyboardAccessoryButtonGroupMediator(mModel);
    }

    public SheetOpenerCallbacks getSheetOpenerCallbacks() {
        return mSheetOpenerCallbacks;
    }

    /**
     * Returns a delegate that executes on several tab-related actions.
     * @return A {@link KeyboardAccessoryCoordinator.TabSwitchingDelegate}.
     */
    public KeyboardAccessoryCoordinator.TabSwitchingDelegate getTabSwitchingDelegate() {
        return mMediator;
    }

    /**
     * Adds a {@link AccessoryTabObserver} that is notified about events emitted when a tab changes.
     * @param accessoryTabObserver The component to be notified of tab changes.
     */
    public void setTabObserver(AccessoryTabObserver accessoryTabObserver) {
        mMediator.setTabObserver(accessoryTabObserver);
    }

    /**
     * Returns an OnPageChangeListener that remains the same even if the assigned views changes.
     * This is useful if multiple views are bound to this component or if the view may temporarily
     * be destroyed (like in a RecyclerView).
     * @return A stable {@link ViewPager.OnPageChangeListener}.
     */
    public ViewPager.OnPageChangeListener getStablePageChangeListener() {
        return mMediator.getStableOnPageChangeListener();
    }

    PropertyModel getModelForTesting() {
        return mModel;
    }

    KeyboardAccessoryButtonGroupMediator getMediatorForTesting() {
        return mMediator;
    }
}