chromium/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogViewBinder.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.tasks.tab_management;

import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.ADD_CLICK_LISTENER;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.ANIMATION_BACKGROUND_COLOR;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.ANIMATION_SOURCE_VIEW;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.BINDING_TOKEN;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.BROWSER_CONTROLS_STATE_PROVIDER;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.COLLAPSE_BUTTON_CONTENT_DESCRIPTION;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.COLLAPSE_CLICK_LISTENER;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.COLOR_ICON_CLICK_LISTENER;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.CONTENT_TOP_MARGIN;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.DIALOG_BACKGROUND_COLOR;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.DIALOG_UNGROUP_BAR_BACKGROUND_COLOR;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.DIALOG_UNGROUP_BAR_HOVERED_BACKGROUND_COLOR;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.DIALOG_UNGROUP_BAR_HOVERED_TEXT_COLOR;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.DIALOG_UNGROUP_BAR_TEXT;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.DIALOG_UNGROUP_BAR_TEXT_COLOR;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.FORCE_ANIMATION_TO_FINISH;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.HAIRLINE_COLOR;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.HAIRLINE_VISIBILITY;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.HEADER_TITLE;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.INITIAL_SCROLL_INDEX;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.IS_DIALOG_VISIBLE;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.IS_INCOGNITO;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.IS_KEYBOARD_VISIBLE;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.IS_MAIN_CONTENT_VISIBLE;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.IS_SHARE_SHEET_VISIBLE;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.IS_TITLE_TEXT_FOCUSED;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.MENU_CLICK_LISTENER;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.PRIMARY_COLOR;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.SCRIMVIEW_CLICK_RUNNABLE;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.SHARE_BUTTON_CLICK_LISTENER;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.SHARE_IMAGE_TILES_CLICK_LISTENER;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.SHOW_IMAGE_TILES;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.SHOW_SHARE_BUTTON;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.TAB_GROUP_COLOR_ID;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.TINT;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.TITLE_CURSOR_VISIBILITY;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.TITLE_TEXT_ON_FOCUS_LISTENER;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.TITLE_TEXT_WATCHER;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.UNGROUP_BAR_STATUS;
import static org.chromium.chrome.browser.tasks.tab_management.TabGridDialogProperties.VISIBILITY_LISTENER;

import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.tab.TabUtils;
import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;

/** ViewBinder for TabGridDialog. */
class TabGridDialogViewBinder {
    /** ViewHolder class to get access to all {@link View}s inside the TabGridDialog. */
    public static class ViewHolder {
        public final TabGridDialogToolbarView toolbarView;
        public final RecyclerView contentView;
        @Nullable public TabGridDialogView dialogView;

        ViewHolder(
                TabGridDialogToolbarView toolbarView,
                RecyclerView contentView,
                @Nullable TabGridDialogView dialogView) {
            this.toolbarView = toolbarView;
            this.contentView = contentView;
            this.dialogView = dialogView;
        }
    }

