NotificationPanelView.java revision d09def75110ea9833e3985ce33a878a471a8a0dc
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.PropertyValuesHolder;
23import android.animation.ValueAnimator;
24import android.content.Context;
25import android.content.res.Configuration;
26import android.util.AttributeSet;
27import android.util.MathUtils;
28import android.view.MotionEvent;
29import android.view.VelocityTracker;
30import android.view.View;
31import android.view.ViewTreeObserver;
32import android.view.accessibility.AccessibilityEvent;
33import android.view.animation.AnimationUtils;
34import android.view.animation.Interpolator;
35import android.widget.FrameLayout;
36import android.widget.TextView;
37
38import com.android.keyguard.KeyguardStatusView;
39import com.android.systemui.R;
40import com.android.systemui.qs.QSPanel;
41import com.android.systemui.statusbar.ExpandableView;
42import com.android.systemui.statusbar.FlingAnimationUtils;
43import com.android.systemui.statusbar.GestureRecorder;
44import com.android.systemui.statusbar.KeyguardAffordanceView;
45import com.android.systemui.statusbar.StatusBarState;
46import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
47import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
48import com.android.systemui.statusbar.stack.StackStateAnimator;
49
50public class NotificationPanelView extends PanelView implements
51        ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener,
52        View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener,
53        KeyguardAffordanceHelper.Callback {
54
55    // Cap and total height of Roboto font. Needs to be adjusted when font for the big clock is
56    // changed.
57    private static final int CAP_HEIGHT = 1456;
58    private static final int FONT_HEIGHT = 2163;
59
60    private static final float HEADER_RUBBERBAND_FACTOR = 2.05f;
61    private static final float LOCK_ICON_ACTIVE_SCALE = 1.2f;
62
63    private KeyguardAffordanceHelper mAfforanceHelper;
64    private StatusBarHeaderView mHeader;
65    private KeyguardUserSwitcher mKeyguardUserSwitcher;
66    private KeyguardStatusBarView mKeyguardStatusBar;
67    private View mQsContainer;
68    private QSPanel mQsPanel;
69    private KeyguardStatusView mKeyguardStatusView;
70    private ObservableScrollView mScrollView;
71    private TextView mClockView;
72    private View mReserveNotificationSpace;
73    private View mQsNavbarScrim;
74    private View mNotificationContainerParent;
75    private NotificationStackScrollLayout mNotificationStackScroller;
76    private int mNotificationTopPadding;
77    private boolean mAnimateNextTopPaddingChange;
78
79    private int mTrackingPointer;
80    private VelocityTracker mVelocityTracker;
81    private boolean mQsTracking;
82
83    /**
84     * Handles launching the secure camera properly even when other applications may be using the
85     * camera hardware.
86     */
87    private SecureCameraLaunchManager mSecureCameraLaunchManager;
88
89    /**
90     * If set, the ongoing touch gesture might both trigger the expansion in {@link PanelView} and
91     * the expansion for quick settings.
92     */
93    private boolean mConflictingQsExpansionGesture;
94
95    /**
96     * Whether we are currently handling a motion gesture in #onInterceptTouchEvent, but haven't
97     * intercepted yet.
98     */
99    private boolean mIntercepting;
100    private boolean mQsExpanded;
101    private boolean mQsExpandedWhenExpandingStarted;
102    private boolean mQsFullyExpanded;
103    private boolean mKeyguardShowing;
104    private boolean mDozing;
105    private int mStatusBarState;
106    private float mInitialHeightOnTouch;
107    private float mInitialTouchX;
108    private float mInitialTouchY;
109    private float mLastTouchX;
110    private float mLastTouchY;
111    private float mQsExpansionHeight;
112    private int mQsMinExpansionHeight;
113    private int mQsMaxExpansionHeight;
114    private int mQsPeekHeight;
115    private boolean mStackScrollerOverscrolling;
116    private boolean mQsExpansionFromOverscroll;
117    private float mLastOverscroll;
118    private boolean mQsExpansionEnabled = true;
119    private ValueAnimator mQsExpansionAnimator;
120    private FlingAnimationUtils mFlingAnimationUtils;
121    private int mStatusBarMinHeight;
122    private boolean mUnlockIconActive;
123    private int mNotificationsHeaderCollideDistance;
124    private int mUnlockMoveDistance;
125    private float mEmptyDragAmount;
126
127    private Interpolator mFastOutSlowInInterpolator;
128    private Interpolator mFastOutLinearInterpolator;
129    private ObjectAnimator mClockAnimator;
130    private int mClockAnimationTarget = -1;
131    private int mTopPaddingAdjustment;
132    private KeyguardClockPositionAlgorithm mClockPositionAlgorithm =
133            new KeyguardClockPositionAlgorithm();
134    private KeyguardClockPositionAlgorithm.Result mClockPositionResult =
135            new KeyguardClockPositionAlgorithm.Result();
136    private boolean mIsExpanding;
137
138    private boolean mBlockTouches;
139    private int mNotificationScrimWaitDistance;
140    private boolean mTwoFingerQsExpand;
141    private boolean mTwoFingerQsExpandPossible;
142
143    /**
144     * If we are in a panel collapsing motion, we reset scrollY of our scroll view but still
145     * need to take this into account in our panel height calculation.
146     */
147    private int mScrollYOverride = -1;
148    private boolean mQsAnimatorExpand;
149    private boolean mIsLaunchTransitionFinished;
150    private boolean mIsLaunchTransitionRunning;
151    private Runnable mLaunchAnimationEndRunnable;
152    private boolean mOnlyAffordanceInThisMotion;
153    private boolean mKeyguardStatusViewAnimating;
154    private boolean mHeaderAnimatingIn;
155    private ObjectAnimator mQsContainerAnimator;
156
157    private boolean mShadeEmpty;
158
159    private boolean mQsScrimEnabled = true;
160    private boolean mLastAnnouncementWasQuickSettings;
161    private boolean mQsTouchAboveFalsingThreshold;
162    private int mQsFalsingThreshold;
163
164    public NotificationPanelView(Context context, AttributeSet attrs) {
165        super(context, attrs);
166    }
167
168    public void setStatusBar(PhoneStatusBar bar) {
169        mStatusBar = bar;
170    }
171
172    @Override
173    protected void onFinishInflate() {
174        super.onFinishInflate();
175        mHeader = (StatusBarHeaderView) findViewById(R.id.header);
176        mHeader.setOnClickListener(this);
177        mKeyguardStatusBar = (KeyguardStatusBarView) findViewById(R.id.keyguard_header);
178        mKeyguardStatusView = (KeyguardStatusView) findViewById(R.id.keyguard_status_view);
179        mQsContainer = findViewById(R.id.quick_settings_container);
180        mQsPanel = (QSPanel) findViewById(R.id.quick_settings_panel);
181        mClockView = (TextView) findViewById(R.id.clock_view);
182        mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view);
183        mScrollView.setListener(this);
184        mScrollView.setFocusable(false);
185        mReserveNotificationSpace = findViewById(R.id.reserve_notification_space);
186        mNotificationContainerParent = findViewById(R.id.notification_container_parent);
187        mNotificationStackScroller = (NotificationStackScrollLayout)
188                findViewById(R.id.notification_stack_scroller);
189        mNotificationStackScroller.setOnHeightChangedListener(this);
190        mNotificationStackScroller.setOverscrollTopChangedListener(this);
191        mNotificationStackScroller.setScrollView(mScrollView);
192        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
193                android.R.interpolator.fast_out_slow_in);
194        mFastOutLinearInterpolator = AnimationUtils.loadInterpolator(getContext(),
195                android.R.interpolator.fast_out_linear_in);
196        mKeyguardBottomArea = (KeyguardBottomAreaView) findViewById(R.id.keyguard_bottom_area);
197        mQsNavbarScrim = findViewById(R.id.qs_navbar_scrim);
198        mAfforanceHelper = new KeyguardAffordanceHelper(this, getContext());
199        mSecureCameraLaunchManager =
200                new SecureCameraLaunchManager(getContext(), mKeyguardBottomArea);
201
202        // recompute internal state when qspanel height changes
203        mQsContainer.addOnLayoutChangeListener(new OnLayoutChangeListener() {
204            @Override
205            public void onLayoutChange(View v, int left, int top, int right,
206                    int bottom, int oldLeft, int oldTop, int oldRight,
207                    int oldBottom) {
208                final int height = bottom - top;
209                final int oldHeight = oldBottom - oldTop;
210                if (height != oldHeight) {
211                    onScrollChanged();
212                }
213            }
214        });
215    }
216
217    @Override
218    protected void loadDimens() {
219        super.loadDimens();
220        mNotificationTopPadding = getResources().getDimensionPixelSize(
221                R.dimen.notifications_top_padding);
222        mFlingAnimationUtils = new FlingAnimationUtils(getContext(), 0.4f);
223        mStatusBarMinHeight = getResources().getDimensionPixelSize(
224                com.android.internal.R.dimen.status_bar_height);
225        mQsPeekHeight = getResources().getDimensionPixelSize(R.dimen.qs_peek_height);
226        mNotificationsHeaderCollideDistance =
227                getResources().getDimensionPixelSize(R.dimen.header_notifications_collide_distance);
228        mUnlockMoveDistance = getResources().getDimensionPixelOffset(R.dimen.unlock_move_distance);
229        mClockPositionAlgorithm.loadDimens(getResources());
230        mNotificationScrimWaitDistance =
231                getResources().getDimensionPixelSize(R.dimen.notification_scrim_wait_distance);
232        mQsFalsingThreshold = getResources().getDimensionPixelSize(
233                R.dimen.qs_falsing_threshold);
234    }
235
236    public void updateResources() {
237        int panelWidth = getResources().getDimensionPixelSize(R.dimen.notification_panel_width);
238        int panelGravity = getResources().getInteger(R.integer.notification_panel_layout_gravity);
239        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mHeader.getLayoutParams();
240        if (lp.width != panelWidth) {
241            lp.width = panelWidth;
242            lp.gravity = panelGravity;
243            mHeader.setLayoutParams(lp);
244            mHeader.post(mUpdateHeader);
245        }
246
247        lp = (FrameLayout.LayoutParams) mNotificationStackScroller.getLayoutParams();
248        if (lp.width != panelWidth) {
249            lp.width = panelWidth;
250            lp.gravity = panelGravity;
251            mNotificationStackScroller.setLayoutParams(lp);
252        }
253
254        lp = (FrameLayout.LayoutParams) mScrollView.getLayoutParams();
255        if (lp.width != panelWidth) {
256            lp.width = panelWidth;
257            lp.gravity = panelGravity;
258            mScrollView.setLayoutParams(lp);
259        }
260    }
261
262    @Override
263    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
264        super.onLayout(changed, left, top, right, bottom);
265
266        // Update Clock Pivot
267        mKeyguardStatusView.setPivotX(getWidth() / 2);
268        mKeyguardStatusView.setPivotY((FONT_HEIGHT - CAP_HEIGHT) / 2048f * mClockView.getTextSize());
269
270        // Calculate quick setting heights.
271        mQsMinExpansionHeight = mKeyguardShowing ? 0 : mHeader.getCollapsedHeight() + mQsPeekHeight;
272        mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight();
273        if (mQsExpanded) {
274            if (mQsFullyExpanded) {
275                mQsExpansionHeight = mQsMaxExpansionHeight;
276                requestScrollerTopPaddingUpdate(false /* animate */);
277            }
278        } else {
279            setQsExpansion(mQsMinExpansionHeight + mLastOverscroll);
280            positionClockAndNotifications();
281            mNotificationStackScroller.setStackHeight(getExpandedHeight());
282            updateHeader();
283        }
284        mNotificationStackScroller.updateIsSmallScreen(
285                mHeader.getCollapsedHeight() + mQsPeekHeight);
286    }
287
288    @Override
289    public void onAttachedToWindow() {
290        mSecureCameraLaunchManager.create();
291    }
292
293    @Override
294    public void onDetachedFromWindow() {
295        mSecureCameraLaunchManager.destroy();
296    }
297
298    /**
299     * Positions the clock and notifications dynamically depending on how many notifications are
300     * showing.
301     */
302    private void positionClockAndNotifications() {
303        boolean animate = mNotificationStackScroller.isAddOrRemoveAnimationPending();
304        int stackScrollerPadding;
305        if (mStatusBarState != StatusBarState.KEYGUARD) {
306            int bottom = mHeader.getCollapsedHeight();
307            stackScrollerPadding = mStatusBarState == StatusBarState.SHADE
308                    ? bottom + mQsPeekHeight + mNotificationTopPadding
309                    : mKeyguardStatusBar.getHeight() + mNotificationTopPadding;
310            mTopPaddingAdjustment = 0;
311        } else {
312            mClockPositionAlgorithm.setup(
313                    mStatusBar.getMaxKeyguardNotifications(),
314                    getMaxPanelHeight(),
315                    getExpandedHeight(),
316                    mNotificationStackScroller.getNotGoneChildCount(),
317                    getHeight(),
318                    mKeyguardStatusView.getHeight(),
319                    mEmptyDragAmount);
320            mClockPositionAlgorithm.run(mClockPositionResult);
321            if (animate || mClockAnimator != null) {
322                startClockAnimation(mClockPositionResult.clockY);
323            } else {
324                mKeyguardStatusView.setY(mClockPositionResult.clockY);
325            }
326            updateClock(mClockPositionResult.clockAlpha, mClockPositionResult.clockScale);
327            stackScrollerPadding = mClockPositionResult.stackScrollerPadding;
328            mTopPaddingAdjustment = mClockPositionResult.stackScrollerPaddingAdjustment;
329        }
330        mNotificationStackScroller.setIntrinsicPadding(stackScrollerPadding);
331        requestScrollerTopPaddingUpdate(animate);
332    }
333
334    private void startClockAnimation(int y) {
335        if (mClockAnimationTarget == y) {
336            return;
337        }
338        mClockAnimationTarget = y;
339        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
340            @Override
341            public boolean onPreDraw() {
342                getViewTreeObserver().removeOnPreDrawListener(this);
343                if (mClockAnimator != null) {
344                    mClockAnimator.removeAllListeners();
345                    mClockAnimator.cancel();
346                }
347                mClockAnimator = ObjectAnimator
348                        .ofFloat(mKeyguardStatusView, View.Y, mClockAnimationTarget);
349                mClockAnimator.setInterpolator(mFastOutSlowInInterpolator);
350                mClockAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
351                mClockAnimator.addListener(new AnimatorListenerAdapter() {
352                    @Override
353                    public void onAnimationEnd(Animator animation) {
354                        mClockAnimator = null;
355                        mClockAnimationTarget = -1;
356                    }
357                });
358                mClockAnimator.start();
359                return true;
360            }
361        });
362    }
363
364    private void updateClock(float alpha, float scale) {
365        if (!mKeyguardStatusViewAnimating) {
366            mKeyguardStatusView.setAlpha(alpha);
367        }
368        mKeyguardStatusView.setScaleX(scale);
369        mKeyguardStatusView.setScaleY(scale);
370    }
371
372    public void animateToFullShade(long delay) {
373        mAnimateNextTopPaddingChange = true;
374        mNotificationStackScroller.goToFullShade(delay);
375        requestLayout();
376    }
377
378    public void setQsExpansionEnabled(boolean qsExpansionEnabled) {
379        mQsExpansionEnabled = qsExpansionEnabled;
380        mHeader.setClickable(qsExpansionEnabled);
381    }
382
383    @Override
384    public void resetViews() {
385        mIsLaunchTransitionFinished = false;
386        mBlockTouches = false;
387        mUnlockIconActive = false;
388        mAfforanceHelper.reset(true);
389        closeQs();
390        mStatusBar.dismissPopups();
391        mNotificationStackScroller.setOverScrollAmount(0f, true /* onTop */, false /* animate */,
392                true /* cancelAnimators */);
393    }
394
395    public void closeQs() {
396        cancelAnimation();
397        setQsExpansion(mQsMinExpansionHeight);
398    }
399
400    public void animateCloseQs() {
401        if (mQsExpansionAnimator != null) {
402            if (!mQsAnimatorExpand) {
403                return;
404            }
405            float height = mQsExpansionHeight;
406            mQsExpansionAnimator.cancel();
407            setQsExpansion(height);
408        }
409        flingSettings(0 /* vel */, false);
410    }
411
412    public void openQs() {
413        cancelAnimation();
414        if (mQsExpansionEnabled) {
415            setQsExpansion(mQsMaxExpansionHeight);
416        }
417    }
418
419    @Override
420    public void fling(float vel, boolean expand) {
421        GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder();
422        if (gr != null) {
423            gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel);
424        }
425        super.fling(vel, expand);
426    }
427
428    @Override
429    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
430        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
431            event.getText().add(getKeyguardOrLockScreenString());
432            mLastAnnouncementWasQuickSettings = false;
433            return true;
434        }
435
436        return super.dispatchPopulateAccessibilityEvent(event);
437    }
438
439    @Override
440    public boolean onInterceptTouchEvent(MotionEvent event) {
441        if (mBlockTouches) {
442            return false;
443        }
444        resetDownStates(event);
445        int pointerIndex = event.findPointerIndex(mTrackingPointer);
446        if (pointerIndex < 0) {
447            pointerIndex = 0;
448            mTrackingPointer = event.getPointerId(pointerIndex);
449        }
450        final float x = event.getX(pointerIndex);
451        final float y = event.getY(pointerIndex);
452
453        switch (event.getActionMasked()) {
454            case MotionEvent.ACTION_DOWN:
455                mIntercepting = true;
456                mInitialTouchY = y;
457                mInitialTouchX = x;
458                initVelocityTracker();
459                trackMovement(event);
460                if (shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) {
461                    getParent().requestDisallowInterceptTouchEvent(true);
462                }
463                if (mQsExpansionAnimator != null) {
464                    onQsExpansionStarted();
465                    mInitialHeightOnTouch = mQsExpansionHeight;
466                    mQsTracking = true;
467                    mIntercepting = false;
468                    mNotificationStackScroller.removeLongPressCallback();
469                }
470                break;
471            case MotionEvent.ACTION_POINTER_UP:
472                final int upPointer = event.getPointerId(event.getActionIndex());
473                if (mTrackingPointer == upPointer) {
474                    // gesture is ongoing, find a new pointer to track
475                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
476                    mTrackingPointer = event.getPointerId(newIndex);
477                    mInitialTouchX = event.getX(newIndex);
478                    mInitialTouchY = event.getY(newIndex);
479                }
480                break;
481
482            case MotionEvent.ACTION_MOVE:
483                final float h = y - mInitialTouchY;
484                trackMovement(event);
485                if (mQsTracking) {
486
487                    // Already tracking because onOverscrolled was called. We need to update here
488                    // so we don't stop for a frame until the next touch event gets handled in
489                    // onTouchEvent.
490                    setQsExpansion(h + mInitialHeightOnTouch);
491                    trackMovement(event);
492                    mIntercepting = false;
493                    return true;
494                }
495                if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)
496                        && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) {
497                    onQsExpansionStarted();
498                    mInitialHeightOnTouch = mQsExpansionHeight;
499                    mInitialTouchY = y;
500                    mInitialTouchX = x;
501                    mQsTracking = true;
502                    mIntercepting = false;
503                    mNotificationStackScroller.removeLongPressCallback();
504                    return true;
505                }
506                break;
507
508            case MotionEvent.ACTION_CANCEL:
509            case MotionEvent.ACTION_UP:
510                trackMovement(event);
511                if (mQsTracking) {
512                    flingQsWithCurrentVelocity();
513                    mQsTracking = false;
514                }
515                mIntercepting = false;
516                break;
517        }
518
519        // Allow closing the whole panel when in SHADE state.
520        if (mStatusBarState == StatusBarState.SHADE) {
521            return super.onInterceptTouchEvent(event);
522        } else {
523            return !mQsExpanded && super.onInterceptTouchEvent(event);
524        }
525    }
526
527    private void resetDownStates(MotionEvent event) {
528        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
529            mOnlyAffordanceInThisMotion = false;
530            mQsTouchAboveFalsingThreshold = mQsFullyExpanded;
531        }
532    }
533
534    @Override
535    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
536
537        // Block request when interacting with the scroll view so we can still intercept the
538        // scrolling when QS is expanded.
539        if (mScrollView.isHandlingTouchEvent()) {
540            return;
541        }
542        super.requestDisallowInterceptTouchEvent(disallowIntercept);
543    }
544
545    private void flingQsWithCurrentVelocity() {
546        float vel = getCurrentVelocity();
547        flingSettings(vel, flingExpandsQs(vel));
548    }
549
550    private boolean flingExpandsQs(float vel) {
551        if (!mQsTouchAboveFalsingThreshold && mStatusBarState == StatusBarState.KEYGUARD) {
552            return false;
553        }
554        if (Math.abs(vel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
555            return getQsExpansionFraction() > 0.5f;
556        } else {
557            return vel > 0;
558        }
559    }
560
561    private float getQsExpansionFraction() {
562        return Math.min(1f, (mQsExpansionHeight - mQsMinExpansionHeight)
563                / (getTempQsMaxExpansion() - mQsMinExpansionHeight));
564    }
565
566    @Override
567    public boolean onTouchEvent(MotionEvent event) {
568        if (mBlockTouches) {
569            return false;
570        }
571        resetDownStates(event);
572        if ((!mIsExpanding || mHintAnimationRunning)
573                && !mQsExpanded
574                && mStatusBar.getBarState() != StatusBarState.SHADE) {
575            mAfforanceHelper.onTouchEvent(event);
576        }
577        if (mOnlyAffordanceInThisMotion) {
578            return true;
579        }
580        if (event.getActionMasked() == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f
581                && mStatusBar.getBarState() != StatusBarState.KEYGUARD && !mQsExpanded
582                && mQsExpansionEnabled) {
583
584            // Down in the empty area while fully expanded - go to QS.
585            mQsTracking = true;
586            mConflictingQsExpansionGesture = true;
587            onQsExpansionStarted();
588            mInitialHeightOnTouch = mQsExpansionHeight;
589            mInitialTouchY = event.getX();
590            mInitialTouchX = event.getY();
591        }
592        if (mExpandedHeight != 0) {
593            handleQsDown(event);
594        }
595        if (!mTwoFingerQsExpand && mQsTracking) {
596            onQsTouch(event);
597            if (!mConflictingQsExpansionGesture) {
598                return true;
599            }
600        }
601        if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
602                || event.getActionMasked() == MotionEvent.ACTION_UP) {
603            mConflictingQsExpansionGesture = false;
604        }
605        if (event.getActionMasked() == MotionEvent.ACTION_DOWN && mExpandedHeight == 0
606                && mQsExpansionEnabled) {
607            mTwoFingerQsExpandPossible = true;
608        }
609        if (mTwoFingerQsExpandPossible && event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN
610                && event.getPointerCount() == 2
611                && event.getY(event.getActionIndex()) < mStatusBarMinHeight) {
612            mTwoFingerQsExpand = true;
613            requestPanelHeightUpdate();
614
615            // Normally, we start listening when the panel is expanded, but here we need to start
616            // earlier so the state is already up to date when dragging down.
617            setListening(true);
618        }
619        super.onTouchEvent(event);
620        return true;
621    }
622
623    private boolean isInQsArea(float x, float y) {
624        return mStatusBarState != StatusBarState.SHADE
625                || y <= mNotificationStackScroller.getBottomMostNotificationBottom()
626                || y <= mQsContainer.getY() + mQsContainer.getHeight();
627    }
628
629    private void handleQsDown(MotionEvent event) {
630        if (event.getActionMasked() == MotionEvent.ACTION_DOWN
631                && shouldQuickSettingsIntercept(event.getX(), event.getY(), -1)) {
632            mQsTracking = true;
633            onQsExpansionStarted();
634            mInitialHeightOnTouch = mQsExpansionHeight;
635            mInitialTouchY = event.getX();
636            mInitialTouchX = event.getY();
637
638            // If we interrupt an expansion gesture here, make sure to update the state correctly.
639            if (mIsExpanding) {
640                onExpandingFinished();
641            }
642        }
643    }
644
645    @Override
646    protected boolean flingExpands(float vel, float vectorVel) {
647        boolean expands = super.flingExpands(vel, vectorVel);
648
649        // If we are already running a QS expansion, make sure that we keep the panel open.
650        if (mQsExpansionAnimator != null) {
651            expands = true;
652        }
653        return expands;
654    }
655
656    @Override
657    protected boolean hasConflictingGestures() {
658        return mStatusBar.getBarState() != StatusBarState.SHADE;
659    }
660
661    private void onQsTouch(MotionEvent event) {
662        int pointerIndex = event.findPointerIndex(mTrackingPointer);
663        if (pointerIndex < 0) {
664            pointerIndex = 0;
665            mTrackingPointer = event.getPointerId(pointerIndex);
666        }
667        final float y = event.getY(pointerIndex);
668        final float x = event.getX(pointerIndex);
669
670        switch (event.getActionMasked()) {
671            case MotionEvent.ACTION_DOWN:
672                mQsTracking = true;
673                mInitialTouchY = y;
674                mInitialTouchX = x;
675                onQsExpansionStarted();
676                mInitialHeightOnTouch = mQsExpansionHeight;
677                initVelocityTracker();
678                trackMovement(event);
679                break;
680
681            case MotionEvent.ACTION_POINTER_UP:
682                final int upPointer = event.getPointerId(event.getActionIndex());
683                if (mTrackingPointer == upPointer) {
684                    // gesture is ongoing, find a new pointer to track
685                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
686                    final float newY = event.getY(newIndex);
687                    final float newX = event.getX(newIndex);
688                    mTrackingPointer = event.getPointerId(newIndex);
689                    mInitialHeightOnTouch = mQsExpansionHeight;
690                    mInitialTouchY = newY;
691                    mInitialTouchX = newX;
692                }
693                break;
694
695            case MotionEvent.ACTION_MOVE:
696                final float h = y - mInitialTouchY;
697                setQsExpansion(h + mInitialHeightOnTouch);
698                if (h >= mQsFalsingThreshold) {
699                    mQsTouchAboveFalsingThreshold = true;
700                }
701                trackMovement(event);
702                break;
703
704            case MotionEvent.ACTION_UP:
705            case MotionEvent.ACTION_CANCEL:
706                mQsTracking = false;
707                mTrackingPointer = -1;
708                trackMovement(event);
709                float fraction = getQsExpansionFraction();
710                if ((fraction != 0f || y >= mInitialTouchY)
711                        && (fraction != 1f || y <= mInitialTouchY)) {
712                    flingQsWithCurrentVelocity();
713                } else {
714                    mScrollYOverride = -1;
715                }
716                if (mVelocityTracker != null) {
717                    mVelocityTracker.recycle();
718                    mVelocityTracker = null;
719                }
720                break;
721        }
722    }
723
724    @Override
725    public void onOverscrolled(float lastTouchX, float lastTouchY, int amount) {
726        if (mIntercepting && shouldQuickSettingsIntercept(lastTouchX, lastTouchY,
727                -1 /* yDiff: Not relevant here */)) {
728            onQsExpansionStarted(amount);
729            mInitialHeightOnTouch = mQsExpansionHeight;
730            mInitialTouchY = mLastTouchY;
731            mInitialTouchX = mLastTouchX;
732            mQsTracking = true;
733        }
734    }
735
736    @Override
737    public void onOverscrollTopChanged(float amount, boolean isRubberbanded) {
738        cancelAnimation();
739        if (!mQsExpansionEnabled) {
740            amount = 0f;
741        }
742        float rounded = amount >= 1f ? amount : 0f;
743        mStackScrollerOverscrolling = rounded != 0f && isRubberbanded;
744        mQsExpansionFromOverscroll = rounded != 0f;
745        mLastOverscroll = rounded;
746        updateQsState();
747        setQsExpansion(mQsMinExpansionHeight + rounded);
748    }
749
750    @Override
751    public void flingTopOverscroll(float velocity, boolean open) {
752        mLastOverscroll = 0f;
753        setQsExpansion(mQsExpansionHeight);
754        flingSettings(!mQsExpansionEnabled && open ? 0f : velocity, open && mQsExpansionEnabled,
755                new Runnable() {
756            @Override
757            public void run() {
758                mStackScrollerOverscrolling = false;
759                mQsExpansionFromOverscroll = false;
760                updateQsState();
761            }
762        });
763    }
764
765    private void onQsExpansionStarted() {
766        onQsExpansionStarted(0);
767    }
768
769    private void onQsExpansionStarted(int overscrollAmount) {
770        cancelAnimation();
771
772        // Reset scroll position and apply that position to the expanded height.
773        float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount;
774        if (mScrollView.getScrollY() != 0) {
775            mScrollYOverride = mScrollView.getScrollY();
776        }
777        mScrollView.scrollTo(0, 0);
778        setQsExpansion(height);
779    }
780
781    private void setQsExpanded(boolean expanded) {
782        boolean changed = mQsExpanded != expanded;
783        if (changed) {
784            mQsExpanded = expanded;
785            updateQsState();
786            requestPanelHeightUpdate();
787            mNotificationStackScroller.setInterceptDelegateEnabled(expanded);
788            mStatusBar.setQsExpanded(expanded);
789        }
790    }
791
792    public void setBarState(int statusBarState, boolean keyguardFadingAway,
793            boolean goingToFullShade) {
794        boolean keyguardShowing = statusBarState == StatusBarState.KEYGUARD
795                || statusBarState == StatusBarState.SHADE_LOCKED;
796        if (!mKeyguardShowing && keyguardShowing) {
797            setQsTranslation(mQsExpansionHeight);
798            mHeader.setTranslationY(0f);
799        }
800        setKeyguardStatusViewVisibility(statusBarState, keyguardFadingAway, goingToFullShade);
801        setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade);
802        if (goingToFullShade) {
803            animateKeyguardStatusBarOut();
804        } else {
805            mKeyguardStatusBar.setAlpha(1f);
806            mKeyguardStatusBar.setVisibility(keyguardShowing ? View.VISIBLE : View.INVISIBLE);
807        }
808        mStatusBarState = statusBarState;
809        mKeyguardShowing = keyguardShowing;
810        updateQsState();
811        if (goingToFullShade) {
812            animateHeaderSlidingIn();
813        }
814    }
815
816    private final Runnable mAnimateKeyguardStatusViewInvisibleEndRunnable = new Runnable() {
817        @Override
818        public void run() {
819            mKeyguardStatusViewAnimating = false;
820            mKeyguardStatusView.setVisibility(View.GONE);
821        }
822    };
823
824    private final Runnable mAnimateKeyguardStatusViewVisibleEndRunnable = new Runnable() {
825        @Override
826        public void run() {
827            mKeyguardStatusViewAnimating = false;
828        }
829    };
830
831    private final Animator.AnimatorListener mAnimateHeaderSlidingInListener
832            = new AnimatorListenerAdapter() {
833        @Override
834        public void onAnimationEnd(Animator animation) {
835            mHeaderAnimatingIn = false;
836            mQsContainerAnimator = null;
837            mQsContainer.removeOnLayoutChangeListener(mQsContainerAnimatorUpdater);
838        }
839    };
840
841    private final OnLayoutChangeListener mQsContainerAnimatorUpdater
842            = new OnLayoutChangeListener() {
843        @Override
844        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
845                int oldTop, int oldRight, int oldBottom) {
846            int oldHeight = oldBottom - oldTop;
847            int height = bottom - top;
848            if (height != oldHeight && mQsContainerAnimator != null) {
849                PropertyValuesHolder[] values = mQsContainerAnimator.getValues();
850                float newEndValue = mHeader.getCollapsedHeight() + mQsPeekHeight - height - top;
851                float newStartValue = -height - top;
852                values[0].setFloatValues(newStartValue, newEndValue);
853                mQsContainerAnimator.setCurrentPlayTime(mQsContainerAnimator.getCurrentPlayTime());
854            }
855        }
856    };
857
858    private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn
859            = new ViewTreeObserver.OnPreDrawListener() {
860        @Override
861        public boolean onPreDraw() {
862            getViewTreeObserver().removeOnPreDrawListener(this);
863            mHeader.setTranslationY(-mHeader.getCollapsedHeight() - mQsPeekHeight);
864            mHeader.animate()
865                    .translationY(0f)
866                    .setStartDelay(mStatusBar.calculateGoingToFullShadeDelay())
867                    .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE)
868                    .setInterpolator(mFastOutSlowInInterpolator)
869                    .start();
870            mQsContainer.setY(-mQsContainer.getHeight());
871            mQsContainerAnimator = ObjectAnimator.ofFloat(mQsContainer, View.TRANSLATION_Y,
872                    mQsContainer.getTranslationY(),
873                    mHeader.getCollapsedHeight() + mQsPeekHeight - mQsContainer.getHeight()
874                            - mQsContainer.getTop());
875            mQsContainerAnimator.setStartDelay(mStatusBar.calculateGoingToFullShadeDelay());
876            mQsContainerAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE);
877            mQsContainerAnimator.setInterpolator(mFastOutSlowInInterpolator);
878            mQsContainerAnimator.addListener(mAnimateHeaderSlidingInListener);
879            mQsContainerAnimator.start();
880            mQsContainer.addOnLayoutChangeListener(mQsContainerAnimatorUpdater);
881            return true;
882        }
883    };
884
885    private void animateHeaderSlidingIn() {
886        mHeaderAnimatingIn = true;
887        getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn);
888
889    }
890
891    private final Runnable mAnimateKeyguardStatusBarInvisibleEndRunnable = new Runnable() {
892        @Override
893        public void run() {
894            mKeyguardStatusBar.setVisibility(View.INVISIBLE);
895        }
896    };
897
898    private void animateKeyguardStatusBarOut() {
899        mKeyguardStatusBar.animate()
900                .alpha(0f)
901                .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
902                .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2)
903                .setInterpolator(PhoneStatusBar.ALPHA_OUT)
904                .withEndAction(mAnimateKeyguardStatusBarInvisibleEndRunnable)
905                .start();
906    }
907
908    private final Runnable mAnimateKeyguardBottomAreaInvisibleEndRunnable = new Runnable() {
909        @Override
910        public void run() {
911            mKeyguardBottomArea.setVisibility(View.GONE);
912        }
913    };
914
915    private void setKeyguardBottomAreaVisibility(int statusBarState,
916            boolean goingToFullShade) {
917        if (goingToFullShade) {
918            mKeyguardBottomArea.animate().cancel();
919            mKeyguardBottomArea.animate()
920                    .alpha(0f)
921                    .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
922                    .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2)
923                    .setInterpolator(PhoneStatusBar.ALPHA_OUT)
924                    .withEndAction(mAnimateKeyguardBottomAreaInvisibleEndRunnable)
925                    .start();
926        } else if (statusBarState == StatusBarState.KEYGUARD
927                || statusBarState == StatusBarState.SHADE_LOCKED) {
928            mKeyguardBottomArea.animate().cancel();
929            mKeyguardBottomArea.setVisibility(View.VISIBLE);
930            mKeyguardBottomArea.setAlpha(1f);
931        } else {
932            mKeyguardBottomArea.animate().cancel();
933            mKeyguardBottomArea.setVisibility(View.GONE);
934            mKeyguardBottomArea.setAlpha(1f);
935        }
936    }
937
938    private void setKeyguardStatusViewVisibility(int statusBarState, boolean keyguardFadingAway,
939            boolean goingToFullShade) {
940        if ((!keyguardFadingAway && mStatusBarState == StatusBarState.KEYGUARD
941                && statusBarState != StatusBarState.KEYGUARD) || goingToFullShade) {
942            mKeyguardStatusView.animate().cancel();
943            mKeyguardStatusViewAnimating = true;
944            mKeyguardStatusView.animate()
945                    .alpha(0f)
946                    .setStartDelay(0)
947                    .setDuration(160)
948                    .setInterpolator(PhoneStatusBar.ALPHA_OUT)
949                    .withEndAction(mAnimateKeyguardStatusViewInvisibleEndRunnable);
950            if (keyguardFadingAway) {
951                mKeyguardStatusView.animate()
952                        .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
953                        .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2)
954                        .start();
955            }
956        } else if (mStatusBarState == StatusBarState.SHADE_LOCKED
957                && statusBarState == StatusBarState.KEYGUARD) {
958            mKeyguardStatusView.animate().cancel();
959            mKeyguardStatusView.setVisibility(View.VISIBLE);
960            mKeyguardStatusViewAnimating = true;
961            mKeyguardStatusView.setAlpha(0f);
962            mKeyguardStatusView.animate()
963                    .alpha(1f)
964                    .setStartDelay(0)
965                    .setDuration(320)
966                    .setInterpolator(PhoneStatusBar.ALPHA_IN)
967                    .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable);
968        } else if (statusBarState == StatusBarState.KEYGUARD) {
969            mKeyguardStatusView.animate().cancel();
970            mKeyguardStatusView.setVisibility(View.VISIBLE);
971            mKeyguardStatusView.setAlpha(1f);
972        } else {
973            mKeyguardStatusView.animate().cancel();
974            mKeyguardStatusView.setVisibility(View.GONE);
975            mKeyguardStatusView.setAlpha(1f);
976        }
977    }
978
979    private void updateQsState() {
980        boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling;
981        mHeader.setVisibility((mQsExpanded || !mKeyguardShowing) ? View.VISIBLE : View.INVISIBLE);
982        mHeader.setExpanded(mKeyguardShowing || (mQsExpanded && !mStackScrollerOverscrolling));
983        mNotificationStackScroller.setScrollingEnabled(
984                mStatusBarState != StatusBarState.KEYGUARD && (!mQsExpanded
985                        || mQsExpansionFromOverscroll));
986        mQsPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE);
987        mQsContainer.setVisibility(
988                mKeyguardShowing && !expandVisually ? View.INVISIBLE : View.VISIBLE);
989        mScrollView.setTouchEnabled(mQsExpanded);
990        updateEmptyShadeView();
991        mQsNavbarScrim.setVisibility(mStatusBarState == StatusBarState.SHADE && mQsExpanded
992                && !mStackScrollerOverscrolling && mQsScrimEnabled
993                        ? View.VISIBLE
994                        : View.INVISIBLE);
995        if (mKeyguardUserSwitcher != null && mQsExpanded && !mStackScrollerOverscrolling) {
996            mKeyguardUserSwitcher.hide(true /* animate */);
997        }
998    }
999
1000    private void setQsExpansion(float height) {
1001        height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
1002        mQsFullyExpanded = height == mQsMaxExpansionHeight;
1003        if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) {
1004            setQsExpanded(true);
1005        } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
1006            setQsExpanded(false);
1007            if (mLastAnnouncementWasQuickSettings && !mTracking) {
1008                announceForAccessibility(getKeyguardOrLockScreenString());
1009                mLastAnnouncementWasQuickSettings = false;
1010            }
1011        }
1012        mQsExpansionHeight = height;
1013        mHeader.setExpansion(getHeaderExpansionFraction());
1014        setQsTranslation(height);
1015        requestScrollerTopPaddingUpdate(false /* animate */);
1016        updateNotificationScrim(height);
1017        if (mKeyguardShowing) {
1018            updateHeaderKeyguard();
1019        }
1020        if (mStatusBarState == StatusBarState.SHADE && mQsExpanded
1021                && !mStackScrollerOverscrolling && mQsScrimEnabled) {
1022            mQsNavbarScrim.setAlpha(getQsExpansionFraction());
1023        }
1024
1025        // Upon initialisation when we are not layouted yet we don't want to announce that we are
1026        // fully expanded, hence the != 0.0f check.
1027        if (height != 0.0f && mQsFullyExpanded && !mLastAnnouncementWasQuickSettings) {
1028            announceForAccessibility(getContext().getString(
1029                    R.string.accessibility_desc_quick_settings));
1030            mLastAnnouncementWasQuickSettings = true;
1031        }
1032    }
1033
1034    private String getKeyguardOrLockScreenString() {
1035        if (mStatusBarState == StatusBarState.KEYGUARD) {
1036            return getContext().getString(R.string.accessibility_desc_lock_screen);
1037        } else {
1038            return getContext().getString(R.string.accessibility_desc_notification_shade);
1039        }
1040    }
1041
1042    private void updateNotificationScrim(float height) {
1043        int startDistance = mQsMinExpansionHeight + mNotificationScrimWaitDistance;
1044        float progress = (height - startDistance) / (mQsMaxExpansionHeight - startDistance);
1045        progress = Math.max(0.0f, Math.min(progress, 1.0f));
1046        mNotificationStackScroller.setScrimAlpha(progress);
1047    }
1048
1049    private float getHeaderExpansionFraction() {
1050        if (!mKeyguardShowing) {
1051            return getQsExpansionFraction();
1052        } else {
1053            return 1f;
1054        }
1055    }
1056
1057    private void setQsTranslation(float height) {
1058        if (!mHeaderAnimatingIn) {
1059            mQsContainer.setY(height - mQsContainer.getHeight() + getHeaderTranslation());
1060        }
1061        if (mKeyguardShowing) {
1062            mHeader.setY(interpolate(getQsExpansionFraction(), -mHeader.getHeight(), 0));
1063        }
1064    }
1065
1066    private float calculateQsTopPadding() {
1067        // We can only do the smoother transition on Keyguard when we also are not collapsing from a
1068        // scrolled quick settings.
1069        if (mKeyguardShowing && mScrollYOverride == -1) {
1070            return interpolate(getQsExpansionFraction(),
1071                    mNotificationStackScroller.getIntrinsicPadding() - mNotificationTopPadding,
1072                    mQsMaxExpansionHeight);
1073        } else {
1074            return mQsExpansionHeight;
1075        }
1076    }
1077
1078    private void requestScrollerTopPaddingUpdate(boolean animate) {
1079        mNotificationStackScroller.updateTopPadding(calculateQsTopPadding(),
1080                mScrollView.getScrollY(),
1081                mAnimateNextTopPaddingChange || animate);
1082        mAnimateNextTopPaddingChange = false;
1083    }
1084
1085    private void trackMovement(MotionEvent event) {
1086        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
1087        mLastTouchX = event.getX();
1088        mLastTouchY = event.getY();
1089    }
1090
1091    private void initVelocityTracker() {
1092        if (mVelocityTracker != null) {
1093            mVelocityTracker.recycle();
1094        }
1095        mVelocityTracker = VelocityTracker.obtain();
1096    }
1097
1098    private float getCurrentVelocity() {
1099        if (mVelocityTracker == null) {
1100            return 0;
1101        }
1102        mVelocityTracker.computeCurrentVelocity(1000);
1103        return mVelocityTracker.getYVelocity();
1104    }
1105
1106    private void cancelAnimation() {
1107        if (mQsExpansionAnimator != null) {
1108            mQsExpansionAnimator.cancel();
1109        }
1110    }
1111
1112    private void flingSettings(float vel, boolean expand) {
1113        flingSettings(vel, expand, null);
1114    }
1115
1116    private void flingSettings(float vel, boolean expand, final Runnable onFinishRunnable) {
1117        float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight;
1118        if (target == mQsExpansionHeight) {
1119            mScrollYOverride = -1;
1120            if (onFinishRunnable != null) {
1121                onFinishRunnable.run();
1122            }
1123            return;
1124        }
1125        mScrollView.setBlockFlinging(true);
1126        ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
1127        mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel);
1128        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1129            @Override
1130            public void onAnimationUpdate(ValueAnimator animation) {
1131                setQsExpansion((Float) animation.getAnimatedValue());
1132            }
1133        });
1134        animator.addListener(new AnimatorListenerAdapter() {
1135            @Override
1136            public void onAnimationEnd(Animator animation) {
1137                mScrollView.setBlockFlinging(false);
1138                mScrollYOverride = -1;
1139                mQsExpansionAnimator = null;
1140                if (onFinishRunnable != null) {
1141                    onFinishRunnable.run();
1142                }
1143            }
1144        });
1145        animator.start();
1146        mQsExpansionAnimator = animator;
1147        mQsAnimatorExpand = expand;
1148    }
1149
1150    /**
1151     * @return Whether we should intercept a gesture to open Quick Settings.
1152     */
1153    private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
1154        if (!mQsExpansionEnabled) {
1155            return false;
1156        }
1157        View header = mKeyguardShowing ? mKeyguardStatusBar : mHeader;
1158        boolean onHeader = x >= header.getLeft() && x <= header.getRight()
1159                && y >= header.getTop() && y <= header.getBottom();
1160        if (mQsExpanded) {
1161            return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0) && isInQsArea(x, y);
1162        } else {
1163            return onHeader;
1164        }
1165    }
1166
1167    @Override
1168    protected boolean isScrolledToBottom() {
1169        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
1170            return true;
1171        }
1172        if (!isInSettings()) {
1173            return mNotificationStackScroller.isScrolledToBottom();
1174        } else {
1175            return mScrollView.isScrolledToBottom();
1176        }
1177    }
1178
1179    @Override
1180    protected int getMaxPanelHeight() {
1181        int min = mStatusBarMinHeight;
1182        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD
1183                && mNotificationStackScroller.getNotGoneChildCount() == 0) {
1184            int minHeight = (int) ((mQsMinExpansionHeight + getOverExpansionAmount())
1185                    * HEADER_RUBBERBAND_FACTOR);
1186            min = Math.max(min, minHeight);
1187        }
1188        int maxHeight;
1189        if (mTwoFingerQsExpand || mQsExpanded || mIsExpanding && mQsExpandedWhenExpandingStarted) {
1190            maxHeight = Math.max(calculatePanelHeightQsExpanded(), calculatePanelHeightShade());
1191        } else {
1192            maxHeight = calculatePanelHeightShade();
1193        }
1194        maxHeight = Math.max(maxHeight, min);
1195        return maxHeight;
1196    }
1197
1198    private boolean isInSettings() {
1199        return mQsExpanded;
1200    }
1201
1202    @Override
1203    protected void onHeightUpdated(float expandedHeight) {
1204        if (!mQsExpanded) {
1205            positionClockAndNotifications();
1206        }
1207        if (mTwoFingerQsExpand || mQsExpanded && !mQsTracking && mQsExpansionAnimator == null
1208                && !mQsExpansionFromOverscroll) {
1209            float panelHeightQsCollapsed = mNotificationStackScroller.getIntrinsicPadding()
1210                    + mNotificationStackScroller.getMinStackHeight()
1211                    + mNotificationStackScroller.getNotificationTopPadding();
1212            float panelHeightQsExpanded = calculatePanelHeightQsExpanded();
1213            float t = (expandedHeight - panelHeightQsCollapsed)
1214                    / (panelHeightQsExpanded - panelHeightQsCollapsed);
1215
1216            setQsExpansion(mQsMinExpansionHeight
1217                    + t * (getTempQsMaxExpansion() - mQsMinExpansionHeight));
1218        }
1219        mNotificationStackScroller.setStackHeight(expandedHeight);
1220        updateHeader();
1221        updateUnlockIcon();
1222        updateNotificationTranslucency();
1223    }
1224
1225    /**
1226     * @return a temporary override of {@link #mQsMaxExpansionHeight}, which is needed when
1227     *         collapsing QS / the panel when QS was scrolled
1228     */
1229    private int getTempQsMaxExpansion() {
1230        int qsTempMaxExpansion = mQsMaxExpansionHeight;
1231        if (mScrollYOverride != -1) {
1232            qsTempMaxExpansion -= mScrollYOverride;
1233        }
1234        return qsTempMaxExpansion;
1235    }
1236
1237    private int calculatePanelHeightShade() {
1238        int emptyBottomMargin = mNotificationStackScroller.getEmptyBottomMargin();
1239        int maxHeight = mNotificationStackScroller.getHeight() - emptyBottomMargin
1240                - mTopPaddingAdjustment;
1241        maxHeight += mNotificationStackScroller.getTopPaddingOverflow();
1242        return maxHeight;
1243    }
1244
1245    private int calculatePanelHeightQsExpanded() {
1246        float notificationHeight = mNotificationStackScroller.getHeight()
1247                - mNotificationStackScroller.getEmptyBottomMargin()
1248                - mNotificationStackScroller.getTopPadding();
1249        float totalHeight = mQsMaxExpansionHeight + notificationHeight
1250                + mNotificationStackScroller.getNotificationTopPadding();
1251        if (totalHeight > mNotificationStackScroller.getHeight()) {
1252            float fullyCollapsedHeight = mQsMaxExpansionHeight
1253                    + mNotificationStackScroller.getMinStackHeight()
1254                    + mNotificationStackScroller.getNotificationTopPadding()
1255                    - getScrollViewScrollY();
1256            totalHeight = Math.max(fullyCollapsedHeight, mNotificationStackScroller.getHeight());
1257        }
1258        return (int) totalHeight;
1259    }
1260
1261    private int getScrollViewScrollY() {
1262        if (mScrollYOverride != -1) {
1263            return mScrollYOverride;
1264        } else {
1265            return mScrollView.getScrollY();
1266        }
1267    }
1268    private void updateNotificationTranslucency() {
1269        float alpha = (getNotificationsTopY() + mNotificationStackScroller.getItemHeight())
1270                / (mQsMinExpansionHeight + mNotificationStackScroller.getBottomStackPeekSize()
1271                        + mNotificationStackScroller.getCollapseSecondCardPadding());
1272        alpha = Math.max(0, Math.min(alpha, 1));
1273        alpha = (float) Math.pow(alpha, 0.75);
1274        if (alpha != 1f && mNotificationStackScroller.getLayerType() != LAYER_TYPE_HARDWARE) {
1275            mNotificationStackScroller.setLayerType(LAYER_TYPE_HARDWARE, null);
1276        } else if (alpha == 1f
1277                && mNotificationStackScroller.getLayerType() == LAYER_TYPE_HARDWARE) {
1278            mNotificationStackScroller.setLayerType(LAYER_TYPE_NONE, null);
1279        }
1280        mNotificationStackScroller.setAlpha(alpha);
1281    }
1282
1283    @Override
1284    protected float getOverExpansionAmount() {
1285        return mNotificationStackScroller.getCurrentOverScrollAmount(true /* top */);
1286    }
1287
1288    @Override
1289    protected float getOverExpansionPixels() {
1290        return mNotificationStackScroller.getCurrentOverScrolledPixels(true /* top */);
1291    }
1292
1293    private void updateUnlockIcon() {
1294        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1295                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
1296            boolean active = getMaxPanelHeight() - getExpandedHeight() > mUnlockMoveDistance;
1297            KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon();
1298            if (active && !mUnlockIconActive && mTracking) {
1299                lockIcon.setImageAlpha(1.0f, true, 150, mFastOutLinearInterpolator, null);
1300                lockIcon.setImageScale(LOCK_ICON_ACTIVE_SCALE, true, 150,
1301                        mFastOutLinearInterpolator);
1302            } else if (!active && mUnlockIconActive && mTracking) {
1303                lockIcon.setImageAlpha(KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT, true,
1304                        150, mFastOutLinearInterpolator, null);
1305                lockIcon.setImageScale(1.0f, true, 150,
1306                        mFastOutLinearInterpolator);
1307            }
1308            mUnlockIconActive = active;
1309        }
1310    }
1311
1312    /**
1313     * Hides the header when notifications are colliding with it.
1314     */
1315    private void updateHeader() {
1316        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1317                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
1318            updateHeaderKeyguard();
1319        } else {
1320            updateHeaderShade();
1321        }
1322
1323    }
1324
1325    private void updateHeaderShade() {
1326        if (!mHeaderAnimatingIn) {
1327            mHeader.setTranslationY(getHeaderTranslation());
1328        }
1329        setQsTranslation(mQsExpansionHeight);
1330    }
1331
1332    private float getHeaderTranslation() {
1333        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1334                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
1335            return 0;
1336        }
1337        if (mNotificationStackScroller.getNotGoneChildCount() == 0) {
1338            if (mExpandedHeight / HEADER_RUBBERBAND_FACTOR >= mQsMinExpansionHeight) {
1339                return 0;
1340            } else {
1341                return mExpandedHeight / HEADER_RUBBERBAND_FACTOR - mQsMinExpansionHeight;
1342            }
1343        }
1344        return Math.min(0, mNotificationStackScroller.getTranslationY()) / HEADER_RUBBERBAND_FACTOR;
1345    }
1346
1347    private void updateHeaderKeyguard() {
1348        float alphaNotifications;
1349        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
1350
1351            // When on Keyguard, we hide the header as soon as the top card of the notification
1352            // stack scroller is close enough (collision distance) to the bottom of the header.
1353            alphaNotifications = getNotificationsTopY()
1354                    /
1355                    (mKeyguardStatusBar.getHeight() + mNotificationsHeaderCollideDistance);
1356        } else {
1357
1358            // In SHADE_LOCKED, the top card is already really close to the header. Hide it as
1359            // soon as we start translating the stack.
1360            alphaNotifications = getNotificationsTopY() / mKeyguardStatusBar.getHeight();
1361        }
1362        alphaNotifications = MathUtils.constrain(alphaNotifications, 0, 1);
1363        alphaNotifications = (float) Math.pow(alphaNotifications, 0.75);
1364        float alphaQsExpansion = 1 - Math.min(1, getQsExpansionFraction() * 2);
1365        mKeyguardStatusBar.setAlpha(Math.min(alphaNotifications, alphaQsExpansion));
1366        mKeyguardBottomArea.setAlpha(Math.min(1 - getQsExpansionFraction(), alphaNotifications));
1367        setQsTranslation(mQsExpansionHeight);
1368    }
1369
1370    private float getNotificationsTopY() {
1371        if (mNotificationStackScroller.getNotGoneChildCount() == 0) {
1372            return getExpandedHeight();
1373        }
1374        return mNotificationStackScroller.getNotificationsTopY();
1375    }
1376
1377    @Override
1378    protected void onExpandingStarted() {
1379        super.onExpandingStarted();
1380        mNotificationStackScroller.onExpansionStarted();
1381        mIsExpanding = true;
1382        mQsExpandedWhenExpandingStarted = mQsExpanded;
1383        if (mQsExpanded) {
1384            onQsExpansionStarted();
1385        }
1386    }
1387
1388    @Override
1389    protected void onExpandingFinished() {
1390        super.onExpandingFinished();
1391        mNotificationStackScroller.onExpansionStopped();
1392        mIsExpanding = false;
1393        mScrollYOverride = -1;
1394        if (mExpandedHeight == 0f) {
1395            setListening(false);
1396        } else {
1397            setListening(true);
1398        }
1399        mTwoFingerQsExpand = false;
1400        mTwoFingerQsExpandPossible = false;
1401    }
1402
1403    private void setListening(boolean listening) {
1404        mHeader.setListening(listening);
1405        mKeyguardStatusBar.setListening(listening);
1406        mQsPanel.setListening(listening);
1407    }
1408
1409    @Override
1410    public void instantExpand() {
1411        super.instantExpand();
1412        setListening(true);
1413    }
1414
1415    @Override
1416    protected void setOverExpansion(float overExpansion, boolean isPixels) {
1417        if (mConflictingQsExpansionGesture || mTwoFingerQsExpand) {
1418            return;
1419        }
1420        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) {
1421            mNotificationStackScroller.setOnHeightChangedListener(null);
1422            if (isPixels) {
1423                mNotificationStackScroller.setOverScrolledPixels(
1424                        overExpansion, true /* onTop */, false /* animate */);
1425            } else {
1426                mNotificationStackScroller.setOverScrollAmount(
1427                        overExpansion, true /* onTop */, false /* animate */);
1428            }
1429            mNotificationStackScroller.setOnHeightChangedListener(this);
1430        }
1431    }
1432
1433    @Override
1434    protected void onTrackingStarted() {
1435        super.onTrackingStarted();
1436        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1437                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
1438            mAfforanceHelper.animateHideLeftRightIcon();
1439        }
1440        if (mQsExpanded) {
1441            mTwoFingerQsExpand = true;
1442        }
1443    }
1444
1445    @Override
1446    protected void onTrackingStopped(boolean expand) {
1447        super.onTrackingStopped(expand);
1448        if (expand) {
1449            mNotificationStackScroller.setOverScrolledPixels(
1450                    0.0f, true /* onTop */, true /* animate */);
1451        }
1452        if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1453                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
1454            if (!mHintAnimationRunning) {
1455                mAfforanceHelper.reset(true);
1456            }
1457        }
1458        if (!expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1459                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
1460            KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon();
1461            lockIcon.setImageAlpha(0.0f, true, 100, mFastOutLinearInterpolator, null);
1462            lockIcon.setImageScale(2.0f, true, 100, mFastOutLinearInterpolator);
1463        }
1464    }
1465
1466    @Override
1467    public void onHeightChanged(ExpandableView view) {
1468
1469        // Block update if we are in quick settings and just the top padding changed
1470        // (i.e. view == null).
1471        if (view == null && mQsExpanded) {
1472            return;
1473        }
1474        requestPanelHeightUpdate();
1475    }
1476
1477    @Override
1478    public void onReset(ExpandableView view) {
1479    }
1480
1481    @Override
1482    public void onScrollChanged() {
1483        if (mQsExpanded) {
1484            requestScrollerTopPaddingUpdate(false /* animate */);
1485            requestPanelHeightUpdate();
1486        }
1487    }
1488
1489    @Override
1490    protected void onConfigurationChanged(Configuration newConfig) {
1491        super.onConfigurationChanged(newConfig);
1492        mAfforanceHelper.onConfigurationChanged();
1493    }
1494
1495    @Override
1496    public void onClick(View v) {
1497        if (v == mHeader) {
1498            onQsExpansionStarted();
1499            if (mQsExpanded) {
1500                flingSettings(0 /* vel */, false /* expand */);
1501            } else if (mQsExpansionEnabled) {
1502                flingSettings(0 /* vel */, true /* expand */);
1503            }
1504        }
1505    }
1506
1507    @Override
1508    public void onAnimationToSideStarted(boolean rightPage) {
1509        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage;
1510        mIsLaunchTransitionRunning = true;
1511        mLaunchAnimationEndRunnable = null;
1512        if (start) {
1513            mKeyguardBottomArea.launchPhone();
1514        } else {
1515            mSecureCameraLaunchManager.startSecureCameraLaunch();
1516        }
1517        mBlockTouches = true;
1518    }
1519
1520    @Override
1521    public void onAnimationToSideEnded() {
1522        mIsLaunchTransitionRunning = false;
1523        mIsLaunchTransitionFinished = true;
1524        if (mLaunchAnimationEndRunnable != null) {
1525            mLaunchAnimationEndRunnable.run();
1526            mLaunchAnimationEndRunnable = null;
1527        }
1528    }
1529
1530    @Override
1531    protected void onEdgeClicked(boolean right) {
1532        if ((right && getRightIcon().getVisibility() != View.VISIBLE)
1533                || (!right && getLeftIcon().getVisibility() != View.VISIBLE)) {
1534            return;
1535        }
1536        mHintAnimationRunning = true;
1537        mAfforanceHelper.startHintAnimation(right, new Runnable() {
1538            @Override
1539            public void run() {
1540                mHintAnimationRunning = false;
1541                mStatusBar.onHintFinished();
1542            }
1543        });
1544        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? right : !right;
1545        if (start) {
1546            mStatusBar.onPhoneHintStarted();
1547        } else {
1548            mStatusBar.onCameraHintStarted();
1549        }
1550    }
1551
1552    @Override
1553    protected void startUnlockHintAnimation() {
1554        super.startUnlockHintAnimation();
1555        startHighlightIconAnimation(getCenterIcon());
1556    }
1557
1558    /**
1559     * Starts the highlight (making it fully opaque) animation on an icon.
1560     */
1561    private void startHighlightIconAnimation(final KeyguardAffordanceView icon) {
1562        icon.setImageAlpha(1.0f, true, KeyguardAffordanceHelper.HINT_PHASE1_DURATION,
1563                mFastOutSlowInInterpolator, new Runnable() {
1564                    @Override
1565                    public void run() {
1566                        icon.setImageAlpha(KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT,
1567                                true, KeyguardAffordanceHelper.HINT_PHASE1_DURATION,
1568                                mFastOutSlowInInterpolator, null);
1569                    }
1570                });
1571    }
1572
1573    @Override
1574    public float getPageWidth() {
1575        return getWidth();
1576    }
1577
1578    @Override
1579    public void onSwipingStarted() {
1580        mSecureCameraLaunchManager.onSwipingStarted();
1581        requestDisallowInterceptTouchEvent(true);
1582        mOnlyAffordanceInThisMotion = true;
1583    }
1584
1585    @Override
1586    public KeyguardAffordanceView getLeftIcon() {
1587        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1588                ? mKeyguardBottomArea.getCameraView()
1589                : mKeyguardBottomArea.getPhoneView();
1590    }
1591
1592    @Override
1593    public KeyguardAffordanceView getCenterIcon() {
1594        return mKeyguardBottomArea.getLockIcon();
1595    }
1596
1597    @Override
1598    public KeyguardAffordanceView getRightIcon() {
1599        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1600                ? mKeyguardBottomArea.getPhoneView()
1601                : mKeyguardBottomArea.getCameraView();
1602    }
1603
1604    @Override
1605    public View getLeftPreview() {
1606        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1607                ? mKeyguardBottomArea.getCameraPreview()
1608                : mKeyguardBottomArea.getPhonePreview();
1609    }
1610
1611    @Override
1612    public View getRightPreview() {
1613        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1614                ? mKeyguardBottomArea.getPhonePreview()
1615                : mKeyguardBottomArea.getCameraPreview();
1616    }
1617
1618    @Override
1619    protected float getPeekHeight() {
1620        if (mNotificationStackScroller.getNotGoneChildCount() > 0) {
1621            return mNotificationStackScroller.getPeekHeight();
1622        } else {
1623            return mQsMinExpansionHeight * HEADER_RUBBERBAND_FACTOR;
1624        }
1625    }
1626
1627    @Override
1628    protected float getCannedFlingDurationFactor() {
1629        if (mQsExpanded) {
1630            return 0.7f;
1631        } else {
1632            return 0.6f;
1633        }
1634    }
1635
1636    @Override
1637    protected boolean fullyExpandedClearAllVisible() {
1638        return mNotificationStackScroller.isDismissViewNotGone()
1639                && mNotificationStackScroller.isScrolledToBottom() && !mTwoFingerQsExpand;
1640    }
1641
1642    @Override
1643    protected boolean isClearAllVisible() {
1644        return mNotificationStackScroller.isDismissViewVisible();
1645    }
1646
1647    @Override
1648    protected int getClearAllHeight() {
1649        return mNotificationStackScroller.getDismissViewHeight();
1650    }
1651
1652    @Override
1653    protected boolean isTrackingBlocked() {
1654        return mConflictingQsExpansionGesture && mQsExpanded;
1655    }
1656
1657    public void notifyVisibleChildrenChanged() {
1658        if (mNotificationStackScroller.getNotGoneChildCount() != 0) {
1659            mReserveNotificationSpace.setVisibility(View.VISIBLE);
1660        } else {
1661            mReserveNotificationSpace.setVisibility(View.GONE);
1662        }
1663    }
1664
1665    public boolean isQsExpanded() {
1666        return mQsExpanded;
1667    }
1668
1669    public boolean isQsDetailShowing() {
1670        return mQsPanel.isShowingDetail();
1671    }
1672
1673    public void closeQsDetail() {
1674        mQsPanel.closeDetail();
1675    }
1676
1677    @Override
1678    public boolean shouldDelayChildPressedState() {
1679        return true;
1680    }
1681
1682    public boolean isLaunchTransitionFinished() {
1683        return mIsLaunchTransitionFinished;
1684    }
1685
1686    public boolean isLaunchTransitionRunning() {
1687        return mIsLaunchTransitionRunning;
1688    }
1689
1690    public void setLaunchTransitionEndRunnable(Runnable r) {
1691        mLaunchAnimationEndRunnable = r;
1692    }
1693
1694    public void setEmptyDragAmount(float amount) {
1695        float factor = 1f;
1696        if (mNotificationStackScroller.getNotGoneChildCount() > 0) {
1697            factor = 0.6f;
1698        } else if (!mStatusBar.hasActiveNotifications()) {
1699            factor = 0.4f;
1700        }
1701        mEmptyDragAmount = amount * factor;
1702        positionClockAndNotifications();
1703    }
1704
1705    private static float interpolate(float t, float start, float end) {
1706        return (1 - t) * start + t * end;
1707    }
1708
1709    private void updateKeyguardStatusBarVisibility() {
1710        mKeyguardStatusBar.setVisibility(mKeyguardShowing && !mDozing ? VISIBLE : INVISIBLE);
1711    }
1712
1713    public void setDozing(boolean dozing) {
1714        if (dozing == mDozing) return;
1715        mDozing = dozing;
1716        if (mDozing) {
1717            setBackgroundColor(0xff000000);
1718        } else {
1719            setBackground(null);
1720        }
1721        updateKeyguardStatusBarVisibility();
1722    }
1723
1724    public void setShadeEmpty(boolean shadeEmpty) {
1725        mShadeEmpty = shadeEmpty;
1726        updateEmptyShadeView();
1727    }
1728
1729    private void updateEmptyShadeView() {
1730
1731        // Hide "No notifications" in QS.
1732        mNotificationStackScroller.updateEmptyShadeView(mShadeEmpty && !mQsExpanded);
1733    }
1734
1735    public void setQsScrimEnabled(boolean qsScrimEnabled) {
1736        boolean changed = mQsScrimEnabled != qsScrimEnabled;
1737        mQsScrimEnabled = qsScrimEnabled;
1738        if (changed) {
1739            updateQsState();
1740        }
1741    }
1742
1743    public void setKeyguardUserSwitcher(KeyguardUserSwitcher keyguardUserSwitcher) {
1744        mKeyguardUserSwitcher = keyguardUserSwitcher;
1745    }
1746
1747    private final Runnable mUpdateHeader = new Runnable() {
1748        @Override
1749        public void run() {
1750            mHeader.updateEverything();
1751        }
1752    };
1753
1754    public void onScreenTurnedOn() {
1755        mKeyguardStatusView.refreshTime();
1756    }
1757}
1758