chromium/chrome/android/java/src/org/chromium/chrome/browser/tab/TabViewManagerImpl.java

// Copyright 2020 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.tab;

import android.graphics.Rect;
import android.util.SparseIntArray;
import android.view.View;
import android.widget.FrameLayout;

import androidx.annotation.ColorInt;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.supplier.DestroyableObservableSupplier;
import org.chromium.chrome.browser.browser_controls.BrowserControlsMarginSupplier;

import java.util.Comparator;
import java.util.PriorityQueue;

/**
 * This class is responsible for displaying custom {@link View}s on top of {@link Tab}'s Content
 * view. Users that want to display a custom {@link View} should:
 *     1. Implement {@link TabViewProvider}
 *     2. Add an entry to {@link TabViewProvider.Type}
 *     3. Add their {@link TabViewProvider.Type} to {@link #PRIORITIZED_TAB_VIEW_PROVIDER_TYPES}
 *        with an appropriate priority. In order to find the right priority, please consider the
 *        existing entries in the array and determine where the new feature fits relative to them.
 *     4. Use {@link Tab#getTabViewManager#addTabViewProvider} and
 *        {@link Tab#getTabViewManager#removeTabViewProvider} to add and remove their
 *        {@link TabViewProvider}.
 */
class TabViewManagerImpl implements TabViewManager, Comparator<TabViewProvider> {
    /**
     * A prioritized list of all {@link TabViewProvider.Type}s, from most important to least
     * important. The {@link TabViewProvider} with the highest priority will always be shown first,
     * regardless of its insertion time relative to other {@link TabViewProvider}s.
     */
    @VisibleForTesting @TabViewProvider.Type
    static final int[] PRIORITIZED_TAB_VIEW_PROVIDER_TYPES =
            new int[] {
                TabViewProvider.Type.SUSPENDED_TAB,
                TabViewProvider.Type.PAINT_PREVIEW,
                TabViewProvider.Type.SAD_TAB
            };

    /**
     * A lookup table for {@link #PRIORITIZED_TAB_VIEW_PROVIDER_TYPES}. This is initialized in the
     * following static block and doesn't need to be manually updated.
     */
    private static final SparseIntArray TAB_VIEW_PROVIDER_PRIORITY_LOOKUP = new SparseIntArray();

    static {
        for (int i = 0; i < PRIORITIZED_TAB_VIEW_PROVIDER_TYPES.length; i++) {
            TAB_VIEW_PROVIDER_PRIORITY_LOOKUP.put(PRIORITIZED_TAB_VIEW_PROVIDER_TYPES[i], i);
        }
    }

    private PriorityQueue<TabViewProvider> mTabViewProviders;
    private TabImpl mTab;
    private View mCurrentView;
    private DestroyableObservableSupplier<Rect> mMarginSupplier;
    private final Rect mViewMargins = new Rect();

    TabViewManagerImpl(TabImpl tab) {
        mTab = tab;
        mTabViewProviders = new PriorityQueue<>(PRIORITIZED_TAB_VIEW_PROVIDER_TYPES.length, this);
    }

    private void initMarginSupplier() {
        if (mTab.getActivity() == null
                || mTab.getActivity().isActivityFinishingOrDestroyed()
                || mMarginSupplier != null) {
            return;
        }

        mMarginSupplier =
                new BrowserControlsMarginSupplier(mTab.getActivity().getBrowserControlsManager());
        mMarginSupplier.addObserver(this::updateViewMargins);
        // Update margins immediately if available rather than waiting for a posted notification.
        // Waiting for a posted notification could allow a layout pass to occur before the margins
        // are set.
        updateViewMargins(mMarginSupplier.get());
    }

    /**
     * @return Whether the given {@link TabViewProvider} is currently being displayed.
     */
    @Override
    public boolean isShowing(TabViewProvider tabViewProvider) {
        TabViewProvider currentTVP = mTabViewProviders.peek();
        return currentTVP != null && currentTVP == tabViewProvider;
    }