    /**
     * Binds the given model to the given view, updating the payload in propertyKey.
     * @param model The model to use.
     * @param viewHolder The ViewHolder to use.
     * @param propertyKey The key for the property to update for.
     */
    public static void bind(PropertyModel model, ViewHolder viewHolder, PropertyKey propertyKey) {
        // The TabGridDialogView is effectively a singleton in the UI with multiple Mediators and
        // PropertyModel's attempting to managed it. This BINDING_TOKEN system prevents collisions.
        //
        // Only one PropertyModel should be binding to the DialogView at a time. To enforce this
        // the {@link TabGridDialogMediator} currently using the view must provide its hashCode as
        // a BINDING_TOKEN.
        final Integer bindingToken = model.get(BINDING_TOKEN);
        final Integer oldBindingToken = viewHolder.dialogView.getBindingToken();
        if (BINDING_TOKEN == propertyKey) {
            if (oldBindingToken != null && oldBindingToken.equals(bindingToken)) {
                return;
            }

            viewHolder.dialogView.setBindingToken(bindingToken);
            if (bindingToken == null) return;

            // Re-bind if the binding token ever changes.
            for (PropertyKey key : TabGridDialogProperties.ALL_KEYS) {
                if (BINDING_TOKEN == key) continue;

                bind(model, viewHolder, key);
            }
        } else if (bindingToken == null || oldBindingToken == null) {
            return;
        } else if (!bindingToken.equals(oldBindingToken)) {
            assert false
                    : "Attempting to update a property of TabGridDialog without binding token.";
            return;
        }

        // The null checks in the following blocks are there for if
        // 1) The dialogView is not initialized.
        // 2) ALL_KEYS are being re-bound upon changing BINDING_TOKEN and a value is unset in the
        //    newly bound model.
        if (COLLAPSE_CLICK_LISTENER == propertyKey) {
            viewHolder.toolbarView.setBackButtonOnClickListener(model.get(COLLAPSE_CLICK_LISTENER));
        } else if (ADD_CLICK_LISTENER == propertyKey) {
            viewHolder.toolbarView.setNewTabButtonOnClickListener(model.get(ADD_CLICK_LISTENER));
        } else if (HEADER_TITLE == propertyKey) {
            if (model.get(HEADER_TITLE) != null) {
                viewHolder.toolbarView.setTitle(model.get(HEADER_TITLE));
            }
        } else if (CONTENT_TOP_MARGIN == propertyKey) {
            ViewGroup.LayoutParams params = viewHolder.contentView.getLayoutParams();
            if (params instanceof FrameLayout.LayoutParams) {
                ((FrameLayout.LayoutParams) params).topMargin = model.get(CONTENT_TOP_MARGIN);
                ViewUtils.requestLayout(viewHolder.contentView, "TabGridDialogViewBinder.bind");
            }
        } else if (PRIMARY_COLOR == propertyKey) {
            viewHolder.toolbarView.setContentBackgroundColor(model.get(PRIMARY_COLOR));
            viewHolder.contentView.setBackgroundColor(model.get(PRIMARY_COLOR));
        } else if (TINT == propertyKey) {
            if (model.get(TINT) != null) {
                viewHolder.toolbarView.setTint(model.get(TINT));
            }
        } else if (SCRIMVIEW_CLICK_RUNNABLE == propertyKey) {
            viewHolder.dialogView.setScrimClickRunnable(model.get(SCRIMVIEW_CLICK_RUNNABLE));
        } else if (IS_DIALOG_VISIBLE == propertyKey) {
            if (model.get(IS_DIALOG_VISIBLE)) {
                viewHolder.dialogView.resetDialog(viewHolder.toolbarView, viewHolder.contentView);
                viewHolder.dialogView.showDialog();
            } else {
                viewHolder.dialogView.hideDialog();
            }
        } else if (VISIBILITY_LISTENER == propertyKey) {
            viewHolder.dialogView.setVisibilityListener(model.get(VISIBILITY_LISTENER));
        } else if (ANIMATION_SOURCE_VIEW == propertyKey) {
            viewHolder.dialogView.setupDialogAnimation(model.get(ANIMATION_SOURCE_VIEW));
        } else if (UNGROUP_BAR_STATUS == propertyKey) {
            viewHolder.dialogView.updateUngroupBar(model.get(UNGROUP_BAR_STATUS));
        } else if (DIALOG_BACKGROUND_COLOR == propertyKey) {
            if (viewHolder.dialogView != null) {
                int backgroundColorInt = model.get(DIALOG_BACKGROUND_COLOR);
                viewHolder.dialogView.updateDialogContainerBackgroundColor(backgroundColorInt);
                viewHolder.toolbarView.setBackgroundColorTint(backgroundColorInt);
            }
        } else if (HAIRLINE_COLOR == propertyKey) {
            if (viewHolder.dialogView != null) {
                viewHolder.dialogView.updateHairlineColor(model.get(HAIRLINE_COLOR));
            }
        } else if (HAIRLINE_VISIBILITY == propertyKey) {
            if (viewHolder.dialogView != null) {
                viewHolder.dialogView.setHairlineVisibility(model.get(HAIRLINE_VISIBILITY));
            }
        } else if (DIALOG_UNGROUP_BAR_BACKGROUND_COLOR == propertyKey) {
            if (viewHolder.dialogView != null) {
                viewHolder.dialogView.updateUngroupBarBackgroundColor(
                        model.get(DIALOG_UNGROUP_BAR_BACKGROUND_COLOR));
            }
        } else if (DIALOG_UNGROUP_BAR_HOVERED_BACKGROUND_COLOR == propertyKey) {
            if (viewHolder.dialogView != null) {
                viewHolder.dialogView.updateUngroupBarHoveredBackgroundColor(
                        model.get(DIALOG_UNGROUP_BAR_HOVERED_BACKGROUND_COLOR));
            }
        } else if (DIALOG_UNGROUP_BAR_TEXT_COLOR == propertyKey) {
            if (viewHolder.dialogView != null) {
                viewHolder.dialogView.updateUngroupBarTextColor(
                        model.get(DIALOG_UNGROUP_BAR_TEXT_COLOR));
            }
        } else if (DIALOG_UNGROUP_BAR_HOVERED_TEXT_COLOR == propertyKey) {
            if (viewHolder.dialogView != null) {
                viewHolder.dialogView.updateUngroupBarHoveredTextColor(
                        model.get(DIALOG_UNGROUP_BAR_HOVERED_TEXT_COLOR));
            }
        } else if (DIALOG_UNGROUP_BAR_TEXT == propertyKey) {
            if (viewHolder.dialogView != null) {
                viewHolder.dialogView.updateUngroupBarText(model.get(DIALOG_UNGROUP_BAR_TEXT));
            }
        } else if (INITIAL_SCROLL_INDEX == propertyKey) {
            if (model.get(INITIAL_SCROLL_INDEX) != null) {
                int index = (Integer) model.get(INITIAL_SCROLL_INDEX);
                RecyclerView view = viewHolder.contentView;
                if (view.getWidth() == 0 || view.getHeight() == 0) {
                    // If layout hasn't happened post the scroll index change until layout happens.
                    view.post(
                            () -> {
                                setScrollIndex(
                                        model.get(BROWSER_CONTROLS_STATE_PROVIDER), view, index);
                            });
                    return;
                }
                setScrollIndex(
                        model.get(BROWSER_CONTROLS_STATE_PROVIDER), viewHolder.contentView, index);
            }
        } else if (IS_MAIN_CONTENT_VISIBLE == propertyKey) {
            viewHolder.contentView.setVisibility(View.VISIBLE);
        } else if (MENU_CLICK_LISTENER == propertyKey) {
            viewHolder.toolbarView.setMenuButtonOnClickListener(model.get(MENU_CLICK_LISTENER));
        } else if (TITLE_TEXT_WATCHER == propertyKey) {
            if (model.get(TITLE_TEXT_WATCHER) != null) {
                viewHolder.toolbarView.setTitleTextOnChangedListener(model.get(TITLE_TEXT_WATCHER));
            }
        } else if (TITLE_TEXT_ON_FOCUS_LISTENER == propertyKey) {
            if (model.get(TITLE_TEXT_ON_FOCUS_LISTENER) != null) {
                viewHolder.toolbarView.setTitleTextOnFocusChangeListener(
                        model.get(TITLE_TEXT_ON_FOCUS_LISTENER));
            }
        } else if (TITLE_CURSOR_VISIBILITY == propertyKey) {
            viewHolder.toolbarView.setTitleCursorVisibility(model.get(TITLE_CURSOR_VISIBILITY));
        } else if (IS_TITLE_TEXT_FOCUSED == propertyKey) {
            viewHolder.toolbarView.updateTitleTextFocus(model.get(IS_TITLE_TEXT_FOCUSED));
        } else if (IS_KEYBOARD_VISIBLE == propertyKey) {
            viewHolder.toolbarView.updateKeyboardVisibility(model.get(IS_KEYBOARD_VISIBLE));
        } else if (COLLAPSE_BUTTON_CONTENT_DESCRIPTION == propertyKey) {
            viewHolder.toolbarView.setBackButtonContentDescription(
                    model.get(COLLAPSE_BUTTON_CONTENT_DESCRIPTION));
        } else if (SHARE_BUTTON_CLICK_LISTENER == propertyKey) {
            viewHolder.toolbarView.setShareButtonClickListener(
                    model.get(SHARE_BUTTON_CLICK_LISTENER));
        } else if (SHARE_IMAGE_TILES_CLICK_LISTENER == propertyKey) {
            viewHolder.toolbarView.setImageTilesClickListener(
                    model.get(SHARE_IMAGE_TILES_CLICK_LISTENER));
        } else if (SHOW_SHARE_BUTTON == propertyKey) {
            viewHolder.toolbarView.setShareButtonVisibility(model.get(SHOW_SHARE_BUTTON));
        } else if (SHOW_IMAGE_TILES == propertyKey) {
            viewHolder.toolbarView.setImageTilesVisibility(model.get(SHOW_IMAGE_TILES));
        } else if (TAB_GROUP_COLOR_ID == propertyKey) {
            viewHolder.toolbarView.setColorIconColor(
                    model.get(TAB_GROUP_COLOR_ID), model.get(IS_INCOGNITO));
        } else if (COLOR_ICON_CLICK_LISTENER == propertyKey) {
            viewHolder.toolbarView.setColorIconOnClickListener(
                    model.get(COLOR_ICON_CLICK_LISTENER));
        } else if (IS_SHARE_SHEET_VISIBLE == propertyKey) {
            if (!model.get(IS_SHARE_SHEET_VISIBLE) && model.get(IS_DIALOG_VISIBLE)) {
                // Fit the scrim to the TabGridDialog again after the bottom sheet visibility
                // changes.
                viewHolder.dialogView.refreshScrim();
            }
        } else if (ANIMATION_BACKGROUND_COLOR == propertyKey) {
            // Only set in LIST mode not GRID mode. Will always be set in LIST mode. Mode is not
            // mutable without restarting the app.
            if (model.get(ANIMATION_BACKGROUND_COLOR) != null) {
                viewHolder.dialogView.updateAnimationBackgroundColor(
                        model.get(ANIMATION_BACKGROUND_COLOR));
            }
        } else if (FORCE_ANIMATION_TO_FINISH == propertyKey) {
            if (model.get(FORCE_ANIMATION_TO_FINISH)) {
                viewHolder.dialogView.forceAnimationToFinish();
            }
        }
    }

    private static void setScrollIndex(
            BrowserControlsStateProvider browserControlsStateProvider,
            RecyclerView view,
            int index) {
        LinearLayoutManager layoutManager = (LinearLayoutManager) view.getLayoutManager();
        int offset = computeOffset(view, layoutManager, browserControlsStateProvider);
        layoutManager.scrollToPositionWithOffset(index, offset);
    }

    private static int computeOffset(
            RecyclerView view,
            LinearLayoutManager layoutManager,
            BrowserControlsStateProvider browserControlsStateProvider) {
        int width = view.getWidth();
        int height = view.getHeight();
        int cardHeight = 0;
        if (layoutManager instanceof GridLayoutManager) {
            int cardWidth = width / ((GridLayoutManager) layoutManager).getSpanCount();
            cardHeight =
                    TabUtils.deriveGridCardHeight(
                            cardWidth, view.getContext(), browserControlsStateProvider);
        } else {
            // Avoid divide by 0 when there are no tabs.
            if (layoutManager.getItemCount() == 0) return 0;

            cardHeight = view.computeVerticalScrollRange() / layoutManager.getItemCount();
        }
        return Math.max(0, height / 2 - cardHeight / 2);
    }
}