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