    /**
     * Adds a {@link TabViewProvider} to be shown in the {@link Tab} associated with this {@link
     * TabViewManager}. If the given {@link TabViewProvider} has the highest priority, it will be
     * shown immediately. Otherwise, it will be shown after other {@link TabViewProvider}s with
     * higher priorities are removed.
     */
    @Override
    public void addTabViewProvider(TabViewProvider tabViewProvider) {
        if (mTabViewProviders.contains(tabViewProvider)) return;

        TabViewProvider currentTabViewProvider = mTabViewProviders.peek();
        mTabViewProviders.add(tabViewProvider);
        updateCurrentTabViewProvider(currentTabViewProvider);
    }

    /**
     * Remove the given {@link TabViewProvider} from the {@link Tab} associated with this {@link
     * TabViewManager}. If the given {@link TabViewProvider} is currently shown, the next available
     * {@link TabViewProvider} with the highest priority will be shown. If there are no other {@link
     * TabViewProvider}s, {@link Tab}'s Content view will be shown.
     */
    @Override
    public void removeTabViewProvider(TabViewProvider tabViewProvider) {
        TabViewProvider currentTabViewProvider = mTabViewProviders.peek();
        mTabViewProviders.remove(tabViewProvider);
        updateCurrentTabViewProvider(currentTabViewProvider);
    }

    private void updateCurrentTabViewProvider(TabViewProvider previousTabViewProvider) {
        if (mTab == null) return;

        TabViewProvider currentTabViewProvider = mTabViewProviders.peek();
        if (currentTabViewProvider != previousTabViewProvider) {
            View view = null;
            @ColorInt Integer backgroundColor = null;
            if (currentTabViewProvider != null) {
                view = currentTabViewProvider.getView();
                assert view != null;
                view.setFocusable(true);
                view.setFocusableInTouchMode(true);
                backgroundColor = currentTabViewProvider.getBackgroundColor(view.getContext());
            }
            mCurrentView = view;
            initMarginSupplier();
            updateViewMargins();
            mTab.setCustomView(mCurrentView, backgroundColor);
            if (previousTabViewProvider != null) previousTabViewProvider.onHidden();
            if (currentTabViewProvider != null) currentTabViewProvider.onShown();
        }
    }

    private void updateViewMargins(Rect viewMargins) {
        if (viewMargins == null) return;

        mViewMargins.set(viewMargins);
        updateViewMargins();
    }

    private void updateViewMargins() {
        if (mCurrentView == null) return;

        FrameLayout.LayoutParams layoutParams =
                new FrameLayout.LayoutParams(
                        FrameLayout.LayoutParams.MATCH_PARENT,
                        FrameLayout.LayoutParams.MATCH_PARENT);
        layoutParams.setMargins(
                mViewMargins.left, mViewMargins.top, mViewMargins.right, mViewMargins.bottom);
        mCurrentView.setLayoutParams(layoutParams);
    }

    /**
     * Compares two {@link TabViewProvider}s based on their priority in
     * {@link #PRIORITIZED_TAB_VIEW_PROVIDER_TYPES}. Do not edit the logic here when you add a new
     * {@link TabViewProvider.Type}. Instead, simply add your new {@link TabViewProvider.Type} to
     * {@link #PRIORITIZED_TAB_VIEW_PROVIDER_TYPES}.
     */
    @Override
    public int compare(TabViewProvider tvp1, TabViewProvider tvp2) {
        int tvp1Priority = TAB_VIEW_PROVIDER_PRIORITY_LOOKUP.get(tvp1.getTabViewProviderType());
        int tvp2Priority = TAB_VIEW_PROVIDER_PRIORITY_LOOKUP.get(tvp2.getTabViewProviderType());
        return tvp1Priority - tvp2Priority;
    }

    void destroy() {
        mTab.setCustomView(null, null);
        TabViewProvider currentTabViewProvider = mTabViewProviders.peek();
        if (currentTabViewProvider != null) currentTabViewProvider.onHidden();
        mTabViewProviders.clear();
        if (mMarginSupplier != null) mMarginSupplier.destroy();
        mTab = null;
    }
}