1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.statusbar.phone;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.ValueAnimator;
23import android.content.Context;
24import android.content.res.Configuration;
25import android.content.res.Resources;
26import android.os.SystemClock;
27import android.os.VibrationEffect;
28import android.util.AttributeSet;
29import android.util.Log;
30import android.view.InputDevice;
31import android.view.MotionEvent;
32import android.view.View;
33import android.view.ViewConfiguration;
34import android.view.ViewTreeObserver;
35import android.view.animation.Interpolator;
36import android.widget.FrameLayout;
37
38import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
39import com.android.internal.util.LatencyTracker;
40import com.android.systemui.DejankUtils;
41import com.android.systemui.Dependency;
42import com.android.systemui.Interpolators;
43import com.android.systemui.R;
44import com.android.systemui.classifier.FalsingManager;
45import com.android.systemui.doze.DozeLog;
46import com.android.systemui.statusbar.FlingAnimationUtils;
47import com.android.systemui.statusbar.StatusBarState;
48import com.android.systemui.statusbar.VibratorHelper;
49
50import java.io.FileDescriptor;
51import java.io.PrintWriter;
52import java.util.function.BiConsumer;
53
54public abstract class PanelView extends FrameLayout {
55    public static final boolean DEBUG = PanelBar.DEBUG;
56    public static final String TAG = PanelView.class.getSimpleName();
57    private static final int INITIAL_OPENING_PEEK_DURATION = 200;
58    private static final int PEEK_ANIMATION_DURATION = 360;
59    private static final int NO_FIXED_DURATION = -1;
60    private long mDownTime;
61    private float mMinExpandHeight;
62    private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
63    private boolean mPanelUpdateWhenAnimatorEnds;
64    private boolean mVibrateOnOpening;
65    protected boolean mLaunchingNotification;
66    private int mFixedDuration = NO_FIXED_DURATION;
67    private BiConsumer<Float, Boolean> mExpansionListener;
68
69    private final void logf(String fmt, Object... args) {
70        Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
71    }
72
73    protected StatusBar mStatusBar;
74    protected HeadsUpManagerPhone mHeadsUpManager;
75
76    private float mPeekHeight;
77    private float mHintDistance;
78    private float mInitialOffsetOnTouch;
79    private boolean mCollapsedAndHeadsUpOnDown;
80    private float mExpandedFraction = 0;
81    protected float mExpandedHeight = 0;
82    private boolean mPanelClosedOnDown;
83    private boolean mHasLayoutedSinceDown;
84    private float mUpdateFlingVelocity;
85    private boolean mUpdateFlingOnLayout;
86    private boolean mPeekTouching;
87    private boolean mJustPeeked;
88    private boolean mClosing;
89    protected boolean mTracking;
90    private boolean mTouchSlopExceeded;
91    private int mTrackingPointer;
92    protected int mTouchSlop;
93    protected boolean mHintAnimationRunning;
94    private boolean mOverExpandedBeforeFling;
95    private boolean mTouchAboveFalsingThreshold;
96    private int mUnlockFalsingThreshold;
97    private boolean mTouchStartedInEmptyArea;
98    private boolean mMotionAborted;
99    private boolean mUpwardsWhenTresholdReached;
100    private boolean mAnimatingOnDown;
101
102    private ValueAnimator mHeightAnimator;
103    private ObjectAnimator mPeekAnimator;
104    private VelocityTrackerInterface mVelocityTracker;
105    private FlingAnimationUtils mFlingAnimationUtils;
106    private FlingAnimationUtils mFlingAnimationUtilsClosing;
107    private FlingAnimationUtils mFlingAnimationUtilsDismissing;
108    private FalsingManager mFalsingManager;
109    private final VibratorHelper mVibratorHelper;
110
111    /**
112     * Whether an instant expand request is currently pending and we are just waiting for layout.
113     */
114    private boolean mInstantExpanding;
115    private boolean mAnimateAfterExpanding;
116
117    PanelBar mBar;
118
119    private String mViewName;
120    private float mInitialTouchY;
121    private float mInitialTouchX;
122    private boolean mTouchDisabled;
123
124    /**
125     * Whether or not the PanelView can be expanded or collapsed with a drag.
126     */
127    private boolean mNotificationsDragEnabled;
128
129    private Interpolator mBounceInterpolator;
130    protected KeyguardBottomAreaView mKeyguardBottomArea;
131
132    /**
133     * Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time.
134     */
135    private float mNextCollapseSpeedUpFactor = 1.0f;
136
137    protected boolean mExpanding;
138    private boolean mGestureWaitForTouchSlop;
139    private boolean mIgnoreXTouchSlop;
140    private boolean mExpandLatencyTracking;
141
142    protected void onExpandingFinished() {
143        mBar.onExpandingFinished();
144    }
145
146    protected void onExpandingStarted() {
147    }
148
149    private void notifyExpandingStarted() {
150        if (!mExpanding) {
151            mExpanding = true;
152            onExpandingStarted();
153        }
154    }
155
156    protected final void notifyExpandingFinished() {
157        endClosing();
158        if (mExpanding) {
159            mExpanding = false;
160            onExpandingFinished();
161        }
162    }
163
164    private void runPeekAnimation(long duration, float peekHeight, boolean collapseWhenFinished) {
165        mPeekHeight = peekHeight;
166        if (DEBUG) logf("peek to height=%.1f", mPeekHeight);
167        if (mHeightAnimator != null) {
168            return;
169        }
170        if (mPeekAnimator != null) {
171            mPeekAnimator.cancel();
172        }
173        mPeekAnimator = ObjectAnimator.ofFloat(this, "expandedHeight", mPeekHeight)
174                .setDuration(duration);
175        mPeekAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
176        mPeekAnimator.addListener(new AnimatorListenerAdapter() {
177            private boolean mCancelled;
178
179            @Override
180            public void onAnimationCancel(Animator animation) {
181                mCancelled = true;
182            }
183
184            @Override
185            public void onAnimationEnd(Animator animation) {
186                mPeekAnimator = null;
187                if (!mCancelled && collapseWhenFinished) {
188                    postOnAnimation(mPostCollapseRunnable);
189                }
190
191            }
192        });
193        notifyExpandingStarted();
194        mPeekAnimator.start();
195        mJustPeeked = true;
196    }
197
198    public PanelView(Context context, AttributeSet attrs) {
199        super(context, attrs);
200        mFlingAnimationUtils = new FlingAnimationUtils(context, 0.6f /* maxLengthSeconds */,
201                0.6f /* speedUpFactor */);
202        mFlingAnimationUtilsClosing = new FlingAnimationUtils(context, 0.5f /* maxLengthSeconds */,
203                0.6f /* speedUpFactor */);
204        mFlingAnimationUtilsDismissing = new FlingAnimationUtils(context,
205                0.5f /* maxLengthSeconds */, 0.2f /* speedUpFactor */, 0.6f /* x2 */,
206                0.84f /* y2 */);
207        mBounceInterpolator = new BounceInterpolator();
208        mFalsingManager = FalsingManager.getInstance(context);
209        mNotificationsDragEnabled =
210                getResources().getBoolean(R.bool.config_enableNotificationShadeDrag);
211        mVibratorHelper = Dependency.get(VibratorHelper.class);
212        mVibrateOnOpening = mContext.getResources().getBoolean(
213                R.bool.config_vibrateOnIconAnimation);
214    }
215
216    protected void loadDimens() {
217        final Resources res = getContext().getResources();
218        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
219        mTouchSlop = configuration.getScaledTouchSlop();
220        mHintDistance = res.getDimension(R.dimen.hint_move_distance);
221        mUnlockFalsingThreshold = res.getDimensionPixelSize(R.dimen.unlock_falsing_threshold);
222    }
223
224    private void trackMovement(MotionEvent event) {
225        // Add movement to velocity tracker using raw screen X and Y coordinates instead
226        // of window coordinates because the window frame may be moving at the same time.
227        float deltaX = event.getRawX() - event.getX();
228        float deltaY = event.getRawY() - event.getY();
229        event.offsetLocation(deltaX, deltaY);
230        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
231        event.offsetLocation(-deltaX, -deltaY);
232    }
233
234    public void setTouchDisabled(boolean disabled) {
235        mTouchDisabled = disabled;
236        if (mTouchDisabled) {
237            cancelHeightAnimator();
238            if (mTracking) {
239                onTrackingStopped(true /* expanded */);
240            }
241            notifyExpandingFinished();
242        }
243    }
244
245    public void startExpandLatencyTracking() {
246        if (LatencyTracker.isEnabled(mContext)) {
247            LatencyTracker.getInstance(mContext).onActionStart(
248                    LatencyTracker.ACTION_EXPAND_PANEL);
249            mExpandLatencyTracking = true;
250        }
251    }
252
253    @Override
254    public boolean onTouchEvent(MotionEvent event) {
255        if (mInstantExpanding
256                || (mTouchDisabled && event.getActionMasked() != MotionEvent.ACTION_CANCEL)
257                || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
258            return false;
259        }
260
261        // If dragging should not expand the notifications shade, then return false.
262        if (!mNotificationsDragEnabled) {
263            if (mTracking) {
264                // Turn off tracking if it's on or the shade can get stuck in the down position.
265                onTrackingStopped(true /* expand */);
266            }
267            return false;
268        }
269
270        // On expanding, single mouse click expands the panel instead of dragging.
271        if (isFullyCollapsed() && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
272            if (event.getAction() == MotionEvent.ACTION_UP) {
273                expand(true);
274            }
275            return true;
276        }
277
278        /*
279         * We capture touch events here and update the expand height here in case according to
280         * the users fingers. This also handles multi-touch.
281         *
282         * If the user just clicks shortly, we show a quick peek of the shade.
283         *
284         * Flinging is also enabled in order to open or close the shade.
285         */
286
287        int pointerIndex = event.findPointerIndex(mTrackingPointer);
288        if (pointerIndex < 0) {
289            pointerIndex = 0;
290            mTrackingPointer = event.getPointerId(pointerIndex);
291        }
292        final float x = event.getX(pointerIndex);
293        final float y = event.getY(pointerIndex);
294
295        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
296            mGestureWaitForTouchSlop = isFullyCollapsed() || hasConflictingGestures();
297            mIgnoreXTouchSlop = isFullyCollapsed() || shouldGestureIgnoreXTouchSlop(x, y);
298        }
299
300        switch (event.getActionMasked()) {
301            case MotionEvent.ACTION_DOWN:
302                startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
303                mJustPeeked = false;
304                mMinExpandHeight = 0.0f;
305                mPanelClosedOnDown = isFullyCollapsed();
306                mHasLayoutedSinceDown = false;
307                mUpdateFlingOnLayout = false;
308                mMotionAborted = false;
309                mPeekTouching = mPanelClosedOnDown;
310                mDownTime = SystemClock.uptimeMillis();
311                mTouchAboveFalsingThreshold = false;
312                mCollapsedAndHeadsUpOnDown = isFullyCollapsed()
313                        && mHeadsUpManager.hasPinnedHeadsUp();
314                if (mVelocityTracker == null) {
315                    initVelocityTracker();
316                }
317                trackMovement(event);
318                if (!mGestureWaitForTouchSlop || (mHeightAnimator != null && !mHintAnimationRunning)
319                        || mPeekAnimator != null) {
320                    mTouchSlopExceeded = (mHeightAnimator != null && !mHintAnimationRunning)
321                            || mPeekAnimator != null;
322                    cancelHeightAnimator();
323                    cancelPeek();
324                    onTrackingStarted();
325                }
326                if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp()
327                        && !mStatusBar.isBouncerShowing()) {
328                    startOpening(event);
329                }
330                break;
331
332            case MotionEvent.ACTION_POINTER_UP:
333                final int upPointer = event.getPointerId(event.getActionIndex());
334                if (mTrackingPointer == upPointer) {
335                    // gesture is ongoing, find a new pointer to track
336                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
337                    final float newY = event.getY(newIndex);
338                    final float newX = event.getX(newIndex);
339                    mTrackingPointer = event.getPointerId(newIndex);
340                    startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight);
341                }
342                break;
343            case MotionEvent.ACTION_POINTER_DOWN:
344                if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
345                    mMotionAborted = true;
346                    endMotionEvent(event, x, y, true /* forceCancel */);
347                    return false;
348                }
349                break;
350            case MotionEvent.ACTION_MOVE:
351                trackMovement(event);
352                float h = y - mInitialTouchY;
353
354                // If the panel was collapsed when touching, we only need to check for the
355                // y-component of the gesture, as we have no conflicting horizontal gesture.
356                if (Math.abs(h) > mTouchSlop
357                        && (Math.abs(h) > Math.abs(x - mInitialTouchX)
358                        || mIgnoreXTouchSlop)) {
359                    mTouchSlopExceeded = true;
360                    if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) {
361                        if (!mJustPeeked && mInitialOffsetOnTouch != 0f) {
362                            startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
363                            h = 0;
364                        }
365                        cancelHeightAnimator();
366                        onTrackingStarted();
367                    }
368                }
369                float newHeight = Math.max(0, h + mInitialOffsetOnTouch);
370                if (newHeight > mPeekHeight) {
371                    if (mPeekAnimator != null) {
372                        mPeekAnimator.cancel();
373                    }
374                    mJustPeeked = false;
375                } else if (mPeekAnimator == null && mJustPeeked) {
376                    // The initial peek has finished, but we haven't dragged as far yet, lets
377                    // speed it up by starting at the peek height.
378                    mInitialOffsetOnTouch = mExpandedHeight;
379                    mInitialTouchY = y;
380                    mMinExpandHeight = mExpandedHeight;
381                    mJustPeeked = false;
382                }
383                newHeight = Math.max(newHeight, mMinExpandHeight);
384                if (-h >= getFalsingThreshold()) {
385                    mTouchAboveFalsingThreshold = true;
386                    mUpwardsWhenTresholdReached = isDirectionUpwards(x, y);
387                }
388                if (!mJustPeeked && (!mGestureWaitForTouchSlop || mTracking) &&
389                        !isTrackingBlocked()) {
390                    setExpandedHeightInternal(newHeight);
391                }
392                break;
393
394            case MotionEvent.ACTION_UP:
395            case MotionEvent.ACTION_CANCEL:
396                trackMovement(event);
397                endMotionEvent(event, x, y, false /* forceCancel */);
398                break;
399        }
400        return !mGestureWaitForTouchSlop || mTracking;
401    }
402
403    private void startOpening(MotionEvent event) {
404        runPeekAnimation(INITIAL_OPENING_PEEK_DURATION, getOpeningHeight(),
405                false /* collapseWhenFinished */);
406        notifyBarPanelExpansionChanged();
407        if (mVibrateOnOpening) {
408            mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
409        }
410
411        //TODO: keyguard opens QS a different way; log that too?
412
413        // Log the position of the swipe that opened the panel
414        float width = mStatusBar.getDisplayWidth();
415        float height = mStatusBar.getDisplayHeight();
416        int rot = mStatusBar.getRotation();
417
418        mLockscreenGestureLogger.writeAtFractionalPosition(MetricsEvent.ACTION_PANEL_VIEW_EXPAND,
419                (int) (event.getX() / width * 100),
420                (int) (event.getY() / height * 100),
421                rot);
422    }
423
424    protected abstract float getOpeningHeight();
425
426    /**
427     * @return whether the swiping direction is upwards and above a 45 degree angle compared to the
428     * horizontal direction
429     */
430    private boolean isDirectionUpwards(float x, float y) {
431        float xDiff = x - mInitialTouchX;
432        float yDiff = y - mInitialTouchY;
433        if (yDiff >= 0) {
434            return false;
435        }
436        return Math.abs(yDiff) >= Math.abs(xDiff);
437    }
438
439    protected void startExpandingFromPeek() {
440        mStatusBar.handlePeekToExpandTransistion();
441    }
442
443    protected void startExpandMotion(float newX, float newY, boolean startTracking,
444            float expandedHeight) {
445        mInitialOffsetOnTouch = expandedHeight;
446        mInitialTouchY = newY;
447        mInitialTouchX = newX;
448        if (startTracking) {
449            mTouchSlopExceeded = true;
450            setExpandedHeight(mInitialOffsetOnTouch);
451            onTrackingStarted();
452        }
453    }
454
455    private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) {
456        mTrackingPointer = -1;
457        if ((mTracking && mTouchSlopExceeded)
458                || Math.abs(x - mInitialTouchX) > mTouchSlop
459                || Math.abs(y - mInitialTouchY) > mTouchSlop
460                || event.getActionMasked() == MotionEvent.ACTION_CANCEL
461                || forceCancel) {
462            float vel = 0f;
463            float vectorVel = 0f;
464            if (mVelocityTracker != null) {
465                mVelocityTracker.computeCurrentVelocity(1000);
466                vel = mVelocityTracker.getYVelocity();
467                vectorVel = (float) Math.hypot(
468                        mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
469            }
470            boolean expand = flingExpands(vel, vectorVel, x, y)
471                    || event.getActionMasked() == MotionEvent.ACTION_CANCEL
472                    || forceCancel;
473            DozeLog.traceFling(expand, mTouchAboveFalsingThreshold,
474                    mStatusBar.isFalsingThresholdNeeded(),
475                    mStatusBar.isWakeUpComingFromTouch());
476                    // Log collapse gesture if on lock screen.
477                    if (!expand && mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
478                        float displayDensity = mStatusBar.getDisplayDensity();
479                        int heightDp = (int) Math.abs((y - mInitialTouchY) / displayDensity);
480                        int velocityDp = (int) Math.abs(vel / displayDensity);
481                        mLockscreenGestureLogger.write(
482                                MetricsEvent.ACTION_LS_UNLOCK,
483                                heightDp, velocityDp);
484                    }
485            fling(vel, expand, isFalseTouch(x, y));
486            onTrackingStopped(expand);
487            mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown;
488            if (mUpdateFlingOnLayout) {
489                mUpdateFlingVelocity = vel;
490            }
491        } else if (mPanelClosedOnDown && !mHeadsUpManager.hasPinnedHeadsUp() && !mTracking
492                && !mStatusBar.isBouncerShowing() && !mStatusBar.isKeyguardFadingAway()) {
493            long timePassed = SystemClock.uptimeMillis() - mDownTime;
494            if (timePassed < ViewConfiguration.getLongPressTimeout()) {
495                // Lets show the user that he can actually expand the panel
496                runPeekAnimation(PEEK_ANIMATION_DURATION, getPeekHeight(), true /* collapseWhenFinished */);
497            } else {
498                // We need to collapse the panel since we peeked to the small height.
499                postOnAnimation(mPostCollapseRunnable);
500            }
501        } else if (!mStatusBar.isBouncerShowing()) {
502            boolean expands = onEmptySpaceClick(mInitialTouchX);
503            onTrackingStopped(expands);
504        }
505
506        if (mVelocityTracker != null) {
507            mVelocityTracker.recycle();
508            mVelocityTracker = null;
509        }
510        mPeekTouching = false;
511    }
512
513    protected float getCurrentExpandVelocity() {
514        if (mVelocityTracker == null) {
515            return 0;
516        }
517        mVelocityTracker.computeCurrentVelocity(1000);
518        return mVelocityTracker.getYVelocity();
519    }
520
521    private int getFalsingThreshold() {
522        float factor = mStatusBar.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
523        return (int) (mUnlockFalsingThreshold * factor);
524    }
525
526    protected abstract boolean hasConflictingGestures();
527
528    protected abstract boolean shouldGestureIgnoreXTouchSlop(float x, float y);
529
530    protected void onTrackingStopped(boolean expand) {
531        mTracking = false;
532        mBar.onTrackingStopped(expand);
533        notifyBarPanelExpansionChanged();
534    }
535
536    protected void onTrackingStarted() {
537        endClosing();
538        mTracking = true;
539        mBar.onTrackingStarted();
540        notifyExpandingStarted();
541        notifyBarPanelExpansionChanged();
542    }
543
544    @Override
545    public boolean onInterceptTouchEvent(MotionEvent event) {
546        if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled
547                || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
548            return false;
549        }
550
551        /*
552         * If the user drags anywhere inside the panel we intercept it if the movement is
553         * upwards. This allows closing the shade from anywhere inside the panel.
554         *
555         * We only do this if the current content is scrolled to the bottom,
556         * i.e isScrolledToBottom() is true and therefore there is no conflicting scrolling gesture
557         * possible.
558         */
559        int pointerIndex = event.findPointerIndex(mTrackingPointer);
560        if (pointerIndex < 0) {
561            pointerIndex = 0;
562            mTrackingPointer = event.getPointerId(pointerIndex);
563        }
564        final float x = event.getX(pointerIndex);
565        final float y = event.getY(pointerIndex);
566        boolean scrolledToBottom = isScrolledToBottom();
567
568        switch (event.getActionMasked()) {
569            case MotionEvent.ACTION_DOWN:
570                mStatusBar.userActivity();
571                mAnimatingOnDown = mHeightAnimator != null;
572                mMinExpandHeight = 0.0f;
573                mDownTime = SystemClock.uptimeMillis();
574                if (mAnimatingOnDown && mClosing && !mHintAnimationRunning
575                        || mPeekAnimator != null) {
576                    cancelHeightAnimator();
577                    cancelPeek();
578                    mTouchSlopExceeded = true;
579                    return true;
580                }
581                mInitialTouchY = y;
582                mInitialTouchX = x;
583                mTouchStartedInEmptyArea = !isInContentBounds(x, y);
584                mTouchSlopExceeded = false;
585                mJustPeeked = false;
586                mMotionAborted = false;
587                mPanelClosedOnDown = isFullyCollapsed();
588                mCollapsedAndHeadsUpOnDown = false;
589                mHasLayoutedSinceDown = false;
590                mUpdateFlingOnLayout = false;
591                mTouchAboveFalsingThreshold = false;
592                initVelocityTracker();
593                trackMovement(event);
594                break;
595            case MotionEvent.ACTION_POINTER_UP:
596                final int upPointer = event.getPointerId(event.getActionIndex());
597                if (mTrackingPointer == upPointer) {
598                    // gesture is ongoing, find a new pointer to track
599                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
600                    mTrackingPointer = event.getPointerId(newIndex);
601                    mInitialTouchX = event.getX(newIndex);
602                    mInitialTouchY = event.getY(newIndex);
603                }
604                break;
605            case MotionEvent.ACTION_POINTER_DOWN:
606                if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
607                    mMotionAborted = true;
608                    if (mVelocityTracker != null) {
609                        mVelocityTracker.recycle();
610                        mVelocityTracker = null;
611                    }
612                }
613                break;
614            case MotionEvent.ACTION_MOVE:
615                final float h = y - mInitialTouchY;
616                trackMovement(event);
617                if (scrolledToBottom || mTouchStartedInEmptyArea || mAnimatingOnDown) {
618                    float hAbs = Math.abs(h);
619                    if ((h < -mTouchSlop || (mAnimatingOnDown && hAbs > mTouchSlop))
620                            && hAbs > Math.abs(x - mInitialTouchX)) {
621                        cancelHeightAnimator();
622                        startExpandMotion(x, y, true /* startTracking */, mExpandedHeight);
623                        return true;
624                    }
625                }
626                break;
627            case MotionEvent.ACTION_CANCEL:
628            case MotionEvent.ACTION_UP:
629                if (mVelocityTracker != null) {
630                    mVelocityTracker.recycle();
631                    mVelocityTracker = null;
632                }
633                break;
634        }
635        return false;
636    }
637
638    /**
639     * @return Whether a pair of coordinates are inside the visible view content bounds.
640     */
641    protected abstract boolean isInContentBounds(float x, float y);
642
643    protected void cancelHeightAnimator() {
644        if (mHeightAnimator != null) {
645            if (mHeightAnimator.isRunning()) {
646                mPanelUpdateWhenAnimatorEnds = false;
647            }
648            mHeightAnimator.cancel();
649        }
650        endClosing();
651    }
652
653    private void endClosing() {
654        if (mClosing) {
655            mClosing = false;
656            onClosingFinished();
657        }
658    }
659
660    private void initVelocityTracker() {
661        if (mVelocityTracker != null) {
662            mVelocityTracker.recycle();
663        }
664        mVelocityTracker = VelocityTrackerFactory.obtain(getContext());
665    }
666
667    protected boolean isScrolledToBottom() {
668        return true;
669    }
670
671    protected float getContentHeight() {
672        return mExpandedHeight;
673    }
674
675    @Override
676    protected void onFinishInflate() {
677        super.onFinishInflate();
678        loadDimens();
679    }
680
681    @Override
682    protected void onConfigurationChanged(Configuration newConfig) {
683        super.onConfigurationChanged(newConfig);
684        loadDimens();
685    }
686
687    /**
688     * @param vel the current vertical velocity of the motion
689     * @param vectorVel the length of the vectorial velocity
690     * @return whether a fling should expands the panel; contracts otherwise
691     */
692    protected boolean flingExpands(float vel, float vectorVel, float x, float y) {
693        if (isFalseTouch(x, y)) {
694            return true;
695        }
696        if (Math.abs(vectorVel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
697            return getExpandedFraction() > 0.5f;
698        } else {
699            return vel > 0;
700        }
701    }
702
703    /**
704     * @param x the final x-coordinate when the finger was lifted
705     * @param y the final y-coordinate when the finger was lifted
706     * @return whether this motion should be regarded as a false touch
707     */
708    private boolean isFalseTouch(float x, float y) {
709        if (!mStatusBar.isFalsingThresholdNeeded()) {
710            return false;
711        }
712        if (mFalsingManager.isClassiferEnabled()) {
713            return mFalsingManager.isFalseTouch();
714        }
715        if (!mTouchAboveFalsingThreshold) {
716            return true;
717        }
718        if (mUpwardsWhenTresholdReached) {
719            return false;
720        }
721        return !isDirectionUpwards(x, y);
722    }
723
724    protected void fling(float vel, boolean expand) {
725        fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false);
726    }
727
728    protected void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) {
729        fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing);
730    }
731
732    protected void fling(float vel, boolean expand, float collapseSpeedUpFactor,
733            boolean expandBecauseOfFalsing) {
734        cancelPeek();
735        float target = expand ? getMaxPanelHeight() : 0;
736        if (!expand) {
737            mClosing = true;
738        }
739        flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
740    }
741
742    protected void flingToHeight(float vel, boolean expand, float target,
743            float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
744        // Hack to make the expand transition look nice when clear all button is visible - we make
745        // the animation only to the last notification, and then jump to the maximum panel height so
746        // clear all just fades in and the decelerating motion is towards the last notification.
747        final boolean clearAllExpandHack = expand && fullyExpandedClearAllVisible()
748                && mExpandedHeight < getMaxPanelHeight() - getClearAllHeight()
749                && !isClearAllVisible();
750        if (clearAllExpandHack) {
751            target = getMaxPanelHeight() - getClearAllHeight();
752        }
753        if (target == mExpandedHeight || getOverExpansionAmount() > 0f && expand) {
754            notifyExpandingFinished();
755            return;
756        }
757        mOverExpandedBeforeFling = getOverExpansionAmount() > 0f;
758        ValueAnimator animator = createHeightAnimator(target);
759        if (expand) {
760            if (expandBecauseOfFalsing && vel < 0) {
761                vel = 0;
762            }
763            mFlingAnimationUtils.apply(animator, mExpandedHeight, target, vel, getHeight());
764            if (vel == 0) {
765                animator.setDuration(350);
766            }
767        } else {
768            if (shouldUseDismissingAnimation()) {
769                if (vel == 0) {
770                    animator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED);
771                    long duration = (long) (200 + mExpandedHeight / getHeight() * 100);
772                    animator.setDuration(duration);
773                } else {
774                    mFlingAnimationUtilsDismissing.apply(animator, mExpandedHeight, target, vel,
775                            getHeight());
776                }
777            } else {
778                mFlingAnimationUtilsClosing
779                        .apply(animator, mExpandedHeight, target, vel, getHeight());
780            }
781
782            // Make it shorter if we run a canned animation
783            if (vel == 0) {
784                animator.setDuration((long) (animator.getDuration() / collapseSpeedUpFactor));
785            }
786            if (mFixedDuration != NO_FIXED_DURATION) {
787                animator.setDuration(mFixedDuration);
788            }
789        }
790        animator.addListener(new AnimatorListenerAdapter() {
791            private boolean mCancelled;
792
793            @Override
794            public void onAnimationCancel(Animator animation) {
795                mCancelled = true;
796            }
797
798            @Override
799            public void onAnimationEnd(Animator animation) {
800                if (clearAllExpandHack && !mCancelled) {
801                    setExpandedHeightInternal(getMaxPanelHeight());
802                }
803                setAnimator(null);
804                if (!mCancelled) {
805                    notifyExpandingFinished();
806                }
807                notifyBarPanelExpansionChanged();
808            }
809        });
810        setAnimator(animator);
811        animator.start();
812    }
813
814    protected abstract boolean shouldUseDismissingAnimation();
815
816    @Override
817    protected void onAttachedToWindow() {
818        super.onAttachedToWindow();
819        mViewName = getResources().getResourceName(getId());
820    }
821
822    public String getName() {
823        return mViewName;
824    }
825
826    public void setExpandedHeight(float height) {
827        if (DEBUG) logf("setExpandedHeight(%.1f)", height);
828        setExpandedHeightInternal(height + getOverExpansionPixels());
829    }
830
831    @Override
832    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
833        super.onLayout(changed, left, top, right, bottom);
834        mStatusBar.onPanelLaidOut();
835        requestPanelHeightUpdate();
836        mHasLayoutedSinceDown = true;
837        if (mUpdateFlingOnLayout) {
838            abortAnimations();
839            fling(mUpdateFlingVelocity, true /* expands */);
840            mUpdateFlingOnLayout = false;
841        }
842    }
843
844    protected void requestPanelHeightUpdate() {
845        float currentMaxPanelHeight = getMaxPanelHeight();
846
847        if (isFullyCollapsed()) {
848            return;
849        }
850
851        if (currentMaxPanelHeight == mExpandedHeight) {
852            return;
853        }
854
855        if (mPeekAnimator != null || mPeekTouching) {
856            return;
857        }
858
859        if (mTracking && !isTrackingBlocked()) {
860            return;
861        }
862
863        if (mHeightAnimator != null) {
864            mPanelUpdateWhenAnimatorEnds = true;
865            return;
866        }
867
868        setExpandedHeight(currentMaxPanelHeight);
869    }
870
871    public void setExpandedHeightInternal(float h) {
872        if (mExpandLatencyTracking && h != 0f) {
873            DejankUtils.postAfterTraversal(() -> LatencyTracker.getInstance(mContext).onActionEnd(
874                    LatencyTracker.ACTION_EXPAND_PANEL));
875            mExpandLatencyTracking = false;
876        }
877        float fhWithoutOverExpansion = getMaxPanelHeight() - getOverExpansionAmount();
878        if (mHeightAnimator == null) {
879            float overExpansionPixels = Math.max(0, h - fhWithoutOverExpansion);
880            if (getOverExpansionPixels() != overExpansionPixels && mTracking) {
881                setOverExpansion(overExpansionPixels, true /* isPixels */);
882            }
883            mExpandedHeight = Math.min(h, fhWithoutOverExpansion) + getOverExpansionAmount();
884        } else {
885            mExpandedHeight = h;
886            if (mOverExpandedBeforeFling) {
887                setOverExpansion(Math.max(0, h - fhWithoutOverExpansion), false /* isPixels */);
888            }
889        }
890
891        // If we are closing the panel and we are almost there due to a slow decelerating
892        // interpolator, abort the animation.
893        if (mExpandedHeight < 1f && mExpandedHeight != 0f && mClosing) {
894            mExpandedHeight = 0f;
895            if (mHeightAnimator != null) {
896                mHeightAnimator.end();
897            }
898        }
899        mExpandedFraction = Math.min(1f,
900                fhWithoutOverExpansion == 0 ? 0 : mExpandedHeight / fhWithoutOverExpansion);
901        onHeightUpdated(mExpandedHeight);
902        notifyBarPanelExpansionChanged();
903    }
904
905    /**
906     * @return true if the panel tracking should be temporarily blocked; this is used when a
907     *         conflicting gesture (opening QS) is happening
908     */
909    protected abstract boolean isTrackingBlocked();
910
911    protected abstract void setOverExpansion(float overExpansion, boolean isPixels);
912
913    protected abstract void onHeightUpdated(float expandedHeight);
914
915    protected abstract float getOverExpansionAmount();
916
917    protected abstract float getOverExpansionPixels();
918
919    /**
920     * This returns the maximum height of the panel. Children should override this if their
921     * desired height is not the full height.
922     *
923     * @return the default implementation simply returns the maximum height.
924     */
925    protected abstract int getMaxPanelHeight();
926
927    public void setExpandedFraction(float frac) {
928        setExpandedHeight(getMaxPanelHeight() * frac);
929    }
930
931    public float getExpandedHeight() {
932        return mExpandedHeight;
933    }
934
935    public float getExpandedFraction() {
936        return mExpandedFraction;
937    }
938
939    public boolean isFullyExpanded() {
940        return mExpandedHeight >= getMaxPanelHeight();
941    }
942
943    public boolean isFullyCollapsed() {
944        return mExpandedFraction <= 0.0f;
945    }
946
947    public boolean isCollapsing() {
948        return mClosing || mLaunchingNotification;
949    }
950
951    public boolean isTracking() {
952        return mTracking;
953    }
954
955    public void setBar(PanelBar panelBar) {
956        mBar = panelBar;
957    }
958
959    public void collapse(boolean delayed, float speedUpFactor) {
960        if (DEBUG) logf("collapse: " + this);
961        if (canPanelBeCollapsed()) {
962            cancelHeightAnimator();
963            notifyExpandingStarted();
964
965            // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state.
966            mClosing = true;
967            if (delayed) {
968                mNextCollapseSpeedUpFactor = speedUpFactor;
969                postDelayed(mFlingCollapseRunnable, 120);
970            } else {
971                fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */);
972            }
973        }
974    }
975
976    public boolean canPanelBeCollapsed() {
977        return !isFullyCollapsed() && !mTracking && !mClosing;
978    }
979
980    private final Runnable mFlingCollapseRunnable = new Runnable() {
981        @Override
982        public void run() {
983            fling(0, false /* expand */, mNextCollapseSpeedUpFactor,
984                    false /* expandBecauseOfFalsing */);
985        }
986    };
987
988    public void cancelPeek() {
989        boolean cancelled = false;
990        if (mPeekAnimator != null) {
991            cancelled = true;
992            mPeekAnimator.cancel();
993        }
994
995        if (cancelled) {
996            // When peeking, we already tell mBar that we expanded ourselves. Make sure that we also
997            // notify mBar that we might have closed ourselves.
998            notifyBarPanelExpansionChanged();
999        }
1000    }
1001
1002    public void expand(final boolean animate) {
1003        if (!isFullyCollapsed() && !isCollapsing()) {
1004            return;
1005        }
1006
1007        mInstantExpanding = true;
1008        mAnimateAfterExpanding = animate;
1009        mUpdateFlingOnLayout = false;
1010        abortAnimations();
1011        cancelPeek();
1012        if (mTracking) {
1013            onTrackingStopped(true /* expands */); // The panel is expanded after this call.
1014        }
1015        if (mExpanding) {
1016            notifyExpandingFinished();
1017        }
1018        notifyBarPanelExpansionChanged();
1019
1020        // Wait for window manager to pickup the change, so we know the maximum height of the panel
1021        // then.
1022        getViewTreeObserver().addOnGlobalLayoutListener(
1023                new ViewTreeObserver.OnGlobalLayoutListener() {
1024                    @Override
1025                    public void onGlobalLayout() {
1026                        if (!mInstantExpanding) {
1027                            getViewTreeObserver().removeOnGlobalLayoutListener(this);
1028                            return;
1029                        }
1030                        if (mStatusBar.getStatusBarWindow().getHeight()
1031                                != mStatusBar.getStatusBarHeight()) {
1032                            getViewTreeObserver().removeOnGlobalLayoutListener(this);
1033                            if (mAnimateAfterExpanding) {
1034                                notifyExpandingStarted();
1035                                fling(0, true /* expand */);
1036                            } else {
1037                                setExpandedFraction(1f);
1038                            }
1039                            mInstantExpanding = false;
1040                        }
1041                    }
1042                });
1043
1044        // Make sure a layout really happens.
1045        requestLayout();
1046    }
1047
1048    public void instantCollapse() {
1049        abortAnimations();
1050        setExpandedFraction(0f);
1051        if (mExpanding) {
1052            notifyExpandingFinished();
1053        }
1054        if (mInstantExpanding) {
1055            mInstantExpanding = false;
1056            notifyBarPanelExpansionChanged();
1057        }
1058    }
1059
1060    private void abortAnimations() {
1061        cancelPeek();
1062        cancelHeightAnimator();
1063        removeCallbacks(mPostCollapseRunnable);
1064        removeCallbacks(mFlingCollapseRunnable);
1065    }
1066
1067    protected void onClosingFinished() {
1068        mBar.onClosingFinished();
1069    }
1070
1071
1072    protected void startUnlockHintAnimation() {
1073
1074        // We don't need to hint the user if an animation is already running or the user is changing
1075        // the expansion.
1076        if (mHeightAnimator != null || mTracking) {
1077            return;
1078        }
1079        cancelPeek();
1080        notifyExpandingStarted();
1081        startUnlockHintAnimationPhase1(() -> {
1082            notifyExpandingFinished();
1083            onUnlockHintFinished();
1084            mHintAnimationRunning = false;
1085        });
1086        onUnlockHintStarted();
1087        mHintAnimationRunning = true;
1088    }
1089
1090    protected void onUnlockHintFinished() {
1091        mStatusBar.onHintFinished();
1092    }
1093
1094    protected void onUnlockHintStarted() {
1095        mStatusBar.onUnlockHintStarted();
1096    }
1097
1098    public boolean isUnlockHintRunning() {
1099        return mHintAnimationRunning;
1100    }
1101
1102    /**
1103     * Phase 1: Move everything upwards.
1104     */
1105    private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) {
1106        float target = Math.max(0, getMaxPanelHeight() - mHintDistance);
1107        ValueAnimator animator = createHeightAnimator(target);
1108        animator.setDuration(250);
1109        animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
1110        animator.addListener(new AnimatorListenerAdapter() {
1111            private boolean mCancelled;
1112
1113            @Override
1114            public void onAnimationCancel(Animator animation) {
1115                mCancelled = true;
1116            }
1117
1118            @Override
1119            public void onAnimationEnd(Animator animation) {
1120                if (mCancelled) {
1121                    setAnimator(null);
1122                    onAnimationFinished.run();
1123                } else {
1124                    startUnlockHintAnimationPhase2(onAnimationFinished);
1125                }
1126            }
1127        });
1128        animator.start();
1129        setAnimator(animator);
1130
1131        View[] viewsToAnimate = {
1132                mKeyguardBottomArea.getIndicationArea(),
1133                mStatusBar.getAmbientIndicationContainer()};
1134        for (View v : viewsToAnimate) {
1135            if (v == null) {
1136                continue;
1137            }
1138            v.animate()
1139                    .translationY(-mHintDistance)
1140                    .setDuration(250)
1141                    .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
1142                    .withEndAction(() -> v.animate()
1143                            .translationY(0)
1144                            .setDuration(450)
1145                            .setInterpolator(mBounceInterpolator)
1146                            .start())
1147                    .start();
1148        }
1149    }
1150
1151    private void setAnimator(ValueAnimator animator) {
1152        mHeightAnimator = animator;
1153        if (animator == null && mPanelUpdateWhenAnimatorEnds) {
1154            mPanelUpdateWhenAnimatorEnds = false;
1155            requestPanelHeightUpdate();
1156        }
1157    }
1158
1159    /**
1160     * Phase 2: Bounce down.
1161     */
1162    private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) {
1163        ValueAnimator animator = createHeightAnimator(getMaxPanelHeight());
1164        animator.setDuration(450);
1165        animator.setInterpolator(mBounceInterpolator);
1166        animator.addListener(new AnimatorListenerAdapter() {
1167            @Override
1168            public void onAnimationEnd(Animator animation) {
1169                setAnimator(null);
1170                onAnimationFinished.run();
1171                notifyBarPanelExpansionChanged();
1172            }
1173        });
1174        animator.start();
1175        setAnimator(animator);
1176    }
1177
1178    private ValueAnimator createHeightAnimator(float targetHeight) {
1179        ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight);
1180        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1181            @Override
1182            public void onAnimationUpdate(ValueAnimator animation) {
1183                setExpandedHeightInternal((Float) animation.getAnimatedValue());
1184            }
1185        });
1186        return animator;
1187    }
1188
1189    protected void notifyBarPanelExpansionChanged() {
1190        mBar.panelExpansionChanged(mExpandedFraction, mExpandedFraction > 0f
1191                || mPeekAnimator != null || mInstantExpanding || isPanelVisibleBecauseOfHeadsUp()
1192                || mTracking || mHeightAnimator != null);
1193        if (mExpansionListener != null) {
1194            mExpansionListener.accept(mExpandedFraction, mTracking);
1195        }
1196    }
1197
1198    public void setExpansionListener(BiConsumer<Float, Boolean> consumer) {
1199        mExpansionListener = consumer;
1200    }
1201
1202    protected abstract boolean isPanelVisibleBecauseOfHeadsUp();
1203
1204    /**
1205     * Gets called when the user performs a click anywhere in the empty area of the panel.
1206     *
1207     * @return whether the panel will be expanded after the action performed by this method
1208     */
1209    protected boolean onEmptySpaceClick(float x) {
1210        if (mHintAnimationRunning) {
1211            return true;
1212        }
1213        return onMiddleClicked();
1214    }
1215
1216    protected final Runnable mPostCollapseRunnable = new Runnable() {
1217        @Override
1218        public void run() {
1219            collapse(false /* delayed */, 1.0f /* speedUpFactor */);
1220        }
1221    };
1222
1223    protected abstract boolean onMiddleClicked();
1224
1225    protected abstract boolean isDozing();
1226
1227    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1228        pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s"
1229                + " tracking=%s justPeeked=%s peekAnim=%s%s timeAnim=%s%s touchDisabled=%s"
1230                + "]",
1231                this.getClass().getSimpleName(),
1232                getExpandedHeight(),
1233                getMaxPanelHeight(),
1234                mClosing?"T":"f",
1235                mTracking?"T":"f",
1236                mJustPeeked?"T":"f",
1237                mPeekAnimator, ((mPeekAnimator!=null && mPeekAnimator.isStarted())?" (started)":""),
1238                mHeightAnimator, ((mHeightAnimator !=null && mHeightAnimator.isStarted())?" (started)":""),
1239                mTouchDisabled?"T":"f"
1240        ));
1241    }
1242
1243    public abstract void resetViews();
1244
1245    protected abstract float getPeekHeight();
1246    /**
1247     * @return whether "Clear all" button will be visible when the panel is fully expanded
1248     */
1249    protected abstract boolean fullyExpandedClearAllVisible();
1250
1251    protected abstract boolean isClearAllVisible();
1252
1253    /**
1254     * @return the height of the clear all button, in pixels
1255     */
1256    protected abstract int getClearAllHeight();
1257
1258    public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) {
1259        mHeadsUpManager = headsUpManager;
1260    }
1261
1262    public void setLaunchingNotification(boolean launchingNotification) {
1263        mLaunchingNotification = launchingNotification;
1264    }
1265
1266    public void collapseWithDuration(int animationDuration) {
1267        mFixedDuration = animationDuration;
1268        collapse(false /* delayed */, 1.0f /* speedUpFactor */);
1269        mFixedDuration = NO_FIXED_DURATION;
1270    }
1271}
1272