/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package androidx.leanback.app;

import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.ViewTreeObserver;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentManager.BackStackEntry;
import androidx.fragment.app.FragmentTransaction;
import androidx.leanback.R;
import androidx.leanback.transition.TransitionHelper;
import androidx.leanback.transition.TransitionListener;
import androidx.leanback.util.StateMachine.Event;
import androidx.leanback.util.StateMachine.State;
import androidx.leanback.widget.BrowseFrameLayout;
import androidx.leanback.widget.InvisibleRowPresenter;
import androidx.leanback.widget.ListRow;
import androidx.leanback.widget.ObjectAdapter;
import androidx.leanback.widget.OnItemViewClickedListener;
import androidx.leanback.widget.OnItemViewSelectedListener;
import androidx.leanback.widget.PageRow;
import androidx.leanback.widget.Presenter;
import androidx.leanback.widget.PresenterSelector;
import androidx.leanback.widget.Row;
import androidx.leanback.widget.RowHeaderPresenter;
import androidx.leanback.widget.RowPresenter;
import androidx.leanback.widget.ScaleFrameLayout;
import androidx.leanback.widget.TitleViewAdapter;
import androidx.leanback.widget.VerticalGridView;
import androidx.recyclerview.widget.RecyclerView;

import java.util.HashMap;
import java.util.Map;

/**
 * A fragment for creating Leanback browse screens. It is composed of a
 * RowsSupportFragment and a HeadersSupportFragment.
 * <p>
 * A BrowseSupportFragment renders the elements of its {@link ObjectAdapter} as a set
 * of rows in a vertical list. The elements in this adapter must be subclasses
 * of {@link Row}.
 * <p>
 * The HeadersSupportFragment can be set to be either shown or hidden by default, or
 * may be disabled entirely. See {@link #setHeadersState} for details.
 * <p>
 * By default the BrowseSupportFragment includes support for returning to the headers
 * when the user presses Back. For Activities that customize {@link
 * FragmentActivity#onBackPressed()}, you must disable this default Back key support by
 * calling {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and
 * use {@link BrowseSupportFragment.BrowseTransitionListener} and
 * {@link #startHeadersTransition(boolean)}.
 * <p>
 * The recommended theme to use with a BrowseSupportFragment is
 * {@link androidx.leanback.R.style#Theme_Leanback_Browse}.
 * </p>
 */
public class BrowseSupportFragment extends BaseSupportFragment {

    // BUNDLE attribute for saving header show/hide status when backstack is used:
    static final String HEADER_STACK_INDEX = "headerStackIndex";
    // BUNDLE attribute for saving header show/hide status when backstack is not used:
    static final String HEADER_SHOW = "headerShow";
    private static final String IS_PAGE_ROW = "isPageRow";
    private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition";

    /**
     * State to hide headers fragment.
     */
    final State STATE_SET_ENTRANCE_START_STATE = new State("SET_ENTRANCE_START_STATE") {
        @Override
        public void run() {
            setEntranceTransitionStartState();
        }
    };

    /**
     * Event for Header fragment view is created, we could perform
     * {@link #setEntranceTransitionStartState()} to hide headers fragment initially.
     */
    final Event EVT_HEADER_VIEW_CREATED = new Event("headerFragmentViewCreated");

    /**
     * Event for {@link #getMainFragment()} view is created, it's additional requirement to execute
     * {@link #onEntranceTransitionPrepare()}.
     */
    final Event EVT_MAIN_FRAGMENT_VIEW_CREATED = new Event("mainFragmentViewCreated");

    /**
     * Event that data for the screen is ready, this is additional requirement to launch entrance
     * transition.
     */
    final Event EVT_SCREEN_DATA_READY = new Event("screenDataReady");

    @Override
    void createStateMachineStates() {
        super.createStateMachineStates();
        mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
    }

    @Override
    void createStateMachineTransitions() {
        super.createStateMachineTransitions();
        // when headers fragment view is created we could setEntranceTransitionStartState()
        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, STATE_SET_ENTRANCE_START_STATE,
                EVT_HEADER_VIEW_CREATED);

