/*
 * 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 com.android.intentresolver.widget;

import static android.content.res.Resources.ID_NULL;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.metrics.LogMaker;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.animation.AnimationUtils;
import android.widget.AbsListView;
import android.widget.OverScroller;

import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ScrollingView;
import androidx.recyclerview.widget.RecyclerView;

import com.android.intentresolver.R;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;

public class ResolverDrawerLayout extends ViewGroup {
    private static final String TAG = "ResolverDrawerLayout";
    private MetricsLogger mMetricsLogger;

    /**
     * Max width of the whole drawer layout
     */
    private final int mMaxWidth;

    /**
     * Max total visible height of views not marked always-show when in the closed/initial state
     */
    private int mMaxCollapsedHeight;

    /**
     * Max total visible height of views not marked always-show when in the closed/initial state
     * when a default option is present
     */
    private int mMaxCollapsedHeightSmall;

    /**
     * Whether {@code mMaxCollapsedHeightSmall} was set explicitly as a layout attribute or
     * inferred by {@code mMaxCollapsedHeight}.
     */
    private final boolean mIsMaxCollapsedHeightSmallExplicit;

    private boolean mSmallCollapsed;

    /**
     * Move views down from the top by this much in px
     */
    private float mCollapseOffset;

    /**
      * Track fractions of pixels from drag calculations. Without this, the view offsets get
      * out of sync due to frequently dropping fractions of a pixel from '(int) dy' casts.
      */
    private float mDragRemainder = 0.0f;
    private int mHeightUsed;
    private int mCollapsibleHeight;
    private int mAlwaysShowHeight;

    /**
     * The height in pixels of reserved space added to the top of the collapsed UI;
     * e.g. chooser targets
     */
    private int mCollapsibleHeightReserved;

    private int mTopOffset;
    private boolean mShowAtTop;
    @IdRes
    private int mIgnoreOffsetTopLimitViewId = ID_NULL;

    private boolean mIsDragging;
    private boolean mOpenOnClick;
    private boolean mOpenOnLayout;
    private boolean mDismissOnScrollerFinished;
    private final int mTouchSlop;
    private final float mMinFlingVelocity;
    private final OverScroller mScroller;
    private final VelocityTracker mVelocityTracker;

    private Drawable mScrollIndicatorDrawable;

    private OnDismissedListener mOnDismissedListener;
    private RunOnDismissedListener mRunOnDismissedListener;
    private OnCollapsedChangedListener mOnCollapsedChangedListener;

    private boolean mDismissLocked;

    private float mInitialTouchX;
    private float mInitialTouchY;
    private float mLastTouchY;
    private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;

    private final Rect mTempRect = new Rect();

    private AbsListView mNestedListChild;
    private RecyclerView mNestedRecyclerChild;

    @Nullable
    private final ScrollablePreviewFlingLogicDelegate mFlingLogicDelegate;

    private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener =
            new ViewTreeObserver.OnTouchModeChangeListener() {
                @Override
                public void onTouchModeChanged(boolean isInTouchMode) {
                    if (!isInTouchMode && hasFocus() && isDescendantClipped(getFocusedChild())) {
                        smoothScrollTo(0, 0);
                    }
                }
            };

    public ResolverDrawerLayout(Context context) {
        this(context, null);
    }

    public ResolverDrawerLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout,
                defStyleAttr, 0);
        mMaxWidth = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_android_maxWidth, -1);
        mMaxCollapsedHeight = a.getDimensionPixelSize(
                R.styleable.ResolverDrawerLayout_maxCollapsedHeight, 0);
        mMaxCollapsedHeightSmall = a.getDimensionPixelSize(
                R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall,
                mMaxCollapsedHeight);
        mIsMaxCollapsedHeightSmallExplicit =
                a.hasValue(R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall);
        mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false);
        if (a.hasValue(R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit)) {
            mIgnoreOffsetTopLimitViewId = a.getResourceId(
                    R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit, ID_NULL);
        }
        mFlingLogicDelegate =
                a.getBoolean(
                        R.styleable.ResolverDrawerLayout_useScrollablePreviewNestedFlingLogic,
                        false)
                    ? new ScrollablePreviewFlingLogicDelegate() {}
                    : null;
        a.recycle();

        mScrollIndicatorDrawable = mContext.getDrawable(
                com.android.internal.R.drawable.scroll_indicator_material);

        mScroller = new OverScroller(context, AnimationUtils.loadInterpolator(context,
                android.R.interpolator.decelerate_quint));
        mVelocityTracker = VelocityTracker.obtain();

        final ViewConfiguration vc = ViewConfiguration.get(context);
        mTouchSlop = vc.getScaledTouchSlop();
        mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();

        setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
    }

    /**
     * Dynamically set the max collapsed height. Note this also updates the small collapsed
     * height if it wasn't specified explicitly.
     */
    public void setMaxCollapsedHeight(int heightInPixels) {
        if (heightInPixels == mMaxCollapsedHeight) {
            return;
        }
        mMaxCollapsedHeight = heightInPixels;
        if (!mIsMaxCollapsedHeightSmallExplicit) {
            mMaxCollapsedHeightSmall = mMaxCollapsedHeight;
        }
        requestLayout();
    }

    public void setSmallCollapsed(boolean smallCollapsed) {
        if (mSmallCollapsed != smallCollapsed) {
            mSmallCollapsed = smallCollapsed;
            requestLayout();
        }
    }

    public boolean isSmallCollapsed() {
        return mSmallCollapsed;
    }

    public boolean isCollapsed() {
        return mCollapseOffset > 0;
    }

    public void setShowAtTop(boolean showOnTop) {
        if (mShowAtTop != showOnTop) {
            mShowAtTop = showOnTop;
            requestLayout();
        }
    }

    public boolean getShowAtTop() {
        return mShowAtTop;
    }

    public void setCollapsed(boolean collapsed) {
        if (!isLaidOut()) {
            mOpenOnLayout = !collapsed;
        } else {
            smoothScrollTo(collapsed ? mCollapsibleHeight : 0, 0);
        }
    }

    public void setCollapsibleHeightReserved(int heightPixels) {
        final int oldReserved = mCollapsibleHeightReserved;
        mCollapsibleHeightReserved = heightPixels;
        if (oldReserved != mCollapsibleHeightReserved) {
            requestLayout();
        }

        final int dReserved = mCollapsibleHeightReserved - oldReserved;
        if (dReserved != 0 && mIsDragging) {
            mLastTouchY -= dReserved;
        }

        final int oldCollapsibleHeight = updateCollapsibleHeight();
        if (updateCollapseOffset(oldCollapsibleHeight, !isDragging())) {
            return;
        }

        invalidate();
    }

    public void setDismissLocked(boolean locked) {
        mDismissLocked = locked;
    }

    private boolean isMoving() {
        return mIsDragging || !mScroller.isFinished();
    }

    private boolean isDragging() {
        return mIsDragging || getNestedScrollAxes() == SCROLL_AXIS_VERTICAL;
    }

    private boolean updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed) {
        if (oldCollapsibleHeight == mCollapsibleHeight) {
            return false;
        }

        if (getShowAtTop()) {
            // Keep the drawer fully open.
            setCollapseOffset(0);
            return false;
        }

        if (isLaidOut()) {
            final boolean isCollapsedOld = mCollapseOffset != 0;
            if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight
                    && mCollapseOffset == oldCollapsibleHeight)) {
                // Stay closed even at the new height.
                setCollapseOffset(mCollapsibleHeight);
            } else {
                setCollapseOffset(Math.min(mCollapseOffset, mCollapsibleHeight));
            }
            final boolean isCollapsedNew = mCollapseOffset != 0;
            if (isCollapsedOld != isCollapsedNew) {
                onCollapsedChanged(isCollapsedNew);
            }
        } else {
            // Start out collapsed at first unless we restored state for otherwise
            setCollapseOffset(mOpenOnLayout ? 0 : mCollapsibleHeight);
        }
        return true;
    }

    private void setCollapseOffset(float collapseOffset) {
        if (mCollapseOffset != collapseOffset) {
            mCollapseOffset = collapseOffset;
            requestLayout();
        }
    }

    private int getMaxCollapsedHeight() {
        return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight)
                + mCollapsibleHeightReserved;
    }

    public void setOnDismissedListener(OnDismissedListener listener) {
        mOnDismissedListener = listener;
    }

    private boolean isDismissable() {
        return mOnDismissedListener != null && !mDismissLocked;
    }

    public void setOnCollapsedChangedListener(OnCollapsedChangedListener listener) {
        mOnCollapsedChangedListener = listener;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getActionMasked();

        if (action == MotionEvent.ACTION_DOWN) {
            mVelocityTracker.clear();
        }

        mVelocityTracker.addMovement(ev);

        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                final float x = ev.getX();
                final float y = ev.getY();
                mInitialTouchX = x;
                mInitialTouchY = mLastTouchY = y;
                mOpenOnClick = isListChildUnderClipped(x, y) && mCollapseOffset > 0;
            }
            break;

            case MotionEvent.ACTION_MOVE: {
                final float x = ev.getX();
                final float y = ev.getY();
                final float dy = y - mInitialTouchY;
                if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null &&
                        (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
                    mActivePointerId = ev.getPointerId(0);
                    mIsDragging = true;
                    mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
                            Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
                }
            }
            break;

            case MotionEvent.ACTION_POINTER_UP: {
                onSecondaryPointerUp(ev);
            }
            break;

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP: {
                resetTouch();
            }
            break;
        }

        if (mIsDragging) {
            abortAnimation();
        }
        return mIsDragging || mOpenOnClick;
    }

    private boolean isNestedListChildScrolled() {
        return  mNestedListChild != null
                && mNestedListChild.getChildCount() > 0
                && (mNestedListChild.getFirstVisiblePosition() > 0
                        || mNestedListChild.getChildAt(0).getTop() < 0);
    }

    private boolean isNestedRecyclerChildScrolled() {
        if (mNestedRecyclerChild != null && mNestedRecyclerChild.getChildCount() > 0) {
            final RecyclerView.ViewHolder vh =
                    mNestedRecyclerChild.findViewHolderForAdapterPosition(0);
            return vh == null || vh.itemView.getTop() < 0;
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        final int action = ev.getActionMasked();

        mVelocityTracker.addMovement(ev);

        boolean handled = false;
        switch (action) {
            case MotionEvent.ACTION_DOWN: {
                final float x = ev.getX();
                final float y = ev.getY();
                mInitialTouchX = x;
                mInitialTouchY = mLastTouchY = y;
                mActivePointerId = ev.getPointerId(0);
                final boolean hitView = findChildUnder(mInitialTouchX, mInitialTouchY) != null;
                handled = isDismissable() || mCollapsibleHeight > 0;
                mIsDragging = hitView && handled;
                abortAnimation();
            }
            break;

            case MotionEvent.ACTION_MOVE: {
                int index = ev.findPointerIndex(mActivePointerId);
                if (index < 0) {
                    Log.e(TAG, "Bad pointer id " + mActivePointerId + ", resetting");
                    index = 0;
                    mActivePointerId = ev.getPointerId(0);
                    mInitialTouchX = ev.getX();
                    mInitialTouchY = mLastTouchY = ev.getY();
                }
                final float x = ev.getX(index);
                final float y = ev.getY(index);
                if (!mIsDragging) {
                    final float dy = y - mInitialTouchY;
                    if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null) {
                        handled = mIsDragging = true;
                        mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
                                Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
                    }
                }
                if (mIsDragging) {
                    final float dy = y - mLastTouchY;
                    if (dy > 0 && isNestedListChildScrolled()) {
                        mNestedListChild.smoothScrollBy((int) -dy, 0);
                    } else if (dy > 0 && isNestedRecyclerChildScrolled()) {
                        mNestedRecyclerChild.scrollBy(0, (int) -dy);
                    } else {
                        performDrag(dy);
                    }
                }
                mLastTouchY = y;
            }
            break;

            case MotionEvent.ACTION_POINTER_DOWN: {
                final int pointerIndex = ev.getActionIndex();
                mActivePointerId = ev.getPointerId(pointerIndex);
                mInitialTouchX = ev.getX(pointerIndex);
                mInitialTouchY = mLastTouchY = ev.getY(pointerIndex);
            }
            break;

            case MotionEvent.ACTION_POINTER_UP: {
                onSecondaryPointerUp(ev);
            }
            break;

            case MotionEvent.ACTION_UP: {
                final boolean wasDragging = mIsDragging;
                mIsDragging = false;
                if (!wasDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null &&
                        findChildUnder(ev.getX(), ev.getY()) == null) {
                    if (isDismissable()) {
                        dispatchOnDismissed();
                        resetTouch();
                        return true;
                    }
                }
                if (mOpenOnClick && Math.abs(ev.getX() - mInitialTouchX) < mTouchSlop &&
                        Math.abs(ev.getY() - mInitialTouchY) < mTouchSlop) {
                    smoothScrollTo(0, 0);
                    return true;
                }
                mVelocityTracker.computeCurrentVelocity(1000);
                final float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
                if (Math.abs(yvel) > mMinFlingVelocity) {
                    if (getShowAtTop()) {
                        if (isDismissable() && yvel < 0) {
                            abortAnimation();
                            dismiss();
                        } else {
                            smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
                        }
                    } else {
                        if (isDismissable()
                                && yvel > 0 && mCollapseOffset > mCollapsibleHeight) {
                            smoothScrollTo(mHeightUsed, yvel);
                            mDismissOnScrollerFinished = true;
                        } else {
                            scrollNestedScrollableChildBackToTop();
                            smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
                        }
                    }
                }else {
                    smoothScrollTo(
                            mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
                }
                resetTouch();
            }
            break;

            case MotionEvent.ACTION_CANCEL: {
                if (mIsDragging) {
                    smoothScrollTo(
                            mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
                }
                resetTouch();
                return true;
            }
        }

        return handled;
    }

    /**
     * Scroll nested scrollable child back to top if it has been scrolled.
     */
    public void scrollNestedScrollableChildBackToTop() {
        if (isNestedListChildScrolled()) {
            mNestedListChild.smoothScrollToPosition(0);
        } else if (isNestedRecyclerChildScrolled()) {
            mNestedRecyclerChild.smoothScrollToPosition(0);
        }
    }

    private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = ev.getActionIndex();
        final int pointerId = ev.getPointerId(pointerIndex);
        if (pointerId == mActivePointerId) {
            // This was our active pointer going up. Choose a new
            // active pointer and adjust accordingly.
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            mInitialTouchX = ev.getX(newPointerIndex);
            mInitialTouchY = mLastTouchY = ev.getY(newPointerIndex);
            mActivePointerId = ev.getPointerId(newPointerIndex);
        }
    }

    private void resetTouch() {
        mActivePointerId = MotionEvent.INVALID_POINTER_ID;
        mIsDragging = false;
        mOpenOnClick = false;
        mInitialTouchX = mInitialTouchY = mLastTouchY = 0;
        mVelocityTracker.clear();
    }

    private void dismiss() {
        mRunOnDismissedListener = new RunOnDismissedListener();
        post(mRunOnDismissedListener);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            final boolean keepGoing = !mScroller.isFinished();
            performDrag(mScroller.getCurrY() - mCollapseOffset);
            if (keepGoing) {
                postInvalidateOnAnimation();
            } else if (mDismissOnScrollerFinished && mOnDismissedListener != null) {
                dismiss();
            }
        }
    }

    private void abortAnimation() {
        mScroller.abortAnimation();
        mRunOnDismissedListener = null;
        mDismissOnScrollerFinished = false;
    }

    private float performDrag(float dy) {
        if (getShowAtTop()) {
            return 0;
        }

        final float newPos = Math.max(0, Math.min(mCollapseOffset + dy, mHeightUsed));
        if (newPos != mCollapseOffset) {
            dy = newPos - mCollapseOffset;

            mDragRemainder += dy - (int) dy;
            if (mDragRemainder >= 1.0f) {
                mDragRemainder -= 1.0f;
                dy += 1.0f;
            } else if (mDragRemainder <= -1.0f) {
                mDragRemainder += 1.0f;
                dy -= 1.0f;
            }

            boolean isIgnoreOffsetLimitSet = false;
            int ignoreOffsetLimit = 0;
            View ignoreOffsetLimitView = findIgnoreOffsetLimitView();
            if (ignoreOffsetLimitView != null) {
                LayoutParams lp = (LayoutParams) ignoreOffsetLimitView.getLayoutParams();
                ignoreOffsetLimit = ignoreOffsetLimitView.getBottom() + lp.bottomMargin;
                isIgnoreOffsetLimitSet = true;
            }
            final int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                if (child.getVisibility() == View.GONE) {
                    continue;
                }
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                if (!lp.ignoreOffset) {
                    child.offsetTopAndBottom((int) dy);
                } else if (isIgnoreOffsetLimitSet) {
                    int top = child.getTop();
                    int targetTop = Math.max(
                            (int) (ignoreOffsetLimit + lp.topMargin + dy),
                            lp.mFixedTop);
                    if (top != targetTop) {
                        child.offsetTopAndBottom(targetTop - top);
                    }
                    ignoreOffsetLimit = child.getBottom() + lp.bottomMargin;
                }
            }
            final boolean isCollapsedOld = mCollapseOffset != 0;
            mCollapseOffset = newPos;
            mTopOffset += dy;
            final boolean isCollapsedNew = newPos != 0;
            if (isCollapsedOld != isCollapsedNew) {
                onCollapsedChanged(isCollapsedNew);
                getMetricsLogger().write(
                        new LogMaker(MetricsEvent.ACTION_SHARESHEET_COLLAPSED_CHANGED)
                        .setSubtype(isCollapsedNew ? 1 : 0));
            }
            onScrollChanged(0, (int) newPos, 0, (int) (newPos - dy));
            postInvalidateOnAnimation();
            return dy;
        }
        return 0;
    }

    private void onCollapsedChanged(boolean isCollapsed) {
        notifyViewAccessibilityStateChangedIfNeeded(
                AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);

        if (mScrollIndicatorDrawable != null) {
            setWillNotDraw(!isCollapsed);
        }

        if (mOnCollapsedChangedListener != null) {
            mOnCollapsedChangedListener.onCollapsedChanged(isCollapsed);
        }
    }

    void dispatchOnDismissed() {
        if (mOnDismissedListener != null) {
            mOnDismissedListener.onDismissed();
        }
        if (mRunOnDismissedListener != null) {
            removeCallbacks(mRunOnDismissedListener);
            mRunOnDismissedListener = null;
        }
    }

    private void smoothScrollTo(int yOffset, float velocity) {
        abortAnimation();
        final int sy = (int) mCollapseOffset;
        int dy = yOffset - sy;
        if (dy == 0) {
            return;
        }

        final int height = getHeight();
        final int halfHeight = height / 2;
        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / height);
        final float distance = halfHeight + halfHeight *
                distanceInfluenceForSnapDuration(distanceRatio);

        int duration = 0;
        velocity = Math.abs(velocity);
        if (velocity > 0) {
            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
        } else {
            final float pageDelta = (float) Math.abs(dy) / height;
            duration = (int) ((pageDelta + 1) * 100);
        }
        duration = Math.min(duration, 300);

        mScroller.startScroll(0, sy, 0, dy, duration);
        postInvalidateOnAnimation();
    }

    private float distanceInfluenceForSnapDuration(float f) {
        f -= 0.5f; // center the values about 0.
        f *= 0.3f * Math.PI / 2.0f;
        return (float) Math.sin(f);
    }

    /**
     * Note: this method doesn't take Z into account for overlapping views
     * since it is only used in contexts where this doesn't affect the outcome.
     */
    private View findChildUnder(float x, float y) {
        return findChildUnder(this, x, y);
    }

    private static View findChildUnder(ViewGroup parent, float x, float y) {
        final int childCount = parent.getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            final View child = parent.getChildAt(i);
            if (isChildUnder(child, x, y)) {
                return child;
            }
        }
        return null;
    }

    private View findListChildUnder(float x, float y) {
        View v = findChildUnder(x, y);
        while (v != null) {
            x -= v.getX();
            y -= v.getY();
            if (v instanceof AbsListView) {
                // One more after this.
                return findChildUnder((ViewGroup) v, x, y);
            }
            v = v instanceof ViewGroup ? findChildUnder((ViewGroup) v, x, y) : null;
        }
        return v;
    }

    /**
     * This only checks clipping along the bottom edge.
     */
    private boolean isListChildUnderClipped(float x, float y) {
        final View listChild = findListChildUnder(x, y);
        return listChild != null && isDescendantClipped(listChild);
    }

    private boolean isDescendantClipped(View child) {
        mTempRect.set(0, 0, child.getWidth(), child.getHeight());
        offsetDescendantRectToMyCoords(child, mTempRect);
        View directChild;
        if (child.getParent() == this) {
            directChild = child;
        } else {
            View v = child;
            ViewParent p = child.getParent();
            while (p != this) {
                v = (View) p;
                p = v.getParent();
            }
            directChild = v;
        }

        // ResolverDrawerLayout lays out vertically in child order;
        // the next view and forward is what to check against.
        int clipEdge = getHeight() - getPaddingBottom();
        final int childCount = getChildCount();
        for (int i = indexOfChild(directChild) + 1; i < childCount; i++) {
            final View nextChild = getChildAt(i);
            if (nextChild.getVisibility() == GONE) {
                continue;
            }
            clipEdge = Math.min(clipEdge, nextChild.getTop());
        }
        return mTempRect.bottom > clipEdge;
    }

    private static boolean isChildUnder(View child, float x, float y) {
        final float left = child.getX();
        final float top = child.getY();
        final float right = left + child.getWidth();
        final float bottom = top + child.getHeight();
        return x >= left && y >= top && x < right && y < bottom;
    }

    @Override
    public void requestChildFocus(View child, View focused) {
        super.requestChildFocus(child, focused);
        if (!isInTouchMode() && isDescendantClipped(focused)) {
            smoothScrollTo(0, 0);
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        getViewTreeObserver().addOnTouchModeChangeListener(mTouchModeChangeListener);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        getViewTreeObserver().removeOnTouchModeChangeListener(mTouchModeChangeListener);
        abortAnimation();
    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        if ((nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0) {
            if (target instanceof AbsListView) {
                mNestedListChild = (AbsListView) target;
            }
            if (target instanceof RecyclerView) {
                mNestedRecyclerChild = (RecyclerView) target;
            }
            return true;
        }
        return false;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int axes) {
        super.onNestedScrollAccepted(child, target, axes);
    }

    @Override
    public void onStopNestedScroll(View child) {
        super.onStopNestedScroll(child);
        if (mScroller.isFinished()) {
            smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
        }
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed) {
        if (dyUnconsumed < 0) {
            performDrag(-dyUnconsumed);
        }
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        if (dy > 0) {
            consumed[1] = (int) -performDrag(-dy);
        }
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        if (mFlingLogicDelegate != null) {
            return mFlingLogicDelegate.onNestedPreFling(this, target, velocityX, velocityY);
        }
        if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) {
            smoothScrollTo(0, velocityY);
            return true;
        }
        return false;
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        if (mFlingLogicDelegate != null) {
            return mFlingLogicDelegate.onNestedFling(this, target, velocityX, velocityY, consumed);
        }
        // TODO: find a more suitable way to fix it.
        //  RecyclerView started reporting `consumed` as true whenever a scrolling is enabled,
        //  previously the value was based on whether the fling can be performed in given direction
        //  i.e. whether it is at the top or at the bottom. isRecyclerViewAtTheTop method is a
        //  workaround that restores the legacy functionality.
        boolean shouldConsume = (Math.abs(velocityY) > mMinFlingVelocity)
                && (!consumed || (velocityY < 0 && isRecyclerViewAtTheTop(target)));
        if (shouldConsume) {
            if (getShowAtTop()) {
                if (isDismissable() && velocityY > 0) {
                    abortAnimation();
                    dismiss();
                } else {
                    smoothScrollTo(velocityY < 0 ? mCollapsibleHeight : 0, velocityY);
                }
            } else {
                if (isDismissable()
                        && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) {
                    smoothScrollTo(mHeightUsed, velocityY);
                    mDismissOnScrollerFinished = true;
                } else {
                    smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY);
                }
            }
            return true;
        }
        return false;
    }

    private static boolean isRecyclerViewAtTheTop(View target) {
        // TODO: there's a very similar functionality in #isNestedRecyclerChildScrolled(),
        //  consolidate the two.
        if (!(target instanceof RecyclerView)) {
            return false;
        }
        RecyclerView recyclerView = (RecyclerView) target;
        if (recyclerView.getChildCount() == 0) {
            return true;
        }
        View firstChild = recyclerView.getChildAt(0);
        return recyclerView.getChildAdapterPosition(firstChild) == 0
                && firstChild.getTop() >= recyclerView.getPaddingTop();
    }

    private static boolean isFlingTargetAtTop(View target) {
        if (target instanceof ScrollingView) {
            return !target.canScrollVertically(-1);
        }
        return false;
    }

    private boolean performAccessibilityActionCommon(int action) {
        switch (action) {
            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
            case AccessibilityNodeInfo.ACTION_EXPAND:
            case com.android.internal.R.id.accessibilityActionScrollDown:
                if (mCollapseOffset != 0) {
                    smoothScrollTo(0, 0);
                    return true;
                }
                break;
            case AccessibilityNodeInfo.ACTION_COLLAPSE:
                if (mCollapseOffset < mCollapsibleHeight) {
                    smoothScrollTo(mCollapsibleHeight, 0);
                    return true;
                }
                break;
            case AccessibilityNodeInfo.ACTION_DISMISS:
                if ((mCollapseOffset < mHeightUsed) && isDismissable()) {
                    smoothScrollTo(mHeightUsed, 0);
                    mDismissOnScrollerFinished = true;
                    return true;
                }
                break;
        }

        return false;
    }

    @Override
    public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
        if (super.onNestedPrePerformAccessibilityAction(target, action, args)) {
            return true;
        }

        return performAccessibilityActionCommon(action);
    }

    @Override
    public CharSequence getAccessibilityClassName() {
        // Since we support scrolling, make this ViewGroup look like a
        // ScrollView. This is kind of a hack until we have support for
        // specifying auto-scroll behavior.
        return android.widget.ScrollView.class.getName();
    }

    @Override
    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfoInternal(info);

        if (isEnabled()) {
            if (mCollapseOffset != 0) {
                info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);
                info.addAction(AccessibilityAction.ACTION_EXPAND);
                info.addAction(AccessibilityAction.ACTION_SCROLL_DOWN);
                info.setScrollable(true);
            }
            if ((mCollapseOffset < mHeightUsed)
                    && ((mCollapseOffset < mCollapsibleHeight) || isDismissable())) {
                info.addAction(AccessibilityAction.ACTION_SCROLL_UP);
                info.setScrollable(true);
            }
            if (mCollapseOffset < mCollapsibleHeight) {
                info.addAction(AccessibilityAction.ACTION_COLLAPSE);
            }
            if (mCollapseOffset < mHeightUsed && isDismissable()) {
                info.addAction(AccessibilityAction.ACTION_DISMISS);
            }
        }

        // This view should never get accessibility focus, but it's interactive
        // via nested scrolling, so we can't hide it completely.
        info.removeAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
    }

    @Override
    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
        if (action == AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS.getId()) {
            // This view should never get accessibility focus.
            return false;
        }

        if (super.performAccessibilityActionInternal(action, arguments)) {
            return true;
        }

        return performAccessibilityActionCommon(action);
    }

    @Override
    public void onDrawForeground(@NonNull Canvas canvas) {
        if (mScrollIndicatorDrawable != null) {
            mScrollIndicatorDrawable.draw(canvas);
        }

        super.onDrawForeground(canvas);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec);
        int widthSize = sourceWidth;
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        // Single-use layout; just ignore the mode and use available space.
        // Clamp to maxWidth.
        if (mMaxWidth >= 0) {
            widthSize = Math.min(widthSize, mMaxWidth + getPaddingLeft() + getPaddingRight());
        }

        final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
        final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);

        // Currently we allot more height than is really needed so that the entirety of the
        // sheet may be pulled up.
        // TODO: Restrict the height here to be the right value.
        int heightUsed = 0;

        // Measure always-show children first.
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (lp.alwaysShow && child.getVisibility() != GONE) {
                if (lp.maxHeight != -1) {
                    final int remainingHeight = heightSize - heightUsed;
                    measureChildWithMargins(child, widthSpec, 0,
                            MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
                            lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
                } else {
                    measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
                }
                heightUsed += child.getMeasuredHeight();
            }
        }

        mAlwaysShowHeight = heightUsed;

        // And now the rest.
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (!lp.alwaysShow && child.getVisibility() != GONE) {
                if (lp.maxHeight != -1) {
                    final int remainingHeight = heightSize - heightUsed;
                    measureChildWithMargins(child, widthSpec, 0,
                            MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
                            lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
                } else {
                    measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
                }
                heightUsed += child.getMeasuredHeight();
            }
        }

        mHeightUsed = heightUsed;
        int oldCollapsibleHeight = updateCollapsibleHeight();
        updateCollapseOffset(oldCollapsibleHeight, !isDragging());

        if (getShowAtTop()) {
            mTopOffset = 0;
        } else {
            mTopOffset = Math.max(0, heightSize - mHeightUsed) + (int) mCollapseOffset;
        }

        setMeasuredDimension(sourceWidth, heightSize);
    }

    private int updateCollapsibleHeight() {
        final int oldCollapsibleHeight = mCollapsibleHeight;
        mCollapsibleHeight = Math.max(0, mHeightUsed - mAlwaysShowHeight - getMaxCollapsedHeight());
        return oldCollapsibleHeight;
    }

    /**
      * @return The space reserved by views with 'alwaysShow=true'
      */
    public int getAlwaysShowHeight() {
        return mAlwaysShowHeight;
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int width = getWidth();

        View indicatorHost = null;

        int ypos = mTopOffset;
        final int leftEdge = getPaddingLeft();
        final int rightEdge = width - getPaddingRight();
        final int widthAvailable = rightEdge - leftEdge;

        boolean isIgnoreOffsetLimitSet = false;
        int ignoreOffsetLimit = 0;
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (lp.hasNestedScrollIndicator) {
                indicatorHost = child;
            }

            if (child.getVisibility() == GONE) {
                continue;
            }

            if (mIgnoreOffsetTopLimitViewId != ID_NULL && !isIgnoreOffsetLimitSet) {
                if (mIgnoreOffsetTopLimitViewId == child.getId()) {
                    ignoreOffsetLimit = child.getBottom() + lp.bottomMargin;
                    isIgnoreOffsetLimitSet = true;
                }
            }

            int top = ypos + lp.topMargin;
            if (lp.ignoreOffset) {
                if (!isDragging()) {
                    lp.mFixedTop = (int) (top - mCollapseOffset);
                }
                if (isIgnoreOffsetLimitSet) {
                    top = Math.max(ignoreOffsetLimit + lp.topMargin, (int) (top - mCollapseOffset));
                    ignoreOffsetLimit = top + child.getMeasuredHeight() + lp.bottomMargin;
                } else {
                    top -= mCollapseOffset;
                }
            }
            final int bottom = top + child.getMeasuredHeight();

            final int childWidth = child.getMeasuredWidth();
            final int left = leftEdge + (widthAvailable - childWidth) / 2;
            final int right = left + childWidth;

            child.layout(left, top, right, bottom);

            ypos = bottom + lp.bottomMargin;
        }

        if (mScrollIndicatorDrawable != null) {
            if (indicatorHost != null) {
                final int left = indicatorHost.getLeft();
                final int right = indicatorHost.getRight();
                final int bottom = indicatorHost.getTop();
                final int top = bottom - mScrollIndicatorDrawable.getIntrinsicHeight();
                mScrollIndicatorDrawable.setBounds(left, top, right, bottom);
                setWillNotDraw(!isCollapsed());
            } else {
                mScrollIndicatorDrawable = null;
                setWillNotDraw(true);
            }
        }
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        if (p instanceof LayoutParams) {
            return new LayoutParams((LayoutParams) p);
        } else if (p instanceof MarginLayoutParams) {
            return new LayoutParams((MarginLayoutParams) p);
        }
        return new LayoutParams(p);
    }

    @Override
    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        final SavedState ss = new SavedState(super.onSaveInstanceState());
        ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0;
        ss.mCollapsibleHeightReserved = mCollapsibleHeightReserved;
        return ss;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        final SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());
        mOpenOnLayout = ss.open;
        mCollapsibleHeightReserved = ss.mCollapsibleHeightReserved;
    }

    private View findIgnoreOffsetLimitView() {
        if (mIgnoreOffsetTopLimitViewId == ID_NULL) {
            return null;
        }
        View v = findViewById(mIgnoreOffsetTopLimitViewId);
        if (v != null && v != this && v.getParent() == this && v.getVisibility() != View.GONE) {
            return v;
        }
        return null;
    }

    public static class LayoutParams extends MarginLayoutParams {
        public boolean alwaysShow;
        public boolean ignoreOffset;
        public boolean hasNestedScrollIndicator;
        public int maxHeight;
        int mFixedTop;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

            final TypedArray a = c.obtainStyledAttributes(attrs,
                    R.styleable.ResolverDrawerLayout_LayoutParams);
            alwaysShow = a.getBoolean(
                    R.styleable.ResolverDrawerLayout_LayoutParams_layout_alwaysShow,
                    false);
            ignoreOffset = a.getBoolean(
                    R.styleable.ResolverDrawerLayout_LayoutParams_layout_ignoreOffset,
                    false);
            hasNestedScrollIndicator = a.getBoolean(
                    R.styleable.ResolverDrawerLayout_LayoutParams_layout_hasNestedScrollIndicator,
                    false);
            maxHeight = a.getDimensionPixelSize(
                    R.styleable.ResolverDrawerLayout_LayoutParams_layout_maxHeight, -1);
            a.recycle();
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(LayoutParams source) {
            super(source);
            this.alwaysShow = source.alwaysShow;
            this.ignoreOffset = source.ignoreOffset;
            this.hasNestedScrollIndicator = source.hasNestedScrollIndicator;
            this.maxHeight = source.maxHeight;
        }

        public LayoutParams(MarginLayoutParams source) {
            super(source);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
    }

    static class SavedState extends BaseSavedState {
        boolean open;
        private int mCollapsibleHeightReserved;

        SavedState(Parcelable superState) {
            super(superState);
        }

        private SavedState(Parcel in) {
            super(in);
            open = in.readInt() != 0;
            mCollapsibleHeightReserved = in.readInt();
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(open ? 1 : 0);
            out.writeInt(mCollapsibleHeightReserved);
        }

        public static final Parcelable.Creator<SavedState> CREATOR =
                new Parcelable.Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }

    /**
     * Listener for sheet dismissed events.
     */
    public interface OnDismissedListener {
        /**
         * Callback when the sheet is dismissed by the user.
         */
        void onDismissed();
    }

    /**
     * Listener for sheet collapsed / expanded events.
     */
    public interface OnCollapsedChangedListener {
        /**
         * Callback when the sheet is either fully expanded or collapsed.
         * @param isCollapsed true when collapsed, false when expanded.
         */
        void onCollapsedChanged(boolean isCollapsed);
    }

    private class RunOnDismissedListener implements Runnable {
        @Override
        public void run() {
            dispatchOnDismissed();
        }
    }

    private MetricsLogger getMetricsLogger() {
        if (mMetricsLogger == null) {
            mMetricsLogger = new MetricsLogger();
        }
        return mMetricsLogger;
    }

    /**
     * Controlled by
     * {@link com.android.intentresolver.Flags#FLAG_SCROLLABLE_PREVIEW}
     */
    private interface ScrollablePreviewFlingLogicDelegate {
        default boolean onNestedPreFling(
                ResolverDrawerLayout drawer, View target, float velocityX, float velocityY) {
            boolean shouldScroll = !drawer.getShowAtTop() && velocityY > drawer.mMinFlingVelocity
                    && drawer.mCollapseOffset != 0;
            if (shouldScroll) {
                drawer.smoothScrollTo(0, velocityY);
                return true;
            }
            boolean shouldDismiss = (Math.abs(velocityY) > drawer.mMinFlingVelocity)
                    && velocityY < 0
                    && isFlingTargetAtTop(target);
            if (shouldDismiss) {
                if (drawer.getShowAtTop()) {
                    drawer.smoothScrollTo(drawer.mCollapsibleHeight, velocityY);
                } else {
                    if (drawer.isDismissable()
                            && drawer.mCollapseOffset > drawer.mCollapsibleHeight) {
                        drawer.smoothScrollTo(drawer.mHeightUsed, velocityY);
                        drawer.mDismissOnScrollerFinished = true;
                    } else {
                        drawer.smoothScrollTo(drawer.mCollapsibleHeight, velocityY);
                    }
                }
                return true;
            }
            return false;
        }

        default boolean onNestedFling(
                ResolverDrawerLayout drawer,
                View target,
                float velocityX,
                float velocityY,
                boolean consumed) {
            // TODO: find a more suitable way to fix it.
            //  RecyclerView started reporting `consumed` as true whenever a scrolling is enabled,
            //  previously the value was based on whether the fling can be performed in given
            //  direction i.e. whether it is at the top or at the bottom. isRecyclerViewAtTheTop
            //  method is a workaround that restores the legacy functionality.
            boolean shouldConsume = (Math.abs(velocityY) > drawer.mMinFlingVelocity) && !consumed;
            if (shouldConsume) {
                if (drawer.getShowAtTop()) {
                    if (drawer.isDismissable() && velocityY > 0) {
                        drawer.abortAnimation();
                        drawer.dismiss();
                    } else {
                        drawer.smoothScrollTo(
                                velocityY < 0 ? drawer.mCollapsibleHeight : 0, velocityY);
                    }
                } else {
                    if (drawer.isDismissable()
                            && velocityY < 0
                            && drawer.mCollapseOffset > drawer.mCollapsibleHeight) {
                        drawer.smoothScrollTo(drawer.mHeightUsed, velocityY);
                        drawer.mDismissOnScrollerFinished = true;
                    } else {
                        drawer.smoothScrollTo(
                                velocityY > 0 ? 0 : drawer.mCollapsibleHeight, velocityY);
                    }
                }
            }
            return shouldConsume;
        }
    }
}
