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