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

// Copyright 2019 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 android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;

import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.BLOCK_TOUCH_INPUT;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.BOTTOM_PADDING;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.BROWSER_CONTROLS_STATE_PROVIDER;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.FETCH_VIEW_BY_INDEX_CALLBACK;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.FOCUS_TAB_INDEX_FOR_ACCESSIBILITY;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.GET_VISIBLE_RANGE_CALLBACK;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.INITIAL_SCROLL_INDEX;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.IS_SCROLLING_SUPPLIER_CALLBACK;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.MODE;

import android.app.Activity;
import android.graphics.Rect;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.core.util.Function;
import androidx.core.util.Pair;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;

import org.chromium.base.Callback;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.tab.TabUtils;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;

/** ViewBinder for TabListRecyclerView. */
class TabListContainerViewBinder {
    /**
     * Bind the given model to the given view, updating the payload in propertyKey.
     *
     * @param model The model to use.
     * @param view The View to use.
     * @param propertyKey The key for the property to update for.
     */
    public static void bind(
            PropertyModel model, TabListRecyclerView view, PropertyKey propertyKey) {
        if (BLOCK_TOUCH_INPUT == propertyKey) {
            view.setBlockTouchInput(model.get(BLOCK_TOUCH_INPUT));
        } else if (INITIAL_SCROLL_INDEX == propertyKey) {
            int index = model.get(INITIAL_SCROLL_INDEX);
            int offset = computeOffset(view, model);
            // RecyclerView#scrollToPosition(int) behaves incorrectly first time after cold start.
            ((LinearLayoutManager) view.getLayoutManager())
                    .scrollToPositionWithOffset(index, offset);
        } else if (FOCUS_TAB_INDEX_FOR_ACCESSIBILITY == propertyKey) {
            int index = model.get(FOCUS_TAB_INDEX_FOR_ACCESSIBILITY);
            RecyclerView.ViewHolder selectedViewHolder =
                    view.findViewHolderForAdapterPosition(index);
            if (selectedViewHolder == null) return;
            View focusView = selectedViewHolder.itemView;
            focusView.requestFocus();
            focusView.sendAccessibilityEvent(TYPE_VIEW_FOCUSED);
        } else if (BOTTOM_PADDING == propertyKey) {
            int left = view.getPaddingLeft();
            int top = view.getPaddingTop();
            int right = view.getPaddingRight();
            int bottom = model.get(BOTTOM_PADDING);
            view.setPadding(left, top, right, bottom);
        } else if (FETCH_VIEW_BY_INDEX_CALLBACK == propertyKey) {
            Callback<Function<Integer, View>> callback = model.get(FETCH_VIEW_BY_INDEX_CALLBACK);
            callback.onResult(
                    (Integer index) -> {
                        RecyclerView.ViewHolder viewHolder =
                                view.findViewHolderForAdapterPosition(index);
                        return viewHolder == null ? null : viewHolder.itemView;
                    });
        } else if (GET_VISIBLE_RANGE_CALLBACK == propertyKey) {
            Callback<Supplier<Pair<Integer, Integer>>> callback =
                    model.get(GET_VISIBLE_RANGE_CALLBACK);
            callback.onResult(
                    () -> {
                        LinearLayoutManager layoutManager =
                                (LinearLayoutManager) view.getLayoutManager();
                        int start = layoutManager.findFirstCompletelyVisibleItemPosition();
                        int end = layoutManager.findLastCompletelyVisibleItemPosition();
                        return new Pair<>(start, end);
                    });
        } else if (IS_SCROLLING_SUPPLIER_CALLBACK == propertyKey) {
            Callback<ObservableSupplier<Boolean>> callback =
                    model.get(IS_SCROLLING_SUPPLIER_CALLBACK);
            ObservableSupplierImpl<Boolean> supplier = new ObservableSupplierImpl<>(false);
            view.addOnScrollListener(
                    new OnScrollListener() {
                        @Override
                        public void onScrollStateChanged(
                                @NonNull RecyclerView recyclerView, int newState) {
                            supplier.set(newState != RecyclerView.SCROLL_STATE_IDLE);
                        }
                    });
            callback.onResult(supplier);
        }
    }

    private static int computeOffset(TabListRecyclerView view, PropertyModel model) {
        int width = view.getWidth();
        int height = view.getHeight();
        final BrowserControlsStateProvider browserControlsStateProvider =
                model.get(BROWSER_CONTROLS_STATE_PROVIDER);
        // If layout hasn't happened yet fallback to dimensions based on visible display frame. This
        // works for multi-window and different orientations. Don't use View#post() because this
        // will cause animation jank for expand/shrink animations.
        if (width == 0 || height == 0) {
            Rect frame = new Rect();
            ((Activity) view.getContext())
                    .getWindow()
                    .getDecorView()
                    .getWindowVisibleDisplayFrame(frame);
            width = frame.width();
            // Remove toolbar height from height.
            height =
                    frame.height()
                            - Math.round(browserControlsStateProvider.getTopVisibleContentOffset());
        }
        if (width <= 0 || height <= 0) return 0;

        @TabListCoordinator.TabListMode int mode = model.get(MODE);
        LinearLayoutManager layoutManager = (LinearLayoutManager) view.getLayoutManager();
        if (mode == TabListCoordinator.TabListMode.GRID) {
            GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
            int cardWidth = width / gridLayoutManager.getSpanCount();
            int cardHeight =
                    TabUtils.deriveGridCardHeight(
                            cardWidth, view.getContext(), browserControlsStateProvider);
            return Math.max(0, height / 2 - cardHeight / 2);
        }
        if (mode == TabListCoordinator.TabListMode.LIST) {
            // Avoid divide by 0 when there are no tabs.
            if (layoutManager.getItemCount() == 0) return 0;

            return Math.max(
                    0,
                    height / 2
                            - view.computeVerticalScrollRange() / layoutManager.getItemCount() / 2);
        }
        assert false : "Unexpected MODE when setting INITIAL_SCROLL_INDEX.";
        return 0;
    }
}