        // add additional requirement for onEntranceTransitionPrepare()
        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
                STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
                EVT_MAIN_FRAGMENT_VIEW_CREATED);
        // add additional requirement to launch entrance transition.
        mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,  STATE_ENTRANCE_PERFORM,
                EVT_SCREEN_DATA_READY);
    }

    final class BackStackListener implements FragmentManager.OnBackStackChangedListener {
        int mLastEntryCount;
        int mIndexOfHeadersBackStack;

        BackStackListener() {
            mLastEntryCount = getFragmentManager().getBackStackEntryCount();
            mIndexOfHeadersBackStack = -1;
        }

        void load(Bundle savedInstanceState) {
            if (savedInstanceState != null) {
                mIndexOfHeadersBackStack = savedInstanceState.getInt(HEADER_STACK_INDEX, -1);
                mShowingHeaders = mIndexOfHeadersBackStack == -1;
            } else {
                if (!mShowingHeaders) {
                    getFragmentManager().beginTransaction()
                            .addToBackStack(mWithHeadersBackStackName).commit();
                }
            }
        }

        void save(Bundle outState) {
            outState.putInt(HEADER_STACK_INDEX, mIndexOfHeadersBackStack);
        }


        @Override
        public void onBackStackChanged() {
            if (getFragmentManager() == null) {
                Log.w(TAG, "getFragmentManager() is null, stack:", new Exception());
                return;
            }
            int count = getFragmentManager().getBackStackEntryCount();
            // if backstack is growing and last pushed entry is "headers" backstack,
            // remember the index of the entry.
            if (count > mLastEntryCount) {
                BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1);
                if (mWithHeadersBackStackName.equals(entry.getName())) {
                    mIndexOfHeadersBackStack = count - 1;
                }
            } else if (count < mLastEntryCount) {
                // if popped "headers" backstack, initiate the show header transition if needed
                if (mIndexOfHeadersBackStack >= count) {
                    if (!isHeadersDataReady()) {
                        // if main fragment was restored first before BrowseSupportFragment's adapter gets
                        // restored: don't start header transition, but add the entry back.
                        getFragmentManager().beginTransaction()
                                .addToBackStack(mWithHeadersBackStackName).commit();
                        return;
                    }
                    mIndexOfHeadersBackStack = -1;
                    if (!mShowingHeaders) {
                        startHeadersTransitionInternal(true);
                    }
                }
            }
            mLastEntryCount = count;
        }
    }

    /**
     * Listener for transitions between browse headers and rows.
     */
    public static class BrowseTransitionListener {
        /**
         * Callback when headers transition starts.
         *
         * @param withHeaders True if the transition will result in headers
         *        being shown, false otherwise.
         */
        public void onHeadersTransitionStart(boolean withHeaders) {
        }
        /**
         * Callback when headers transition stops.
         *
         * @param withHeaders True if the transition will result in headers
         *        being shown, false otherwise.
         */
        public void onHeadersTransitionStop(boolean withHeaders) {
        }
    }

    private final class SetSelectionRunnable implements Runnable {
        static final int TYPE_INVALID = -1;
        static final int TYPE_INTERNAL_SYNC = 0;
        static final int TYPE_USER_REQUEST = 1;

        private int mPosition;
        private int mType;
        private boolean mSmooth;

        SetSelectionRunnable() {
            reset();
        }

        void post(int position, int type, boolean smooth) {
            // Posting the set selection, rather than calling it immediately, prevents an issue
            // with adapter changes.  Example: a row is added before the current selected row;
            // first the fast lane view updates its selection, then the rows fragment has that
            // new selection propagated immediately; THEN the rows view processes the same adapter
            // change and moves the selection again.
            if (type >= mType) {
                mPosition = position;
                mType = type;
                mSmooth = smooth;
                mBrowseFrame.removeCallbacks(this);
                if (!mStopped) {
                    mBrowseFrame.post(this);
                }
            }
        }

        @Override
        public void run() {
            setSelection(mPosition, mSmooth);
            reset();
        }

        public void stop() {
            // remove possible callback when stop, it will be re-added in start().
            mBrowseFrame.removeCallbacks(this);
        }

        public void start() {
            if (mType != TYPE_INVALID) {
                mBrowseFrame.post(this);
            }
        }

        private void reset() {
            mPosition = -1;
            mType = TYPE_INVALID;
            mSmooth = false;
        }
    }

    /**
     * Possible set of actions that {@link BrowseSupportFragment} exposes to clients. Custom
     * fragments can interact with {@link BrowseSupportFragment} using this interface.
     */
    public interface FragmentHost {
        /**
         * Fragments are required to invoke this callback once their view is created
         * inside {@link Fragment#onViewCreated} method. {@link BrowseSupportFragment} starts the entrance
         * animation only after receiving this callback. Failure to invoke this method
         * will lead to fragment not showing up.
         *
         * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment.
         */
        void notifyViewCreated(MainFragmentAdapter fragmentAdapter);

        /**
         * Fragments mapped to {@link PageRow} are required to invoke this callback once their data
         * is created for transition, the entrance animation only after receiving this callback.
         * Failure to invoke this method will lead to fragment not showing up.
         *
         * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment.
         */
        void notifyDataReady(MainFragmentAdapter fragmentAdapter);

        /**
         * Show or hide title view in {@link BrowseSupportFragment} for fragments mapped to
         * {@link PageRow}.  Otherwise the request is ignored, in that case BrowseSupportFragment is fully
         * in control of showing/hiding title view.
         * <p>
         * When HeadersSupportFragment is visible, BrowseSupportFragment will hide search affordance view if
         * there are other focusable rows above currently focused row.
         *
         * @param show Boolean indicating whether or not to show the title view.
         */
        void showTitleView(boolean show);
    }

    /**
     * Default implementation of {@link FragmentHost} that is used only by
     * {@link BrowseSupportFragment}.
     */
    private final class FragmentHostImpl implements FragmentHost {
        boolean mShowTitleView = true;

        FragmentHostImpl() {
        }

        @Override
        public void notifyViewCreated(MainFragmentAdapter fragmentAdapter) {
            mStateMachine.fireEvent(EVT_MAIN_FRAGMENT_VIEW_CREATED);
            if (!mIsPageRow) {
                // If it's not a PageRow: it's a ListRow, so we already have data ready.
                mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
            }
        }

        @Override
        public void notifyDataReady(MainFragmentAdapter fragmentAdapter) {
            // If fragment host is not the currently active fragment (in BrowseSupportFragment), then
            // ignore the request.
            if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {
                return;
            }

            // We only honor showTitle request for PageRows.
            if (!mIsPageRow) {
                return;
            }

            mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
        }

        @Override
        public void showTitleView(boolean show) {
            mShowTitleView = show;

            // If fragment host is not the currently active fragment (in BrowseSupportFragment), then
            // ignore the request.
            if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {
                return;
            }

            // We only honor showTitle request for PageRows.
            if (!mIsPageRow) {
                return;
            }

            updateTitleViewVisibility();
        }
    }

    /**
     * Interface that defines the interaction between {@link BrowseSupportFragment} and its main
     * content fragment. The key method is {@link MainFragmentAdapter#getFragment()},
     * it will be used to get the fragment to be shown in the content section. Clients can
     * provide any implementation of fragment and customize its interaction with
     * {@link BrowseSupportFragment} by overriding the necessary methods.
     *
     * <p>
     * Clients are expected to provide
     * an instance of {@link MainFragmentAdapterRegistry} which will be responsible for providing
     * implementations of {@link MainFragmentAdapter} for given content types. Currently
     * we support different types of content - {@link ListRow}, {@link PageRow} or any subtype
     * of {@link Row}. We provide an out of the box adapter implementation for any rows other than
     * {@link PageRow} - {@link androidx.leanback.app.RowsSupportFragment.MainFragmentAdapter}.
     *
     * <p>
     * {@link PageRow} is intended to give full flexibility to developers in terms of Fragment
     * design. Users will have to provide an implementation of {@link MainFragmentAdapter}
     * and provide that through {@link MainFragmentAdapterRegistry}.
     * {@link MainFragmentAdapter} implementation can supply any fragment and override
     * just those interactions that makes sense.
     */
    public static class MainFragmentAdapter<T extends Fragment> {
        private boolean mScalingEnabled;
        private final T mFragment;
        FragmentHostImpl mFragmentHost;

        public MainFragmentAdapter(T fragment) {
            this.mFragment = fragment;
        }

        public final T getFragment() {
            return mFragment;
        }

        /**
         * Returns whether its scrolling.
         */
        public boolean isScrolling() {
            return false;
        }

        /**
         * Set the visibility of titles/hover card of browse rows.
         */
        public void setExpand(boolean expand) {
        }

        /**
         * For rows that willing to participate entrance transition,  this function
         * hide views if afterTransition is true,  show views if afterTransition is false.
         */
        public void setEntranceTransitionState(boolean state) {
        }

        /**
         * Sets the window alignment and also the pivots for scale operation.
         */
        public void setAlignment(int windowAlignOffsetFromTop) {
        }

        /**
         * Callback indicating transition prepare start.
         */
        public boolean onTransitionPrepare() {
            return false;
        }

        /**
         * Callback indicating transition start.
         */
        public void onTransitionStart() {
        }

        /**
         * Callback indicating transition end.
         */
        public void onTransitionEnd() {
        }

        /**
         * Returns whether row scaling is enabled.
         */
        public boolean isScalingEnabled() {
            return mScalingEnabled;
        }

        /**
         * Sets the row scaling property.
         */
        public void setScalingEnabled(boolean scalingEnabled) {
            this.mScalingEnabled = scalingEnabled;
        }

        /**
         * Returns the current host interface so that main fragment can interact with
         * {@link BrowseSupportFragment}.
         */
        public final FragmentHost getFragmentHost() {
            return mFragmentHost;
        }

        void setFragmentHost(FragmentHostImpl fragmentHost) {
            this.mFragmentHost = fragmentHost;
        }
    }

    /**
     * Interface to be implemented by all fragments for providing an instance of
     * {@link MainFragmentAdapter}. Both {@link RowsSupportFragment} and custom fragment provided
     * against {@link PageRow} will need to implement this interface.
     */
    public interface MainFragmentAdapterProvider {
        /**
         * Returns an instance of {@link MainFragmentAdapter} that {@link BrowseSupportFragment}
         * would use to communicate with the target fragment.
         */
        MainFragmentAdapter getMainFragmentAdapter();
    }

    /**
     * Interface to be implemented by {@link RowsSupportFragment} and its subclasses for providing
     * an instance of {@link MainFragmentRowsAdapter}.
     */
    public interface MainFragmentRowsAdapterProvider {
        /**
         * Returns an instance of {@link MainFragmentRowsAdapter} that {@link BrowseSupportFragment}
         * would use to communicate with the target fragment.
         */
        MainFragmentRowsAdapter getMainFragmentRowsAdapter();
    }

    /**
     * This is used to pass information to {@link RowsSupportFragment} or its subclasses.
     * {@link BrowseSupportFragment} uses this interface to pass row based interaction events to
     * the target fragment.
     */
    public static class MainFragmentRowsAdapter<T extends Fragment> {
        private final T mFragment;

        public MainFragmentRowsAdapter(T fragment) {
            if (fragment == null) {
                throw new IllegalArgumentException("Fragment can't be null");
            }
            this.mFragment = fragment;
        }

        public final T getFragment() {
            return mFragment;
        }
        /**
         * Set the visibility titles/hover of browse rows.
         */
        public void setAdapter(ObjectAdapter adapter) {
        }

        /**
         * Sets an item clicked listener on the fragment.
         */
        public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
        }

        /**
         * Sets an item selection listener.
         */
        public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
        }

        /**
         * Selects a Row and perform an optional task on the Row.
         */
        public void setSelectedPosition(int rowPosition,
                                        boolean smooth,
                                        final Presenter.ViewHolderTask rowHolderTask) {
        }

        /**
         * Selects a Row.
         */
        public void setSelectedPosition(int rowPosition, boolean smooth) {
        }

        /**
         * @return The position of selected row.
         */
        public int getSelectedPosition() {
            return 0;
        }

        /**
         * @param position Position of Row.
         * @return Row ViewHolder.
         */
        public RowPresenter.ViewHolder findRowViewHolderByPosition(int position) {
            return null;
        }
    }

    private boolean createMainFragment(ObjectAdapter adapter, int position) {
        Object item = null;
        if (!mCanShowHeaders) {
            // when header is disabled, we can decide to use RowsSupportFragment even no data.
        } else if (adapter == null || adapter.size() == 0) {
            return false;
        } else {
            if (position < 0) {
                position = 0;
            } else if (position >= adapter.size()) {
                throw new IllegalArgumentException(
                        String.format("Invalid position %d requested", position));
            }
            item = adapter.get(position);
        }

        boolean oldIsPageRow = mIsPageRow;
        Object oldPageRow = mPageRow;
        mIsPageRow = mCanShowHeaders && item instanceof PageRow;
        mPageRow = mIsPageRow ? item : null;
        boolean swap;

        if (mMainFragment == null) {
            swap = true;
        } else {
            if (oldIsPageRow) {
                if (mIsPageRow) {
                    if (oldPageRow == null) {
                        // fragment is restored, page row object not yet set, so just set the
                        // mPageRow object and there is no need to replace the fragment
                        swap = false;
                    } else {
                        // swap if page row object changes
                        swap = oldPageRow != mPageRow;
                    }
                } else {
                    swap = true;
                }
            } else {
                swap = mIsPageRow;
            }
        }

        if (swap) {
            mMainFragment = mMainFragmentAdapterRegistry.createFragment(item);
            if (!(mMainFragment instanceof MainFragmentAdapterProvider)) {
                throw new IllegalArgumentException(
                        "Fragment must implement MainFragmentAdapterProvider");
            }

            setMainFragmentAdapter();
        }

        return swap;
    }

    void setMainFragmentAdapter() {
        mMainFragmentAdapter = ((MainFragmentAdapterProvider) mMainFragment)
                .getMainFragmentAdapter();
        mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
        if (!mIsPageRow) {
            if (mMainFragment instanceof MainFragmentRowsAdapterProvider) {
                setMainFragmentRowsAdapter(((MainFragmentRowsAdapterProvider) mMainFragment)
                        .getMainFragmentRowsAdapter());
            } else {
                setMainFragmentRowsAdapter(null);
            }
            mIsPageRow = mMainFragmentRowsAdapter == null;
        } else {
            setMainFragmentRowsAdapter(null);
        }
    }

    /**
     * Factory class responsible for creating fragment given the current item. {@link ListRow}
     * should return {@link RowsSupportFragment} or its subclass whereas {@link PageRow}
     * can return any fragment class.
     */
    public abstract static class FragmentFactory<T extends Fragment> {
        public abstract T createFragment(Object row);
    }

    /**
     * FragmentFactory implementation for {@link ListRow}.
     */
    public static class ListRowFragmentFactory extends FragmentFactory<RowsSupportFragment> {
        @Override
        public RowsSupportFragment createFragment(Object row) {
            return new RowsSupportFragment();
        }
    }

    /**
     * Registry class maintaining the mapping of {@link Row} subclasses to {@link FragmentFactory}.
     * BrowseRowFragment automatically registers {@link ListRowFragmentFactory} for
     * handling {@link ListRow}. Developers can override that and also if they want to
     * use custom fragment, they can register a custom {@link FragmentFactory}
     * against {@link PageRow}.
     */
    public final static class MainFragmentAdapterRegistry {
        private final Map<Class<?>, FragmentFactory> mItemToFragmentFactoryMapping =
                new HashMap<>();
        private final static FragmentFactory sDefaultFragmentFactory = new ListRowFragmentFactory();

        public MainFragmentAdapterRegistry() {
            registerFragment(ListRow.class, sDefaultFragmentFactory);
        }

        public void registerFragment(Class<?> rowClass, FragmentFactory factory) {
            mItemToFragmentFactoryMapping.put(rowClass, factory);
        }

        public Fragment createFragment(Object item) {
            FragmentFactory fragmentFactory = item == null ? sDefaultFragmentFactory :
                    mItemToFragmentFactoryMapping.get(item.getClass());
            if (fragmentFactory == null && !(item instanceof PageRow)) {
                fragmentFactory = sDefaultFragmentFactory;
            }

            return fragmentFactory.createFragment(item);
        }
    }

    static final String TAG = "BrowseSupportFragment";

    private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_";

    static final boolean DEBUG = false;

    /** The headers fragment is enabled and shown by default. */
    public static final int HEADERS_ENABLED = 1;

    /** The headers fragment is enabled and hidden by default. */
    public static final int HEADERS_HIDDEN = 2;

    /** The headers fragment is disabled and will never be shown. */
    public static final int HEADERS_DISABLED = 3;

    private MainFragmentAdapterRegistry mMainFragmentAdapterRegistry =
            new MainFragmentAdapterRegistry();
    MainFragmentAdapter mMainFragmentAdapter;
    Fragment mMainFragment;
    HeadersSupportFragment mHeadersSupportFragment;
    MainFragmentRowsAdapter mMainFragmentRowsAdapter;
    ListRowDataAdapter mMainFragmentListRowDataAdapter;

    private ObjectAdapter mAdapter;
    private PresenterSelector mAdapterPresenter;

    private int mHeadersState = HEADERS_ENABLED;
    private int mBrandColor = Color.TRANSPARENT;
    private boolean mBrandColorSet;

    BrowseFrameLayout mBrowseFrame;
    private ScaleFrameLayout mScaleFrameLayout;
    boolean mHeadersBackStackEnabled = true;
    String mWithHeadersBackStackName;
    boolean mShowingHeaders = true;
    boolean mCanShowHeaders = true;
    private int mContainerListMarginStart;
    private int mContainerListAlignTop;
    private boolean mMainFragmentScaleEnabled = true;
    OnItemViewSelectedListener mExternalOnItemViewSelectedListener;
    private OnItemViewClickedListener mOnItemViewClickedListener;
    private int mSelectedPosition = -1;
    private float mScaleFactor;
    boolean mIsPageRow;
    Object mPageRow;
    boolean mStopped = true;

    private PresenterSelector mHeaderPresenterSelector;
    private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();

    // transition related:
    Object mSceneWithHeaders;
    Object mSceneWithoutHeaders;
    private Object mSceneAfterEntranceTransition;
    Object mHeadersTransition;
    BackStackListener mBackStackChangedListener;
    BrowseTransitionListener mBrowseTransitionListener;

    private static final String ARG_TITLE = BrowseSupportFragment.class.getCanonicalName() + ".title";
    private static final String ARG_HEADERS_STATE =
        BrowseSupportFragment.class.getCanonicalName() + ".headersState";

    /**
     * Creates arguments for a browse fragment.
     *
     * @param args The Bundle to place arguments into, or null if the method
     *        should return a new Bundle.
     * @param title The title of the BrowseSupportFragment.
     * @param headersState The initial state of the headers of the
     *        BrowseSupportFragment. Must be one of {@link #HEADERS_ENABLED}, {@link
     *        #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}.
     * @return A Bundle with the given arguments for creating a BrowseSupportFragment.
     */
    public static Bundle createArgs(Bundle args, String title, int headersState) {
        if (args == null) {
            args = new Bundle();
        }
        args.putString(ARG_TITLE, title);
        args.putInt(ARG_HEADERS_STATE, headersState);
        return args;
    }

    /**
     * Sets the brand color for the browse fragment. The brand color is used as
     * the primary color for UI elements in the browse fragment. For example,
     * the background color of the headers fragment uses the brand color.
     *
     * @param color The color to use as the brand color of the fragment.
     */
    public void setBrandColor(@ColorInt int color) {
        mBrandColor = color;
        mBrandColorSet = true;

        if (mHeadersSupportFragment != null) {
            mHeadersSupportFragment.setBackgroundColor(mBrandColor);
        }
    }

    /**
     * Returns the brand color for the browse fragment.
     * The default is transparent.
     */
    @ColorInt
    public int getBrandColor() {
        return mBrandColor;
    }

    /**
     * Wrapping app provided PresenterSelector to support InvisibleRowPresenter for SectionRow
     * DividerRow and PageRow.
     */
    private void updateWrapperPresenter() {
        if (mAdapter == null) {
            mAdapterPresenter = null;
            return;
        }
        final PresenterSelector adapterPresenter = mAdapter.getPresenterSelector();
        if (adapterPresenter == null) {
            throw new IllegalArgumentException("Adapter.getPresenterSelector() is null");
        }
        if (adapterPresenter == mAdapterPresenter) {
            return;
        }
        mAdapterPresenter = adapterPresenter;

        Presenter[] presenters = adapterPresenter.getPresenters();
        final Presenter invisibleRowPresenter = new InvisibleRowPresenter();
        final Presenter[] allPresenters = new Presenter[presenters.length + 1];
        System.arraycopy(allPresenters, 0, presenters, 0, presenters.length);
        allPresenters[allPresenters.length - 1] = invisibleRowPresenter;
        mAdapter.setPresenterSelector(new PresenterSelector() {
            @Override
            public Presenter getPresenter(@Nullable Object item) {
                Row row = (Row) item;
                if (row.isRenderedAsRowView()) {
                    return adapterPresenter.getPresenter(item);
                } else {
                    return invisibleRowPresenter;
                }
            }

            @Override
            public Presenter[] getPresenters() {
                return allPresenters;
            }
        });
    }

    /**
     * Sets the adapter containing the rows for the fragment.
     *
     * <p>The items referenced by the adapter must be be derived from
     * {@link Row}. These rows will be used by the rows fragment and the headers
     * fragment (if not disabled) to render the browse rows.
     *
     * @param adapter An ObjectAdapter for the browse rows. All items must
     *        derive from {@link Row}.
     */
    public void setAdapter(ObjectAdapter adapter) {
        mAdapter = adapter;
        updateWrapperPresenter();
        if (getView() == null) {
            return;
        }

        updateMainFragmentRowsAdapter();
        mHeadersSupportFragment.setAdapter(mAdapter);
    }

    void setMainFragmentRowsAdapter(MainFragmentRowsAdapter mainFragmentRowsAdapter) {
        if (mainFragmentRowsAdapter == mMainFragmentRowsAdapter) {
            return;
        }
        // first clear previous mMainFragmentRowsAdapter and set a new mMainFragmentRowsAdapter
        if (mMainFragmentRowsAdapter != null) {
            // RowsFragment cannot change click/select listeners after view created.
            // The main fragment and adapter should be GCed as long as there is no reference from
            // BrowseSupportFragment to it.
            mMainFragmentRowsAdapter.setAdapter(null);
        }
        mMainFragmentRowsAdapter = mainFragmentRowsAdapter;
        if (mMainFragmentRowsAdapter != null) {
            mMainFragmentRowsAdapter.setOnItemViewSelectedListener(
                    new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter));
            mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener);
        }
        // second update mMainFragmentListRowDataAdapter set on mMainFragmentRowsAdapter
        updateMainFragmentRowsAdapter();
    }

    /**
     * Update mMainFragmentListRowDataAdapter and set it on mMainFragmentRowsAdapter.
     * It also clears old mMainFragmentListRowDataAdapter.
     */
    void updateMainFragmentRowsAdapter() {
        if (mMainFragmentListRowDataAdapter != null) {
            mMainFragmentListRowDataAdapter.detach();
            mMainFragmentListRowDataAdapter = null;
        }
        if (mMainFragmentRowsAdapter != null) {
            mMainFragmentListRowDataAdapter = mAdapter == null
                    ? null : new ListRowDataAdapter(mAdapter);
            mMainFragmentRowsAdapter.setAdapter(mMainFragmentListRowDataAdapter);
        }
    }

    public final MainFragmentAdapterRegistry getMainFragmentRegistry() {
        return mMainFragmentAdapterRegistry;
    }

    /**
     * Returns the adapter containing the rows for the fragment.
     */
    public ObjectAdapter getAdapter() {
        return mAdapter;
    }

    /**
     * Sets an item selection listener.
     */
    public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
        mExternalOnItemViewSelectedListener = listener;
    }

    /**
     * Returns an item selection listener.
     */
    public OnItemViewSelectedListener getOnItemViewSelectedListener() {
        return mExternalOnItemViewSelectedListener;
    }

    /**
     * Get RowsSupportFragment if it's bound to BrowseSupportFragment or null if either BrowseSupportFragment has
     * not been created yet or a different fragment is bound to it.
     *
     * @return RowsSupportFragment if it's bound to BrowseSupportFragment or null otherwise.
     */
    public RowsSupportFragment getRowsSupportFragment() {
        if (mMainFragment instanceof RowsSupportFragment) {
            return (RowsSupportFragment) mMainFragment;
        }

        return null;
    }

    /**
     * @return Current main fragment or null if not created.
     */
    public Fragment getMainFragment() {
        return mMainFragment;
    }

    /**
     * Get currently bound HeadersSupportFragment or null if HeadersSupportFragment has not been created yet.
     * @return Currently bound HeadersSupportFragment or null if HeadersSupportFragment has not been created yet.
     */
    public HeadersSupportFragment getHeadersSupportFragment() {
        return mHeadersSupportFragment;
    }

    /**
     * Sets an item clicked listener on the fragment.
     * OnItemViewClickedListener will override {@link View.OnClickListener} that
     * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
     * So in general, developer should choose one of the listeners but not both.
     */
    public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
        mOnItemViewClickedListener = listener;
        if (mMainFragmentRowsAdapter != null) {
            mMainFragmentRowsAdapter.setOnItemViewClickedListener(listener);
        }
    }

    /**
     * Returns the item Clicked listener.
     */
    public OnItemViewClickedListener getOnItemViewClickedListener() {
        return mOnItemViewClickedListener;
    }

    /**
     * Starts a headers transition.
     *
     * <p>This method will begin a transition to either show or hide the
     * headers, depending on the value of withHeaders. If headers are disabled
     * for this browse fragment, this method will throw an exception.
     *
     * @param withHeaders True if the headers should transition to being shown,
     *        false if the transition should result in headers being hidden.
     */
    public void startHeadersTransition(boolean withHeaders) {
        if (!mCanShowHeaders) {
            throw new IllegalStateException("Cannot start headers transition");
        }
        if (isInHeadersTransition() || mShowingHeaders == withHeaders) {
            return;
        }
        startHeadersTransitionInternal(withHeaders);
    }

    /**
     * Returns true if the headers transition is currently running.
     */
    public boolean isInHeadersTransition() {
        return mHeadersTransition != null;
    }

    /**
     * Returns true if headers are shown.
     */
    public boolean isShowingHeaders() {
        return mShowingHeaders;
    }

    /**
     * Sets a listener for browse fragment transitions.
     *
     * @param listener The listener to call when a browse headers transition
     *        begins or ends.
     */
    public void setBrowseTransitionListener(BrowseTransitionListener listener) {
        mBrowseTransitionListener = listener;
    }

    /**
     * @deprecated use {@link BrowseSupportFragment#enableMainFragmentScaling(boolean)} instead.
     *
     * @param enable true to enable row scaling
     */
    @Deprecated
    public void enableRowScaling(boolean enable) {
        enableMainFragmentScaling(enable);
    }

    /**
     * Enables scaling of main fragment when headers are present. For the page/row fragment,
     * scaling is enabled only when both this method and
     * {@link MainFragmentAdapter#isScalingEnabled()} are enabled.
     *
     * @param enable true to enable row scaling
     */
    public void enableMainFragmentScaling(boolean enable) {
        mMainFragmentScaleEnabled = enable;
    }

    void startHeadersTransitionInternal(final boolean withHeaders) {
        if (getFragmentManager().isDestroyed()) {
            return;
        }
        if (!isHeadersDataReady()) {
            return;
        }
        mShowingHeaders = withHeaders;
        mMainFragmentAdapter.onTransitionPrepare();
        mMainFragmentAdapter.onTransitionStart();
        onExpandTransitionStart(!withHeaders, new Runnable() {
            @Override
            public void run() {
                mHeadersSupportFragment.onTransitionPrepare();
                mHeadersSupportFragment.onTransitionStart();
                createHeadersTransition();
                if (mBrowseTransitionListener != null) {
                    mBrowseTransitionListener.onHeadersTransitionStart(withHeaders);
                }
                TransitionHelper.runTransition(
                        withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, mHeadersTransition);
                if (mHeadersBackStackEnabled) {
                    if (!withHeaders) {
                        getFragmentManager().beginTransaction()
                                .addToBackStack(mWithHeadersBackStackName).commit();
                    } else {
                        int index = mBackStackChangedListener.mIndexOfHeadersBackStack;
                        if (index >= 0) {
                            BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index);
                            getFragmentManager().popBackStackImmediate(entry.getId(),
                                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
                        }
                    }
                }
            }
        });
    }

    boolean isVerticalScrolling() {
        // don't run transition
        return mHeadersSupportFragment.isScrolling() || mMainFragmentAdapter.isScrolling();
    }


    private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
            new BrowseFrameLayout.OnFocusSearchListener() {
        @Override
        public View onFocusSearch(View focused, int direction) {
            // if headers is running transition,  focus stays
            if (mCanShowHeaders && isInHeadersTransition()) {
                return focused;
            }
            if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);

            if (getTitleView() != null && focused != getTitleView()
                    && direction == View.FOCUS_UP) {
                return getTitleView();
            }
            if (getTitleView() != null && getTitleView().hasFocus()
                    && direction == View.FOCUS_DOWN) {
                return mCanShowHeaders && mShowingHeaders
                        ? mHeadersSupportFragment.getVerticalGridView() : mMainFragment.getView();
            }

            boolean isRtl = ViewCompat.getLayoutDirection(focused)
                    == ViewCompat.LAYOUT_DIRECTION_RTL;
            int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
            int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT;
            if (mCanShowHeaders && direction == towardStart) {
                if (isVerticalScrolling() || mShowingHeaders || !isHeadersDataReady()) {
                    return focused;
                }
                return mHeadersSupportFragment.getVerticalGridView();
            } else if (direction == towardEnd) {
                if (isVerticalScrolling()) {
                    return focused;
                } else if (mMainFragment != null && mMainFragment.getView() != null) {
                    return mMainFragment.getView();
                }
                return focused;
            } else if (direction == View.FOCUS_DOWN && mShowingHeaders) {
                // disable focus_down moving into PageFragment.
                return focused;
            } else {
                return null;
            }
        }
    };

    final boolean isHeadersDataReady() {
        return mAdapter != null && mAdapter.size() != 0;
    }

    private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener =
            new BrowseFrameLayout.OnChildFocusListener() {

        @Override
        public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
            if (getChildFragmentManager().isDestroyed()) {
                return true;
            }
            // Make sure not changing focus when requestFocus() is called.
            if (mCanShowHeaders && mShowingHeaders) {
                if (mHeadersSupportFragment != null && mHeadersSupportFragment.getView() != null
                        && mHeadersSupportFragment.getView().requestFocus(
                                direction, previouslyFocusedRect)) {
                    return true;
                }
            }
            if (mMainFragment != null && mMainFragment.getView() != null
                    && mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
                return true;
            }
            return getTitleView() != null
                    && getTitleView().requestFocus(direction, previouslyFocusedRect);
        }

        @Override
        public void onRequestChildFocus(View child, View focused) {
            if (getChildFragmentManager().isDestroyed()) {
                return;
            }
            if (!mCanShowHeaders || isInHeadersTransition()) return;
            int childId = child.getId();
            if (childId == R.id.browse_container_dock && mShowingHeaders) {
                startHeadersTransitionInternal(false);
            } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {
                startHeadersTransitionInternal(true);
            }
        }
    };

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition);
        outState.putBoolean(IS_PAGE_ROW, mIsPageRow);

        if (mBackStackChangedListener != null) {
            mBackStackChangedListener.save(outState);
        } else {
            outState.putBoolean(HEADER_SHOW, mShowingHeaders);
        }
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final Context context = getContext();
        TypedArray ta = context.obtainStyledAttributes(R.styleable.LeanbackTheme);
        mContainerListMarginStart = (int) ta.getDimension(
                R.styleable.LeanbackTheme_browseRowsMarginStart, context.getResources()
                .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start));
        mContainerListAlignTop = (int) ta.getDimension(
                R.styleable.LeanbackTheme_browseRowsMarginTop, context.getResources()
                .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top));
        ta.recycle();

        readArguments(getArguments());

        if (mCanShowHeaders) {
            if (mHeadersBackStackEnabled) {
                mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this;
                mBackStackChangedListener = new BackStackListener();
                getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener);
                mBackStackChangedListener.load(savedInstanceState);
            } else {
                if (savedInstanceState != null) {
                    mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW);
                }
            }
        }

        mScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1);
    }

    @Override
    public void onDestroyView() {
        setMainFragmentRowsAdapter(null);
        mPageRow = null;
        mMainFragmentAdapter = null;
        mMainFragment = null;
        mHeadersSupportFragment = null;
        mBrowseFrame = null;
        mScaleFrameLayout = null;
        mSceneAfterEntranceTransition = null;
        mSceneWithHeaders = null;
        mSceneWithoutHeaders = null;
        super.onDestroyView();
    }

    @Override
    public void onDestroy() {
        if (mBackStackChangedListener != null) {
            getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener);
        }
        super.onDestroy();
    }

    /**
     * Creates a new {@link HeadersSupportFragment} instance. Subclass of BrowseSupportFragment may override and
     * return an instance of subclass of HeadersSupportFragment, e.g. when app wants to replace presenter
     * to render HeaderItem.
     *
     * @return A new instance of {@link HeadersSupportFragment} or its subclass.
     */
    public HeadersSupportFragment onCreateHeadersSupportFragment() {
        return new HeadersSupportFragment();
    }

    @Override
    @Nullable
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
            @Nullable Bundle savedInstanceState) {

        if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) {
            mHeadersSupportFragment = onCreateHeadersSupportFragment();

            createMainFragment(mAdapter, mSelectedPosition);
            FragmentTransaction ft = getChildFragmentManager().beginTransaction()
                    .replace(R.id.browse_headers_dock, mHeadersSupportFragment);

            if (mMainFragment != null) {
                ft.replace(R.id.scale_frame, mMainFragment);
            } else {
                // Empty adapter used to guard against lazy adapter loading. When this
                // fragment is instantiated, mAdapter might not have the data or might not
                // have been set. In either of those cases mFragmentAdapter will be null.
                // This way we can maintain the invariant that mMainFragmentAdapter is never
                // null and it avoids doing null checks all over the code.
                mMainFragmentAdapter = new MainFragmentAdapter<>(null);
                mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
            }

            ft.commit();
        } else {
            mHeadersSupportFragment = (HeadersSupportFragment) getChildFragmentManager()
                    .findFragmentById(R.id.browse_headers_dock);
            mMainFragment = getChildFragmentManager().findFragmentById(R.id.scale_frame);

            mIsPageRow = savedInstanceState != null
                    && savedInstanceState.getBoolean(IS_PAGE_ROW, false);
            // mPageRow object is unable to restore, if its null and mIsPageRow is true, this is
            // the case for restoring, later if setSelection() triggers a createMainFragment(),
            // should not create fragment.

            mSelectedPosition = savedInstanceState != null
                    ? savedInstanceState.getInt(CURRENT_SELECTED_POSITION, 0) : 0;

            setMainFragmentAdapter();
        }

        mHeadersSupportFragment.setHeadersGone(!mCanShowHeaders);
        if (mHeaderPresenterSelector != null) {
            mHeadersSupportFragment.setPresenterSelector(mHeaderPresenterSelector);
        }
        mHeadersSupportFragment.setAdapter(mAdapter);
        mHeadersSupportFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener);
        mHeadersSupportFragment.setOnHeaderClickedListener(mHeaderClickedListener);

        View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);

        getProgressBarManager().setRootView((ViewGroup)root);

        mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
        mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener);
        mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);

        installTitleView(inflater, mBrowseFrame, savedInstanceState);

        mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame);
        mScaleFrameLayout.setPivotX(0);
        mScaleFrameLayout.setPivotY(mContainerListAlignTop);

        if (mBrandColorSet) {
            mHeadersSupportFragment.setBackgroundColor(mBrandColor);
        }

        mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
            @Override
            public void run() {
                showHeaders(true);
            }
        });
        mSceneWithoutHeaders =  TransitionHelper.createScene(mBrowseFrame, new Runnable() {
            @Override
            public void run() {
                showHeaders(false);
            }
        });
        mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
            @Override
            public void run() {
                setEntranceTransitionEndState();
            }
        });

        return root;
    }

    void createHeadersTransition() {
        mHeadersTransition = TransitionHelper.loadTransition(getContext(),
                mShowingHeaders
                        ? R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out);

        TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() {
            @Override
            public void onTransitionStart(Object transition) {
            }
            @Override
            public void onTransitionEnd(Object transition) {
                mHeadersTransition = null;
                if (mMainFragmentAdapter != null) {
                    mMainFragmentAdapter.onTransitionEnd();
                    if (!mShowingHeaders && mMainFragment != null) {
                        View mainFragmentView = mMainFragment.getView();
                        if (mainFragmentView != null && !mainFragmentView.hasFocus()) {
                            mainFragmentView.requestFocus();
                        }
                    }
                }
                if (mHeadersSupportFragment != null) {
                    mHeadersSupportFragment.onTransitionEnd();
                    if (mShowingHeaders) {
                        VerticalGridView headerGridView = mHeadersSupportFragment.getVerticalGridView();
                        if (headerGridView != null && !headerGridView.hasFocus()) {
                            headerGridView.requestFocus();
                        }
                    }
                }

                // Animate TitleView once header animation is complete.
                updateTitleViewVisibility();

                if (mBrowseTransitionListener != null) {
                    mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders);
                }
            }
        });
    }

    void updateTitleViewVisibility() {
        if (!mShowingHeaders) {
            boolean showTitleView;
            if (mIsPageRow && mMainFragmentAdapter != null) {
                // page fragment case:
                showTitleView = mMainFragmentAdapter.mFragmentHost.mShowTitleView;
            } else {
                // regular row view case:
                showTitleView = isFirstRowWithContent(mSelectedPosition);
            }
            if (showTitleView) {
                showTitle(TitleViewAdapter.FULL_VIEW_VISIBLE);
            } else {
                showTitle(false);
            }
        } else {
            // when HeaderFragment is showing,  showBranding and showSearch are slightly different
            boolean showBranding;
            boolean showSearch;
            if (mIsPageRow && mMainFragmentAdapter != null) {
                showBranding = mMainFragmentAdapter.mFragmentHost.mShowTitleView;
            } else {
                showBranding = isFirstRowWithContent(mSelectedPosition);
            }
            showSearch = isFirstRowWithContentOrPageRow(mSelectedPosition);
            int flags = 0;
            if (showBranding) flags |= TitleViewAdapter.BRANDING_VIEW_VISIBLE;
            if (showSearch) flags |= TitleViewAdapter.SEARCH_VIEW_VISIBLE;
            if (flags != 0) {
                showTitle(flags);
            } else {
                showTitle(false);
            }
        }
    }

    boolean isFirstRowWithContentOrPageRow(int rowPosition) {
        if (mAdapter == null || mAdapter.size() == 0) {
            return true;
        }
        for (int i = 0; i < mAdapter.size(); i++) {
            final Row row = (Row) mAdapter.get(i);
            if (row.isRenderedAsRowView() || row instanceof PageRow) {
                return rowPosition == i;
            }
        }
        return true;
    }

    boolean isFirstRowWithContent(int rowPosition) {
        if (mAdapter == null || mAdapter.size() == 0) {
            return true;
        }
        for (int i = 0; i < mAdapter.size(); i++) {
            final Row row = (Row) mAdapter.get(i);
            if (row.isRenderedAsRowView()) {
                return rowPosition == i;
            }
        }
        return true;
    }

    /**
     * Sets the {@link PresenterSelector} used to render the row headers.
     *
     * @param headerPresenterSelector The PresenterSelector that will determine
     *        the Presenter for each row header.
     */
    public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) {
        mHeaderPresenterSelector = headerPresenterSelector;
        if (mHeadersSupportFragment != null) {
            mHeadersSupportFragment.setPresenterSelector(mHeaderPresenterSelector);
        }
    }

    private void setHeadersOnScreen(boolean onScreen) {
        MarginLayoutParams lp;
        View containerList;
        containerList = mHeadersSupportFragment.getView();
        if (containerList == null) {
            // Headers fragment has destroyed view.
            return;
        }
        lp = (MarginLayoutParams) containerList.getLayoutParams();
        lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
        containerList.setLayoutParams(lp);
    }

    void showHeaders(boolean show) {
        if (DEBUG) Log.v(TAG, "showHeaders " + show);
        mHeadersSupportFragment.setHeadersEnabled(show);
        setHeadersOnScreen(show);
        expandMainFragment(!show);
    }

    private void expandMainFragment(boolean expand) {
        MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams();
        params.setMarginStart(!expand ? mContainerListMarginStart : 0);
        mScaleFrameLayout.setLayoutParams(params);
        mMainFragmentAdapter.setExpand(expand);

        setMainFragmentAlignment();
        final float scaleFactor = !expand
                && mMainFragmentScaleEnabled
                && mMainFragmentAdapter.isScalingEnabled() ? mScaleFactor : 1;
        mScaleFrameLayout.setLayoutScaleY(scaleFactor);
        mScaleFrameLayout.setChildScale(scaleFactor);
    }

    private HeadersSupportFragment.OnHeaderClickedListener mHeaderClickedListener =
        new HeadersSupportFragment.OnHeaderClickedListener() {
            @Override
            public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
                if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {
                    return;
                }
                if (mMainFragment == null || mMainFragment.getView() == null) {
                    return;
                }
                startHeadersTransitionInternal(false);
                mMainFragment.getView().requestFocus();
            }
        };

    class MainFragmentItemViewSelectedListener implements OnItemViewSelectedListener {
        MainFragmentRowsAdapter mMainFragmentRowsAdapter;

        public MainFragmentItemViewSelectedListener(MainFragmentRowsAdapter fragmentRowsAdapter) {
            mMainFragmentRowsAdapter = fragmentRowsAdapter;
        }

        @Override
        public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
                RowPresenter.ViewHolder rowViewHolder, Row row) {
            int position = mMainFragmentRowsAdapter.getSelectedPosition();
            if (DEBUG) Log.v(TAG, "row selected position " + position);
            onRowSelected(position);
            if (mExternalOnItemViewSelectedListener != null) {
                mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
                        rowViewHolder, row);
            }
        }
    };

    private HeadersSupportFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener =
            new HeadersSupportFragment.OnHeaderViewSelectedListener() {
        @Override
        public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
            int position = mHeadersSupportFragment.getSelectedPosition();
            if (DEBUG) Log.v(TAG, "header selected position " + position);
            // Layout of Headers Fragment in hidden state may triggers the onRowSelected and
            // reset to 0. Skip in that case.
            if (mShowingHeaders) {
                onRowSelected(position);
            }
        }
    };

    void onRowSelected(int position) {
        // even position is same, it could be data changed, always post selection runnable
        // to possibly swap main fragment.
        mSetSelectionRunnable.post(
                position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);
    }

    void setSelection(int position, boolean smooth) {
        if (position == NO_POSITION) {
            return;
        }

        mSelectedPosition = position;
        if (mHeadersSupportFragment == null || mMainFragmentAdapter == null) {
            // onDestroyView() called
            return;
        }
        mHeadersSupportFragment.setSelectedPosition(position, smooth);
        replaceMainFragment(position);

        if (mMainFragmentRowsAdapter != null) {
            mMainFragmentRowsAdapter.setSelectedPosition(position, smooth);
        }

        updateTitleViewVisibility();
    }

    private void replaceMainFragment(int position) {
        if (createMainFragment(mAdapter, position)) {
            swapToMainFragment();
            expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
        }
    }

    @SuppressWarnings("ReferenceEquality")
    final void commitMainFragment() {
        FragmentManager fm = getChildFragmentManager();
        Fragment currentFragment = fm.findFragmentById(R.id.scale_frame);
        if (currentFragment != mMainFragment) {
            fm.beginTransaction()
                    .replace(R.id.scale_frame, mMainFragment).commit();
        }
    }

    private final RecyclerView.OnScrollListener mWaitScrollFinishAndCommitMainFragment =
            new RecyclerView.OnScrollListener() {
        @SuppressWarnings("ReferenceEquality")
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                recyclerView.removeOnScrollListener(this);
                if (!mStopped) {
                    commitMainFragment();
                }
            }
        }
    };

    private void swapToMainFragment() {
        if (mStopped) {
            return;
        }
        final VerticalGridView gridView = mHeadersSupportFragment.getVerticalGridView();
        if (isShowingHeaders() && gridView != null
                && gridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
            // if user is scrolling HeadersSupportFragment,  swap to empty fragment and wait scrolling
            // finishes.
            getChildFragmentManager().beginTransaction()
                    .replace(R.id.scale_frame, new Fragment()).commit();
            gridView.removeOnScrollListener(mWaitScrollFinishAndCommitMainFragment);
            gridView.addOnScrollListener(mWaitScrollFinishAndCommitMainFragment);
        } else {
            // Otherwise swap immediately
            commitMainFragment();
        }
    }

    /**
     * Sets the selected row position with smooth animation.
     */
    public void setSelectedPosition(int position) {
        setSelectedPosition(position, true);
    }

    /**
     * Gets position of currently selected row.
     * @return Position of currently selected row.
     */
    public int getSelectedPosition() {
        return mSelectedPosition;
    }

    /**
     * @return selected row ViewHolder inside fragment created by {@link MainFragmentRowsAdapter}.
     */
    public RowPresenter.ViewHolder getSelectedRowViewHolder() {
        if (mMainFragmentRowsAdapter != null) {
            int rowPos = mMainFragmentRowsAdapter.getSelectedPosition();
            return mMainFragmentRowsAdapter.findRowViewHolderByPosition(rowPos);
        }
        return null;
    }

    /**
     * Sets the selected row position.
     */
    public void setSelectedPosition(int position, boolean smooth) {
        mSetSelectionRunnable.post(
                position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth);
    }

    /**
     * Selects a Row and perform an optional task on the Row. For example
     * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code>
     * scrolls to 11th row and selects 6th item on that row.  The method will be ignored if
     * RowsSupportFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater,
     * ViewGroup, Bundle)}).
     *
     * @param rowPosition Which row to select.
     * @param smooth True to scroll to the row, false for no animation.
     * @param rowHolderTask Optional task to perform on the Row.  When the task is not null, headers
     * fragment will be collapsed.
     */
    public void setSelectedPosition(int rowPosition, boolean smooth,
            final Presenter.ViewHolderTask rowHolderTask) {
        if (mMainFragmentAdapterRegistry == null) {
            return;
        }
        if (rowHolderTask != null) {
            startHeadersTransition(false);
        }
        if (mMainFragmentRowsAdapter != null) {
            mMainFragmentRowsAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask);
        }
    }

    @Override
    public void onStart() {
        super.onStart();
        mHeadersSupportFragment.setAlignment(mContainerListAlignTop);
        setMainFragmentAlignment();

        if (mCanShowHeaders && mShowingHeaders && mHeadersSupportFragment != null
                && mHeadersSupportFragment.getView() != null) {
            mHeadersSupportFragment.getView().requestFocus();
        } else if ((!mCanShowHeaders || !mShowingHeaders) && mMainFragment != null
                && mMainFragment.getView() != null) {
            mMainFragment.getView().requestFocus();
        }

        if (mCanShowHeaders) {
            showHeaders(mShowingHeaders);
        }

        mStateMachine.fireEvent(EVT_HEADER_VIEW_CREATED);

        mStopped = false;
        // if main fragment wasn't commited in stopped state, do it again in onStart()
        commitMainFragment();
        mSetSelectionRunnable.start();
    }

    @Override
    public void onStop() {
        mStopped = true;
        mSetSelectionRunnable.stop();
        super.onStop();
    }

    private void onExpandTransitionStart(boolean expand, final Runnable callback) {
        if (expand) {
            callback.run();
            return;
        }
        // Run a "pre" layout when we go non-expand, in order to get the initial
        // positions of added rows.
        new ExpandPreLayout(callback, mMainFragmentAdapter, getView()).execute();
    }

    private void setMainFragmentAlignment() {
        int alignOffset = mContainerListAlignTop;
        if (mMainFragmentScaleEnabled
                && mMainFragmentAdapter.isScalingEnabled()
                && mShowingHeaders) {
            alignOffset = (int) (alignOffset / mScaleFactor + 0.5f);
        }
        mMainFragmentAdapter.setAlignment(alignOffset);
    }

    /**
     * Enables/disables headers transition on back key support. This is enabled by
     * default. The BrowseSupportFragment will add a back stack entry when headers are
     * showing. Running a headers transition when the back key is pressed only
     * works when the headers state is {@link #HEADERS_ENABLED} or
     * {@link #HEADERS_HIDDEN}.
     * <p>
     * NOTE: If an Activity has its own onBackPressed() handling, you must
     * disable this feature. You may use {@link #startHeadersTransition(boolean)}
     * and {@link BrowseTransitionListener} in your own back stack handling.
     */
    public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) {
        mHeadersBackStackEnabled = headersBackStackEnabled;
    }

    /**
     * Returns true if headers transition on back key support is enabled.
     */
    public final boolean isHeadersTransitionOnBackEnabled() {
        return mHeadersBackStackEnabled;
    }

    private void readArguments(Bundle args) {
        if (args == null) {
            return;
        }
        if (args.containsKey(ARG_TITLE)) {
            setTitle(args.getString(ARG_TITLE));
        }
        if (args.containsKey(ARG_HEADERS_STATE)) {
            setHeadersState(args.getInt(ARG_HEADERS_STATE));
        }
    }

    /**
     * Sets the state for the headers column in the browse fragment. Must be one
     * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or
     * {@link #HEADERS_DISABLED}.
     *
     * @param headersState The state of the headers for the browse fragment.
     */
    public void setHeadersState(int headersState) {
        if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {
            throw new IllegalArgumentException("Invalid headers state: " + headersState);
        }
        if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);

        if (headersState != mHeadersState) {
            mHeadersState = headersState;
            switch (headersState) {
                case HEADERS_ENABLED:
                    mCanShowHeaders = true;
                    mShowingHeaders = true;
                    break;
                case HEADERS_HIDDEN:
                    mCanShowHeaders = true;
                    mShowingHeaders = false;
                    break;
                case HEADERS_DISABLED:
                    mCanShowHeaders = false;
                    mShowingHeaders = false;
                    break;
                default:
                    Log.w(TAG, "Unknown headers state: " + headersState);
                    break;
            }
            if (mHeadersSupportFragment != null) {
                mHeadersSupportFragment.setHeadersGone(!mCanShowHeaders);
            }
        }
    }

    /**
     * Returns the state of the headers column in the browse fragment.
     */
    public int getHeadersState() {
        return mHeadersState;
    }

    @Override
    protected Object createEntranceTransition() {
        return TransitionHelper.loadTransition(getContext(),
                R.transition.lb_browse_entrance_transition);
    }

    @Override
    protected void runEntranceTransition(Object entranceTransition) {
        TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition);
    }

    @Override
    protected void onEntranceTransitionPrepare() {
        mHeadersSupportFragment.onTransitionPrepare();
        mMainFragmentAdapter.setEntranceTransitionState(false);
        mMainFragmentAdapter.onTransitionPrepare();
    }

    @Override
    protected void onEntranceTransitionStart() {
        mHeadersSupportFragment.onTransitionStart();
        mMainFragmentAdapter.onTransitionStart();
    }

    @Override
    protected void onEntranceTransitionEnd() {
        if (mMainFragmentAdapter != null) {
            mMainFragmentAdapter.onTransitionEnd();
        }

        if (mHeadersSupportFragment != null) {
            mHeadersSupportFragment.onTransitionEnd();
        }
    }

    void setSearchOrbViewOnScreen(boolean onScreen) {
        View searchOrbView = getTitleViewAdapter().getSearchAffordanceView();
        if (searchOrbView != null) {
            MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams();
            lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
            searchOrbView.setLayoutParams(lp);
        }
    }

    void setEntranceTransitionStartState() {
        setHeadersOnScreen(false);
        setSearchOrbViewOnScreen(false);
        // NOTE that mMainFragmentAdapter.setEntranceTransitionState(false) will be called
        // in onEntranceTransitionPrepare() because mMainFragmentAdapter is still the dummy
        // one when setEntranceTransitionStartState() is called.
    }

    void setEntranceTransitionEndState() {
        setHeadersOnScreen(mShowingHeaders);
        setSearchOrbViewOnScreen(true);
        mMainFragmentAdapter.setEntranceTransitionState(true);
    }

    private class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener {

        private final View mView;
        private final Runnable mCallback;
        private int mState;
        private MainFragmentAdapter mainFragmentAdapter;

        final static int STATE_INIT = 0;
        final static int STATE_FIRST_DRAW = 1;
        final static int STATE_SECOND_DRAW = 2;

        ExpandPreLayout(Runnable callback, MainFragmentAdapter adapter, View view) {
            mView = view;
            mCallback = callback;
            mainFragmentAdapter = adapter;
        }

        void execute() {
            mView.getViewTreeObserver().addOnPreDrawListener(this);
            mainFragmentAdapter.setExpand(false);
            // always trigger onPreDraw even adapter setExpand() does nothing.
            mView.invalidate();
            mState = STATE_INIT;
        }

        @Override
        public boolean onPreDraw() {
            if (getView() == null || getContext() == null) {
                mView.getViewTreeObserver().removeOnPreDrawListener(this);
                return true;
            }
            if (mState == STATE_INIT) {
                mainFragmentAdapter.setExpand(true);
                // always trigger onPreDraw even adapter setExpand() does nothing.
                mView.invalidate();
                mState = STATE_FIRST_DRAW;
            } else if (mState == STATE_FIRST_DRAW) {
                mCallback.run();
                mView.getViewTreeObserver().removeOnPreDrawListener(this);
                mState = STATE_SECOND_DRAW;
            }
            return false;
        }
    }
}
