chromium/chrome/android/java/src/org/chromium/chrome/browser/ntp/RecentTabsPage.java

// Copyright 2015 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.ntp;

import android.app.Activity;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ExpandableListView;

import org.chromium.base.Callback;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.tab_ui.InvalidationAwareThumbnailProvider;
import org.chromium.chrome.browser.ui.native_page.NativePage;
import org.chromium.chrome.browser.ui.native_page.NativePageHost;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.base.ViewUtils;

/**
 * The native recent tabs page. Lists recently closed tabs, open windows and tabs from the user's
 * synced devices, and snapshot documents sent from Chrome to Mobile in an expandable list view.
 */
public class RecentTabsPage
        implements NativePage,
                ExpandableListView.OnChildClickListener,
                ExpandableListView.OnGroupCollapseListener,
                ExpandableListView.OnGroupExpandListener,
                RecentTabsManager.UpdatedCallback,
                View.OnAttachStateChangeListener,
                View.OnCreateContextMenuListener,
                InvalidationAwareThumbnailProvider,
                BrowserControlsStateProvider.Observer {
    private final Activity mActivity;
    private final BrowserControlsStateProvider mBrowserControlsStateProvider;
    private final ExpandableListView mListView;
    private final String mTitle;
    private final ViewGroup mView;

    private RecentTabsManager mRecentTabsManager;
    private RecentTabsRowAdapter mAdapter;
    private NativePageHost mPageHost;

    private boolean mSnapshotContentChanged;
    private int mSnapshotListPosition;
    private int mSnapshotListTop;
    private int mSnapshotWidth;
    private int mSnapshotHeight;

    /** Whether {@link #mView} is attached to the application window. */
    private boolean mIsAttachedToWindow;

    private final ObservableSupplier<Integer> mTabStripHeightSupplier;
    private Callback<Integer> mTabStripHeightChangeCallback;

    /**
     * Constructor returns an instance of RecentTabsPage.
     *
     * @param activity The activity this view belongs to.
     * @param recentTabsManager The RecentTabsManager which provides the model data.
     * @param pageHost The NativePageHost used to provide a history navigation delegate object.
     * @param browserControlsStateProvider The {@link BrowserControlsStateProvider} used to provide
     *     offset values.
     * @param tabStripHeightSupplier Supplier for the tab strip height.
     */
    public RecentTabsPage(
            Activity activity,
            RecentTabsManager recentTabsManager,
            NativePageHost pageHost,
            BrowserControlsStateProvider browserControlsStateProvider,
            ObservableSupplier<Integer> tabStripHeightSupplier) {
        mActivity = activity;
        mRecentTabsManager = recentTabsManager;
        mPageHost = pageHost;
        Resources resources = activity.getResources();

        mTitle = resources.getString(R.string.recent_tabs);
        mRecentTabsManager.setUpdatedCallback(this);
        LayoutInflater inflater = LayoutInflater.from(activity);
        mView = (ViewGroup) inflater.inflate(R.layout.recent_tabs_page, null);
        mListView = mView.findViewById(R.id.odp_listview);
        mAdapter = new RecentTabsRowAdapter(activity, recentTabsManager);
        mListView.setAdapter(mAdapter);
        mListView.setOnChildClickListener(this);
        mListView.setGroupIndicator(null);
        mListView.setOnGroupCollapseListener(this);
        mListView.setOnGroupExpandListener(this);
        mListView.setOnCreateContextMenuListener(this);

        mView.addOnAttachStateChangeListener(this);

        if (!DeviceFormFactor.isNonMultiDisplayContextOnTablet(mActivity)) {
            mBrowserControlsStateProvider = browserControlsStateProvider;
            mBrowserControlsStateProvider.addObserver(this);
            onBottomControlsHeightChanged(
                    mBrowserControlsStateProvider.getBottomControlsHeight(),
                    mBrowserControlsStateProvider.getBottomControlsMinHeight());
        } else {
            mBrowserControlsStateProvider = null;
        }

        mTabStripHeightSupplier = tabStripHeightSupplier;
        mView.setPadding(0, mTabStripHeightSupplier.get(), 0, 0);
        mTabStripHeightChangeCallback =
                newHeight ->
                        mView.setPadding(
                                mView.getPaddingLeft(),
                                newHeight,
                                mView.getPaddingRight(),
                                mView.getPaddingBottom());
        mTabStripHeightSupplier.addObserver(mTabStripHeightChangeCallback);

        onUpdated();
    }

    // NativePage overrides

    @Override
    public String getUrl() {
        return UrlConstants.RECENT_TABS_URL;
    }

    @Override
    public String getTitle() {
        return mTitle;
    }

    @Override
    public int getBackgroundColor() {
        return SemanticColorUtils.getDefaultBgColor(mActivity);
    }

    @Override
    public boolean needsToolbarShadow() {
        return true;
    }

    @Override
    public View getView() {
        return mView;
    }

    @Override
    public String getHost() {
        return UrlConstants.RECENT_TABS_HOST;
    }

    @Override
    public void destroy() {
        assert !mIsAttachedToWindow : "Destroy called before removed from window";
        mRecentTabsManager.destroy();
        mRecentTabsManager = null;
        mPageHost = null;
        mAdapter.notifyDataSetInvalidated();
        mAdapter = null;
        mListView.setAdapter((RecentTabsRowAdapter) null);

        mView.removeOnAttachStateChangeListener(this);
        if (mBrowserControlsStateProvider != null) {
            mBrowserControlsStateProvider.removeObserver(this);
        }

        mTabStripHeightSupplier.removeObserver(mTabStripHeightChangeCallback);
    }

    @Override
    public void updateForUrl(String url) {}

    // View.OnAttachStateChangeListener
    @Override
    public void onViewAttachedToWindow(View view) {
        // Called when the user opens the RecentTabsPage or switches back to the RecentTabsPage from
        // another tab.
        mIsAttachedToWindow = true;

        // Work around a bug on Samsung devices where the recent tabs page does not appear after
        // toggling the Sync quick setting.  For some reason, the layout is being dropped on the
        // flow and we need to force a root level layout to get the UI to appear.
        ViewUtils.requestLayout(view.getRootView(), "RecentTabsPage.onViewAttachedToWindow");
    }

    @Override
    public void onViewDetachedFromWindow(View view) {
        // Called when the user navigates from the RecentTabsPage or switches to another tab.
        mIsAttachedToWindow = false;
    }

    // ExpandableListView.OnChildClickedListener
    @Override
    public boolean onChildClick(
            ExpandableListView parent, View v, int groupPosition, int childPosition, long id) {
        return mAdapter.getGroup(groupPosition).onChildClick(childPosition);
    }

    // ExpandableListView.OnGroupExpandedListener
    @Override
    public void onGroupExpand(int groupPosition) {
        mAdapter.getGroup(groupPosition).setCollapsed(false);
        mSnapshotContentChanged = true;
    }

    // ExpandableListView.OnGroupCollapsedListener
    @Override
    public void onGroupCollapse(int groupPosition) {
        mAdapter.getGroup(groupPosition).setCollapsed(true);
        mSnapshotContentChanged = true;
    }

    // RecentTabsManager.UpdatedCallback
    @Override
    public void onUpdated() {
        mAdapter.notifyDataSetChanged();
        for (int i = 0; i < mAdapter.getGroupCount(); i++) {
            if (mAdapter.getGroup(i).isCollapsed()) {
                mListView.collapseGroup(i);
            } else {
                mListView.expandGroup(i);
            }
        }
        mSnapshotContentChanged = true;
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
        // Would prefer to have this context menu view managed internal to RecentTabsGroupView
        // Unfortunately, setting either onCreateContextMenuListener or onLongClickListener
        // disables the native onClick (expand/collapse) behaviour of the group view.
        ExpandableListView.ExpandableListContextMenuInfo info =
                (ExpandableListView.ExpandableListContextMenuInfo) menuInfo;

        int type = ExpandableListView.getPackedPositionType(info.packedPosition);
        int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition);

        if (type == ExpandableListView.PACKED_POSITION_TYPE_GROUP) {
            mAdapter.getGroup(groupPosition).onCreateContextMenuForGroup(menu, mActivity);
        } else if (type == ExpandableListView.PACKED_POSITION_TYPE_CHILD) {
            int childPosition = ExpandableListView.getPackedPositionChild(info.packedPosition);
            mAdapter.getGroup(groupPosition)
                    .onCreateContextMenuForChild(childPosition, menu, mActivity);
        }
    }

    // InvalidationAwareThumbnailProvider

    @Override
    public boolean shouldCaptureThumbnail() {
        if (mView.getWidth() == 0 || mView.getHeight() == 0) return false;

        View topItem = mListView.getChildAt(0);
        return mSnapshotContentChanged
                || mSnapshotListPosition != mListView.getFirstVisiblePosition()
                || mSnapshotListTop != (topItem == null ? 0 : topItem.getTop())
                || mView.getWidth() != mSnapshotWidth
                || mView.getHeight() != mSnapshotHeight;
    }

    @Override
    public void captureThumbnail(Canvas canvas) {
        ViewUtils.captureBitmap(mView, canvas);
        mSnapshotContentChanged = false;
        mSnapshotListPosition = mListView.getFirstVisiblePosition();
        View topItem = mListView.getChildAt(0);
        mSnapshotListTop = topItem == null ? 0 : topItem.getTop();
        mSnapshotWidth = mView.getWidth();
        mSnapshotHeight = mView.getHeight();
    }

    @Override
    public void onBottomControlsHeightChanged(
            int bottomControlsHeight, int bottomControlsMinHeight) {
        updateMargins();
    }

    @Override
    public void onTopControlsHeightChanged(int topControlsHeight, int topControlsMinHeight) {
        updateMargins();
    }

    @Override
    public void onControlsOffsetChanged(
            int topOffset,
            int topControlsMinHeightOffset,
            int bottomOffset,
            int bottomControlsMinHeightOffset,
            boolean needsAnimate,
            boolean isVisibilityForced) {
        updateMargins();
    }

    private void updateMargins() {
        final View recentTabsRoot = mView.findViewById(R.id.recent_tabs_root);
        final int topControlsHeight = mBrowserControlsStateProvider.getTopControlsHeight();
        final int contentOffset = mBrowserControlsStateProvider.getContentOffset();
        ViewGroup.MarginLayoutParams layoutParams =
                (ViewGroup.MarginLayoutParams) recentTabsRoot.getLayoutParams();
        int topMargin = layoutParams.topMargin;

        // If the top controls are at the resting position or their height is decreasing, we want to
        // update the margin. We don't do this if the controls height is increasing because changing
        // the margin shrinks the view height to its final value, leaving a gap at the bottom until
        // the animation finishes.
        if (contentOffset >= topControlsHeight) {
            topMargin = topControlsHeight;
        }

        // If the content offset is different from the margin, we use translationY to position the
        // view in line with the content offset.
        recentTabsRoot.setTranslationY(contentOffset - topMargin);

        final int bottomMargin = mBrowserControlsStateProvider.getBottomControlsHeight();
        if (topMargin != layoutParams.topMargin || bottomMargin != layoutParams.bottomMargin) {
            layoutParams.topMargin = topMargin;
            layoutParams.bottomMargin = bottomMargin;
            recentTabsRoot.setLayoutParams(layoutParams);
        }
    }

    Callback<Integer> getTabStripHeightChangeCallbackForTesting() {
        return mTabStripHeightChangeCallback;
    }
}