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

// Copyright 2022 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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;

import org.chromium.base.Callback;
import org.chromium.base.CollectionUtil;
import org.chromium.chrome.browser.tab_ui.TabListFaviconProvider;
import org.chromium.ui.modelutil.MVCListAdapter.ListItem;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.ModelListPropertyChangeFilter;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * Observes a {@link ModelList} and returns tokens for conceptual snapshots of the model state.
 * The opaque token can be compared against other tokens with {@link Object#equals(Object)}. The
 * purpose of these tokens is to decide when the bottom toolbar should take a bitmap capture in
 * preparation for browser controls being scrolled off the screen. This class only watches
 * properties that directly effect the steady state of the view, and thus it implicitly tightly
 * coupled with the TabListMode.STRIP mode of the {@link TabListCoordinator} component.
 */
public class TabStripSnapshotter {
    private static final Set<PropertyKey> SNAPSHOT_PROPERTY_KEY_SET =
            CollectionUtil.newHashSet(
                    TabProperties.FAVICON_FETCHER,
                    TabProperties.FAVICON_FETCHED,
                    TabProperties.IS_SELECTED);

    /**
     * A token that contains an ordered list of tuples for each tab in the tab strip. Should be
     * compared against other snapshot tokens with {@link Object#equals(Object)}.
     */
    private static class TabStripSnapshotToken {
        private final int mScrollX;
        private final List<TabStripItemSnapshot> mList;

        public TabStripSnapshotToken(ModelList modelList, int scrollX) {
            mScrollX = scrollX;
            mList = new ArrayList<>(modelList.size());
            for (int i = 0; i < modelList.size(); i++) {
                ListItem listItem = modelList.get(i);
                TabStripItemSnapshot itemSnapshot = new TabStripItemSnapshot(listItem.model);
                mList.add(itemSnapshot);
            }
        }

        @Override
        public int hashCode() {
            return Objects.hash(mList, mScrollX);
        }

        @Override
        public boolean equals(@Nullable Object obj) {
            if (!(obj instanceof TabStripSnapshotToken)) {
                return false;
            }
            TabStripSnapshotToken other = (TabStripSnapshotToken) obj;
            if (mScrollX != other.mScrollX) {
                return false;
            }
            return mList.equals(other.mList);
        }
    }

    /** Simple tuple to hold all relevant fields for a single tab item. */
    private static class TabStripItemSnapshot {
        @Nullable public final TabListFaviconProvider.TabFaviconFetcher mTabFaviconFetcher;
        public final boolean mFaviconFetched;
        public final boolean mIsSelected;

        public TabStripItemSnapshot(PropertyModel propertyModel) {
            mTabFaviconFetcher = propertyModel.get(TabProperties.FAVICON_FETCHER);
            mFaviconFetched = propertyModel.get(TabProperties.FAVICON_FETCHED);
            mIsSelected = propertyModel.get(TabProperties.IS_SELECTED);
        }

        @Override
        public int hashCode() {
            return Objects.hash(mTabFaviconFetcher, mFaviconFetched, mIsSelected);
        }

        @Override
        public boolean equals(@Nullable Object obj) {
            if (!(obj instanceof TabStripItemSnapshot)) {
                return false;
            }
            TabStripItemSnapshot other = (TabStripItemSnapshot) obj;
            return Objects.equals(mTabFaviconFetcher, other.mTabFaviconFetcher)
                    && this.mFaviconFetched == other.mFaviconFetched
                    && this.mIsSelected == other.mIsSelected;
        }
    }

    private final Callback<Object> mOnModelTokenChange;
    private final ModelList mModelList;
    private final RecyclerView mRecyclerView;
    private final OnScrollListener mOnScrollListener;
    private final ModelListPropertyChangeFilter mPropertyObserverFilter;

    /**
     * @param onModelTokenChange Where to pass the token when the snapshot is taken.
     * @param modelList The model to observe.
     * @param recyclerView The recycler view that can be scrolled.
     */
    public TabStripSnapshotter(
            @NonNull Callback<Object> onModelTokenChange,
            @NonNull ModelList modelList,
            @NonNull RecyclerView recyclerView) {
        mOnModelTokenChange = onModelTokenChange;
        mModelList = modelList;
        mRecyclerView = recyclerView;
        mOnScrollListener =
                new OnScrollListener() {
                    @Override
                    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                            doSnapshot();
                        }
                    }
                };
        mRecyclerView.addOnScrollListener(mOnScrollListener);
        mPropertyObserverFilter =
                new ModelListPropertyChangeFilter(
                        this::doSnapshot, modelList, SNAPSHOT_PROPERTY_KEY_SET);
    }

    private void doSnapshot() {
        int scrollX = mRecyclerView.computeHorizontalScrollOffset();
        mOnModelTokenChange.onResult(new TabStripSnapshotToken(mModelList, scrollX));
    }

    public void destroy() {
        mRecyclerView.removeOnScrollListener(mOnScrollListener);
        mPropertyObserverFilter.destroy();
    }
}