chromium/chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/Layout.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.compositor.layouts;

import android.content.Context;
import android.graphics.PointF;
import android.graphics.RectF;
import android.view.MotionEvent;
import android.view.ViewGroup;

import androidx.annotation.IntDef;

import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.compositor.layouts.components.LayoutTab;
import org.chromium.chrome.browser.layouts.EventFilter;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.layouts.animation.CompositorAnimationHandler;
import org.chromium.chrome.browser.layouts.scene_layer.SceneLayer;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.resources.ResourceManager;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;

/**
 * Abstract representation of an OpenGL layout tailored to draw tabs. It is a framework used as an
 * alternative to the Android UI for lower level hardware accelerated rendering.
 * This layout also pass through all the events that may happen.
 */
public abstract class Layout {
    /** The orientation of the device. */
    @IntDef({Orientation.UNSET, Orientation.PORTRAIT, Orientation.LANDSCAPE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Orientation {
        int UNSET = 0;
        int PORTRAIT = 1;
        int LANDSCAPE = 2;
    }

    /** The possible variations of the visible viewport that different layouts may need. */
    @IntDef({
        ViewportMode.ALWAYS_FULLSCREEN,
        ViewportMode.ALWAYS_SHOWING_BROWSER_CONTROLS,
        ViewportMode.DYNAMIC_BROWSER_CONTROLS,
        ViewportMode.USE_PREVIOUS_BROWSER_CONTROLS_STATE
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ViewportMode {
        /** The viewport is assumed to be always fullscreen. */
        int ALWAYS_FULLSCREEN = 0;

        /** The viewport is assuming that browser controls are permanently shown. */
        int ALWAYS_SHOWING_BROWSER_CONTROLS = 1;

        /** The viewport will account for animating browser controls (both shown and hidden). */
        int DYNAMIC_BROWSER_CONTROLS = 2;

        /** Use a viewport that accounts for the browser controls state in the previous layout. */
        int USE_PREVIOUS_BROWSER_CONTROLS_STATE = 3;
    }

    @IntDef({
        LayoutState.STARTING_TO_SHOW,
        LayoutState.SHOWING,
        LayoutState.STARTING_TO_HIDE,
        LayoutState.HIDDEN
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface LayoutState {
        /** The layout is going to hide as soon as the animation finishes. */
        int STARTING_TO_SHOW = 0;

        /** Actively being showed, no ongoing animation. */
        int SHOWING = 1;

        /** The layout is going to show as soon as the animation finishes. */
        int STARTING_TO_HIDE = 2;

        /** Not currently showed, and no ongoing animation. */
        int HIDDEN = 3;
    }

    /** Length of the unstalling animation. **/
    public static final long UNSTALLED_ANIMATION_DURATION_MS = 500;

    private static final float SNAP_SPEED = 1.0f; // dp per second

    // Drawing area properties.
    private float mWidthDp;
    private float mHeightDp;

    /** A {@link Context} instance. */
    private Context mContext;

    /** The current {@link Orientation} of the layout. */
    private @Orientation int mCurrentOrientation;

    // Tabs
    protected TabModelSelector mTabModelSelector;
    protected TabContentManager mTabContentManager;

    // Helpers
    private final LayoutUpdateHost mUpdateHost;
    protected final LayoutRenderHost mRenderHost;

    /** The tabs currently being rendered as part of this layout. The tabs are
     * drawn using the same ordering as this array. */
    protected LayoutTab[] mLayoutTabs;

    // Current state of the Layout.
    private @LayoutState int mLayoutState;

    // The ratio of dp to px.
    protected final float mDpToPx;
    protected final float mPxToDp;

    /**
     * The {@link Layout} is not usable until sizeChanged is called.
     * This is convenient this way so we can pre-create the layout before the host is fully defined.
     * @param context      The current Android's context.
     * @param updateHost   The parent {@link LayoutUpdateHost}.
     * @param renderHost   The parent {@link LayoutRenderHost}.
     */
    public Layout(Context context, LayoutUpdateHost updateHost, LayoutRenderHost renderHost) {
        mContext = context;
        mUpdateHost = updateHost;
        mRenderHost = renderHost;

        // Invalid sizes
        mWidthDp = -1;
        mHeightDp = -1;

        mCurrentOrientation = Orientation.UNSET;
        mDpToPx = context.getResources().getDisplayMetrics().density;
        mPxToDp = 1 / mDpToPx;

        mLayoutState = LayoutState.HIDDEN;
    }

    /**
     * @return The handler responsible for running compositor animations.
     */
    public CompositorAnimationHandler getAnimationHandler() {
        return mUpdateHost.getAnimationHandler();
    }

    /** Called when native initialization is completed. */
    public void onFinishNativeInitialization() {}

    /** Cleans up any internal state. This object should not be used after this call. */
    public void destroy() {}

    /**
     * @return The current {@link Context} instance associated with this {@link Layout}.
     */
    public Context getContext() {
        return mContext;
    }

    /**
     * @return Whether the {@link Layout} is currently active.
     */
    public boolean isActive() {
        return mUpdateHost.isActiveLayout(this);
    }

    /**
     * Creates a {@link LayoutTab}.
     * @param id              The id of the reference {@link Tab} in the {@link TabModel}.
     * @param isIncognito     Whether the new tab is incognito.
     * @return                The newly created {@link LayoutTab}.
     */
    public LayoutTab createLayoutTab(int id, boolean isIncognito) {
        return createLayoutTab(id, isIncognito, -1.f, -1.f);
    }

    /**
     * Creates a {@link LayoutTab}.
     * @param id               The id of the reference {@link Tab} in the {@link TabModel}.
     * @param isIncognito      Whether the new tab is incognito.
     * @param maxContentWidth  The max content width of the tab.  Negative numbers will use the
     *                         original content width.
     * @param maxContentHeight The max content height of the tab.  Negative numbers will use the
     *                         original content height.
     * @return                 The newly created {@link LayoutTab}.
     */
    public LayoutTab createLayoutTab(
            int id, boolean isIncognito, float maxContentWidth, float maxContentHeight) {
        LayoutTab layoutTab =
                mUpdateHost.createLayoutTab(id, isIncognito, maxContentWidth, maxContentHeight);
        initLayoutTabFromHost(layoutTab);
        return layoutTab;
    }

    /**
     * Releases the data we keep for that {@link LayoutTab}.
     * @param layoutTab The {@link LayoutTab} to release.
     */
    public void releaseTabLayout(LayoutTab layoutTab) {
        mUpdateHost.releaseTabLayout(layoutTab.getId());
    }

    /**
     * Releases cached title texture resources for the {@link LayoutTab}.
     * @param layoutTab The {@link LayoutTab} to release resources for.
     */
    public void releaseResourcesForTab(LayoutTab layoutTab) {
        mUpdateHost.releaseResourcesForTab(layoutTab.getId());
    }

    /**
     * Update the animation and give chance to cascade the changes.
     * @param time The current time of the app in ms.
     * @param dt   The delta time between update frames in ms.
     * @return     Whether the layout is done updating.
     */
    public final boolean onUpdate(long time, long dt) {
        final boolean doneAnimating = onUpdateAnimation(time, false);

        // Don't update the layout if onUpdateAnimation ended up making a new layout active.
        if (mUpdateHost.isActiveLayout(this)) updateLayout(time, dt);

        return doneAnimating;
    }

    /**
     * Layout-specific updates. Cascades the values updated by the animations.
     * @param time The current time of the app in ms.
     * @param dt   The delta time between update frames in ms.
     */
    protected void updateLayout(long time, long dt) {}

    /**
     * Update snapping to pixel. To be called once every frame.
     *
     * <p>TODO(crbug.com/40126259): Temporary placement. This is some Mediator logic and should move
     * to the appropriate location when doing MVC. Maybe move to {@link LayoutMediator}.
     *
     * @param dt The delta time between update frames in ms.
     * @param layoutTab The {@link LayoutTab} that needs to be updating.
     * @return True if the snapping requests to render at least one more frame.
     */
    protected boolean updateSnap(long dt, PropertyModel layoutTab) {
        final float step = dt * SNAP_SPEED / 1000.0f;
        final float renderX = layoutTab.get(LayoutTab.RENDER_X);
        final float renderY = layoutTab.get(LayoutTab.RENDER_Y);
        final float x = updateSnap(step, renderX, layoutTab.get(LayoutTab.X));
        final float y = updateSnap(step, renderY, layoutTab.get(LayoutTab.Y));
        final boolean change = x != renderX || y != renderY;
        layoutTab.set(LayoutTab.RENDER_X, x);
        layoutTab.set(LayoutTab.RENDER_Y, y);
        return change;
    }

    private float updateSnap(float step, float current, float ref) {
        if (Math.abs(current - ref) > mPxToDp) return ref;
        final float refRounded = Math.round(ref * mDpToPx) * mPxToDp;
        if (refRounded < ref) {
            current -= step;
            current = Math.max(refRounded, current);
        } else {
            current += step;
            current = Math.min(refRounded, current);
        }
        return current;
    }

    /**
     * Request that the renderer render a frame (after the current frame). This
     * should be called whenever a new frame should be rendered.
     */
    public void requestRender() {
        mRenderHost.requestRender();
    }

    /**
     * Requests one more frame of refresh for the transforms and changing properties. Primarily,
     * this is so animations can continue to animate.
     */
    public void requestUpdate() {
        mUpdateHost.requestUpdate();
    }

    /**
     * Called when the context and size of the view has changed.
     *
     * @param context     The current Android's context.
     */
    public void contextChanged(Context context) {
        mContext = context;
        LayoutTab.resetDimensionConstants(context);
    }

    /**
     * Called when the size of the viewport has changed.
     *
     * @param screenViewportPx The viewport of the screen in px.
     * @param orientation The new orientation. Valid values are defined by {@link Orientation}.
     */
    final void sizeChanged(RectF screenViewportPx, @Orientation int orientation) {
        mWidthDp = screenViewportPx.width() / mDpToPx;
        mHeightDp = screenViewportPx.height() / mDpToPx;
        mCurrentOrientation = orientation;
    }

    /**
     * Sets the the {@link TabModelSelector} for the layout.
     * @param modelSelector The {@link TabModelSelector} to be set on the layout.
     */
    public void setTabModelSelector(TabModelSelector modelSelector) {
        mTabModelSelector = modelSelector;
    }

    /**
     * Sets the {@link TabContentManager} needed for the layout to get thumbnails.
     * @param manager The {@link TabContentManager} to get tab display content.
     */
    protected void setTabContentManager(TabContentManager manager) {
        if (manager == null) return;

        mTabContentManager = manager;
    }

    /**
     * @return The sizing mode for the layout.
     */
    public @ViewportMode int getViewportMode() {
        return ViewportMode.ALWAYS_SHOWING_BROWSER_CONTROLS;
    }

    /**
     * Informs this cache of the visible {@link Tab} {@code id}s, as well as the
     * primary screen-filling tab.
     */
    protected void updateCacheVisibleIdsAndPrimary(List<Integer> visible, int primaryTabId) {
        if (mTabContentManager != null) mTabContentManager.updateVisibleIds(visible, primaryTabId);
    }

    /**
     * Informs this cache of the visible {@link Tab} {@code id}s, in cases where there
     * is no primary screen-filling tab.
     */
    protected void updateCacheVisibleIds(List<Integer> visible) {
        updateCacheVisibleIdsAndPrimary(visible, Tab.INVALID_TAB_ID);
    }

    /** To be called when the layout is starting a transition out of the view mode. */
    public void startHiding() {
        mUpdateHost.startHiding();
        mLayoutState = LayoutState.STARTING_TO_HIDE;
    }

    /**
     * @return True is the layout is in the process of hiding itself.
     */
    public boolean isStartingToHide() {
        return mLayoutState == LayoutState.STARTING_TO_HIDE;
    }

    /**
     * @return True is the layout is in the process of showing itself.
     */
    public boolean isStartingToShow() {
        return mLayoutState == LayoutState.STARTING_TO_SHOW;
    }

    /**
     * @return The incognito state of the layout.
     */
    public boolean isIncognito() {
        return mTabModelSelector.isIncognitoSelected();
    }

    /** To be called when the transition into the layout is done. */
    public void doneShowing() {
        if (mLayoutState != LayoutState.STARTING_TO_SHOW) return;

        mLayoutState = LayoutState.SHOWING;
        mUpdateHost.doneShowing();
    }

    /**
     * To be called when the transition out of the view mode is done.
     * This is currently called by the renderer when all the animation are done while hiding.
     */
    public void doneHiding() {
        if (mLayoutState != LayoutState.STARTING_TO_HIDE) return;

        mLayoutState = LayoutState.HIDDEN;
        mUpdateHost.doneHiding();
        if (mRenderHost != null && mRenderHost.getResourceManager() != null) {
            mRenderHost.getResourceManager().clearTintedResourceCache();
        }

        if (getSceneLayer() != null) getSceneLayer().removeFromParent();
    }

    /**
     * Initialize the layout to be shown.
     *
     * @param time The current time of the app in ms.
     * @param animate Whether to play an entry animation.
     */
    public void show(long time, boolean animate) {
        // TODO(crbug.com/40141330): Remove after LayoutManager explicitly hide the old layout.
        mLayoutState = LayoutState.STARTING_TO_SHOW;
    }

    /**
     * Hands the layout an Android view to attach it's views to.
     * @param container The Android View to attach the layout's views to.
     */
    public void attachViews(ViewGroup container) {}

    /** Signal to the Layout to detach it's views from the container. */
    public void detachViews() {}

    /** Forces the current animation to finish and broadcasts the proper event. */
    protected void forceAnimationToFinish() {}

    /**
     * @return The width of the drawing area in dp.
     */
    public float getWidth() {
        return mWidthDp;
    }

    /**
     * @return The height of the drawing area in dp.
     */
    public float getHeight() {
        return mHeightDp;
    }

    /**
     * @see Orientation
     * @return The orientation of the screen (portrait or landscape). Values are defined by
     *         {@link Orientation}.
     */
    public @Orientation int getOrientation() {
        return mCurrentOrientation;
    }

    /**
     * Initializes a {@link LayoutTab} with data from the {@link LayoutUpdateHost}. This function
     * eventually needs to be called but may be overridden to manage the posting traffic.
     *
     * @param layoutTab The {@link LayoutTab} To initialize from a
     *                  {@link Tab} on the UI thread.
     * @return          Whether the asynchronous initialization of the {@link LayoutTab} has really
     *                  been posted.
     */
    protected boolean initLayoutTabFromHost(LayoutTab layoutTab) {
        if (layoutTab.isInitFromHostNeeded()) {
            mUpdateHost.initLayoutTabFromHost(layoutTab.getId());
            return true;
        }
        return false;
    }

    /** Called by the LayoutManager when an animation should be killed. */
    public void unstallImmediately() {}

    /**
     * Called by the LayoutManager when an animation should be killed.
     * @param tabId The tab that the kill signal is associated with
     */
    public void unstallImmediately(int tabId) {}

    /**
     * Called by the LayoutManager when they system back button is pressed.
     * @return Whether or not the layout consumed the event.
     */
    public boolean onBackPressed() {
        return false;
    }

    /**
     * Called when a tab get selected. Typically when a tab get closed and the new current tab get
     * selected.
     * @param time      The current time of the app in ms.
     * @param tabId     The id of the selected tab.
     * @param prevId    The id of the previously selected tab.
     * @param incognito Whether or not the affected model was incognito.
     */
    public void onTabSelected(long time, int tabId, int prevId, boolean incognito) {}

    /**
     * Called when a tab is being closed. When called, the closing tab will not
     * be part of the model.
     * @param time      The current time of the app in ms.
     * @param tabId     The id of the tab being closed.
     * @param nextTabId The id if the tab that is being switched to.
     * @param incognito Whether or not the affected model was incognito.
     */
    public void onTabClosed(long time, int tabId, int nextTabId, boolean incognito) {}

    /**
     * Called when all the tabs in the current stack will be closed.
     * When called, the tabs will still be part of the model.
     * @param incognito True if this is the incognito tab model.
     */
    public void onTabsAllClosing(boolean incognito) {}

    /**
     * Called before a tab is created from the top left button.
     *
     * @param sourceTabId The id of the source tab.
     */
    public void onTabCreating(int sourceTabId) {}

    /**
     * Called when a tab is created from the top left button.
     * @param time           The current time of the app in ms.
     * @param tabId          The id of the newly created tab.
     * @param tabIndex       The index of the newly created tab.
     * @param sourceTabId    The id of the source tab.
     * @param newIsIncognito Whether the new tab is incognito.
     * @param background     Whether the tab is created in the background.
     * @param originX        The X screen coordinate in dp of the last touch down event that spawned
     *                       this tab.
     * @param originY        The Y screen coordinate in dp of the last touch down event that spawned
     *                       this tab.
     */
    public void onTabCreated(
            long time,
            int tabId,
            int tabIndex,
            int sourceTabId,
            boolean newIsIncognito,
            boolean background,
            float originX,
            float originY) {}

    /**
     * Called when a tab is restored (created FROM_RESTORE).
     * @param time  The current time of the app in ms.
     * @param tabId The id of the restored tab.
     */
    public void onTabRestored(long time, int tabId) {}

    /**
     * Called when the current tabModel switched (e.g. standard -> incognito).
     *
     * @param incognito True if the new model is incognito.
     */
    public void onTabModelSwitched(boolean incognito) {}

    /**
     * Called when a tab is finally closed if the action was previously undoable.
     * @param time      The current time of the app in ms.
     * @param id        The id of the Tab.
     * @param incognito True if the tab is incognito
     */
    public void onTabClosureCommitted(long time, int id, boolean incognito) {}

    /**
     * Steps the animation forward and updates all the animated values.
     * @param time      The current time of the app in ms.
     * @param jumpToEnd Whether to finish the animation.
     * @return          Whether the animation was finished.
     */
    protected boolean onUpdateAnimation(long time, boolean jumpToEnd) {
        return true;
    }

    /**
     * @return The {@link LayoutTab}s to be drawn.
     */
    public LayoutTab[] getLayoutTabsToRender() {
        return mLayoutTabs;
    }

    /**
     * @return Whether the layout is handling the model updates when a tab is closing.
     */
    public boolean handlesTabClosing() {
        return false;
    }

    /**
     * @return Whether the layout is handling the model updates when a tab is creating.
     */
    public boolean handlesTabCreating() {
        if (mLayoutTabs == null || mLayoutTabs.length != 1) return false;
        return false;
    }

    /**
     * @return True if the content decoration layer should be shown.
     */
    public boolean shouldDisplayContentOverlay() {
        return false;
    }

    /**
     * @return True if the host container can set itself as focusable e.g. for accessibility.
     *         Subclasses can override e.g. to provide a different default focused view.
     */
    public boolean canHostBeFocusable() {
        return true;
    }

    /**
     * @param e                 The {@link MotionEvent} to consider.
     * @param offsets           The current motion offsets that should be applied to the
     *                          {@link EventFilter}s.
     * @param isKeyboardShowing Whether or not the keyboard is showing.
     * @return The {@link EventFilter} the {@link Layout} is listening to.
     */
    public EventFilter findInterceptingEventFilter(
            MotionEvent e, PointF offsets, boolean isKeyboardShowing) {
        EventFilter layoutEventFilter = getEventFilter();
        if (layoutEventFilter != null) {
            if (offsets != null) {
                layoutEventFilter.setCurrentMotionEventOffsets(offsets.x, offsets.y);
            }
            if (layoutEventFilter.onInterceptTouchEvent(e, isKeyboardShowing)) {
                return layoutEventFilter;
            }
            if (layoutEventFilter.onInterceptHoverEvent(e)) {
                return layoutEventFilter;
            }
        }
        return null;
    }

    /**
     * Build a {@link SceneLayer} if it hasn't already been built, and update it and return it.
     *
     * @param viewport          A viewport in which to display content in px.
     * @param visibleViewport   The visible section of the viewport in px.
     * @param tabContentManager A tab content manager.
     * @param resourceManager   A resource manager.
     * @param browserControls   A browser controls state provider.
     * @return                  A {@link SceneLayer} that represents the content for this
     *                          {@link Layout}.
     */
    public final SceneLayer getUpdatedSceneLayer(
            RectF viewport,
            RectF visibleViewport,
            TabContentManager tabContentManager,
            ResourceManager resourceManager,
            BrowserControlsStateProvider browserControls) {
        updateSceneLayer(
                viewport, visibleViewport, tabContentManager, resourceManager, browserControls);
        return getSceneLayer();
    }

    /**
     * @return Whether or not to force the browser controls Android view to hide.
     */
    public boolean forceHideBrowserControlsAndroidView() {
        return false;
    }

    /**
     * @return Whether or not the layout should permanently show the browser controls.
     */
    public boolean forceShowBrowserControlsAndroidView() {
        return false;
    }

    /**
     * @return The EventFilter to use for processing events for this Layout.
     */
    protected abstract EventFilter getEventFilter();

    /**
     * Get an instance of {@link SceneLayer}. Any class inheriting {@link Layout}
     * should override this function in order for other functions to work.
     *
     * @return The scene layer for this {@link Layout}.
     */
    protected abstract SceneLayer getSceneLayer();

    /**
     * Update {@link SceneLayer} instance this layout holds. Any class inheriting {@link Layout}
     * should override this function in order for other functions to work.
     */
    protected void updateSceneLayer(
            RectF viewport,
            RectF contentViewport,
            TabContentManager tabContentManager,
            ResourceManager resourceManager,
            BrowserControlsStateProvider browserControls) {}

    /**
     * @return The {@link LayoutType}.
     */
    public abstract @LayoutType int getLayoutType();

    /** Returns whether the layout is currently running animations. */
    public boolean isRunningAnimations() {
        return false;
    }
}