NotificationPanelView.java revision 12a6782d61dbefd5ba0bb286372936f957849637
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 (isBelowFalsingThreshold()) {
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 boolean isBelowFalsingThreshold() {
562        return !mQsTouchAboveFalsingThreshold && mStatusBar.isFalsingThresholdNeeded();
563    }
564
565    private float getQsExpansionFraction() {
566        return Math.min(1f, (mQsExpansionHeight - mQsMinExpansionHeight)
567                / (getTempQsMaxExpansion() - mQsMinExpansionHeight));
568    }
569
570    @Override
571    public boolean onTouchEvent(MotionEvent event) {
572        if (mBlockTouches) {
573            return false;
574        }
575        resetDownStates(event);
576        if ((!mIsExpanding || mHintAnimationRunning)
577                && !mQsExpanded
578                && mStatusBar.getBarState() != StatusBarState.SHADE) {
579            mAfforanceHelper.onTouchEvent(event);
580        }
581        if (mOnlyAffordanceInThisMotion) {
582            return true;
583        }
584        if (event.getActionMasked() == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f
585                && mStatusBar.getBarState() != StatusBarState.KEYGUARD && !mQsExpanded
586                && mQsExpansionEnabled) {
587
588            // Down in the empty area while fully expanded - go to QS.
589            mQsTracking = true;
590            mConflictingQsExpansionGesture = true;
591            onQsExpansionStarted();
592            mInitialHeightOnTouch = mQsExpansionHeight;
593            mInitialTouchY = event.getX();
594            mInitialTouchX = event.getY();
595        }
596        if (mExpandedHeight != 0) {
597            handleQsDown(event);
598        }
599        if (!mTwoFingerQsExpand && mQsTracking) {
600            onQsTouch(event);
601            if (!mConflictingQsExpansionGesture) {
602                return true;
603            }
604        }
605        if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
606                || event.getActionMasked() == MotionEvent.ACTION_UP) {
607            mConflictingQsExpansionGesture = false;
608        }
609        if (event.getActionMasked() == MotionEvent.ACTION_DOWN && mExpandedHeight == 0
610                && mQsExpansionEnabled) {
611            mTwoFingerQsExpandPossible = true;
612        }
613        if (mTwoFingerQsExpandPossible && event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN
614                && event.getPointerCount() == 2
615                && event.getY(event.getActionIndex()) < mStatusBarMinHeight) {
616            mTwoFingerQsExpand = true;
617            requestPanelHeightUpdate();
618
619            // Normally, we start listening when the panel is expanded, but here we need to start
620            // earlier so the state is already up to date when dragging down.
621            setListening(true);
622        }
623        super.onTouchEvent(event);
624        return true;
625    }
626
627    private boolean isInQsArea(float x, float y) {
628        return mStatusBarState != StatusBarState.SHADE
629                || y <= mNotificationStackScroller.getBottomMostNotificationBottom()
630                || y <= mQsContainer.getY() + mQsContainer.getHeight();
631    }
632
633    private void handleQsDown(MotionEvent event) {
634        if (event.getActionMasked() == MotionEvent.ACTION_DOWN
635                && shouldQuickSettingsIntercept(event.getX(), event.getY(), -1)) {
636            mQsTracking = true;
637            onQsExpansionStarted();
638            mInitialHeightOnTouch = mQsExpansionHeight;
639            mInitialTouchY = event.getX();
640            mInitialTouchX = event.getY();
641
642            // If we interrupt an expansion gesture here, make sure to update the state correctly.
643            if (mIsExpanding) {
644                onExpandingFinished();
645            }
646        }
647    }
648
649    @Override
650    protected boolean flingExpands(float vel, float vectorVel) {
651        boolean expands = super.flingExpands(vel, vectorVel);
652
653        // If we are already running a QS expansion, make sure that we keep the panel open.
654        if (mQsExpansionAnimator != null) {
655            expands = true;
656        }
657        return expands;
658    }
659
660    @Override
661    protected boolean hasConflictingGestures() {
662        return mStatusBar.getBarState() != StatusBarState.SHADE;
663    }
664
665    private void onQsTouch(MotionEvent event) {
666        int pointerIndex = event.findPointerIndex(mTrackingPointer);
667        if (pointerIndex < 0) {
668            pointerIndex = 0;
669            mTrackingPointer = event.getPointerId(pointerIndex);
670        }
671        final float y = event.getY(pointerIndex);
672        final float x = event.getX(pointerIndex);
673
674        switch (event.getActionMasked()) {
675            case MotionEvent.ACTION_DOWN:
676                mQsTracking = true;
677                mInitialTouchY = y;
678                mInitialTouchX = x;
679                onQsExpansionStarted();
680                mInitialHeightOnTouch = mQsExpansionHeight;
681                initVelocityTracker();
682                trackMovement(event);
683                break;
684
685            case MotionEvent.ACTION_POINTER_UP:
686                final int upPointer = event.getPointerId(event.getActionIndex());
687                if (mTrackingPointer == upPointer) {
688                    // gesture is ongoing, find a new pointer to track
689                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
690                    final float newY = event.getY(newIndex);
691                    final float newX = event.getX(newIndex);
692                    mTrackingPointer = event.getPointerId(newIndex);
693                    mInitialHeightOnTouch = mQsExpansionHeight;
694                    mInitialTouchY = newY;
695                    mInitialTouchX = newX;
696                }
697                break;
698
699            case MotionEvent.ACTION_MOVE:
700                final float h = y - mInitialTouchY;
701                setQsExpansion(h + mInitialHeightOnTouch);
702                if (h >= mQsFalsingThreshold) {
703                    mQsTouchAboveFalsingThreshold = true;
704                }
705                trackMovement(event);
706                break;
707
708            case MotionEvent.ACTION_UP:
709            case MotionEvent.ACTION_CANCEL:
710                mQsTracking = false;
711                mTrackingPointer = -1;
712                trackMovement(event);
713                float fraction = getQsExpansionFraction();
714                if ((fraction != 0f || y >= mInitialTouchY)
715                        && (fraction != 1f || y <= mInitialTouchY)) {
716                    flingQsWithCurrentVelocity();
717                } else {
718                    mScrollYOverride = -1;
719                }
720                if (mVelocityTracker != null) {
721                    mVelocityTracker.recycle();
722                    mVelocityTracker = null;
723                }
724                break;
725        }
726    }
727
728    @Override
729    public void onOverscrolled(float lastTouchX, float lastTouchY, int amount) {
730        if (mIntercepting && shouldQuickSettingsIntercept(lastTouchX, lastTouchY,
731                -1 /* yDiff: Not relevant here */)) {
732            onQsExpansionStarted(amount);
733            mInitialHeightOnTouch = mQsExpansionHeight;
734            mInitialTouchY = mLastTouchY;
735            mInitialTouchX = mLastTouchX;
736            mQsTracking = true;
737        }
738    }
739
740    @Override
741    public void onOverscrollTopChanged(float amount, boolean isRubberbanded) {
742        cancelAnimation();
743        if (!mQsExpansionEnabled) {
744            amount = 0f;
745        }
746        float rounded = amount >= 1f ? amount : 0f;
747        mStackScrollerOverscrolling = rounded != 0f && isRubberbanded;
748        mQsExpansionFromOverscroll = rounded != 0f;
749        mLastOverscroll = rounded;
750        updateQsState();
751        setQsExpansion(mQsMinExpansionHeight + rounded);
752    }
753
754    @Override
755    public void flingTopOverscroll(float velocity, boolean open) {
756        mLastOverscroll = 0f;
757        setQsExpansion(mQsExpansionHeight);
758        flingSettings(!mQsExpansionEnabled && open ? 0f : velocity, open && mQsExpansionEnabled,
759                new Runnable() {
760            @Override
761            public void run() {
762                mStackScrollerOverscrolling = false;
763                mQsExpansionFromOverscroll = false;
764                updateQsState();
765            }
766        });
767    }
768
769    private void onQsExpansionStarted() {
770        onQsExpansionStarted(0);
771    }
772
773    private void onQsExpansionStarted(int overscrollAmount) {
774        cancelAnimation();
775
776        // Reset scroll position and apply that position to the expanded height.
777        float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount;
778        if (mScrollView.getScrollY() != 0) {
779            mScrollYOverride = mScrollView.getScrollY();
780        }
781        mScrollView.scrollTo(0, 0);
782        setQsExpansion(height);
783    }
784
785    private void setQsExpanded(boolean expanded) {
786        boolean changed = mQsExpanded != expanded;
787        if (changed) {
788            mQsExpanded = expanded;
789            updateQsState();
790            requestPanelHeightUpdate();
791            mNotificationStackScroller.setInterceptDelegateEnabled(expanded);
792            mStatusBar.setQsExpanded(expanded);
793        }
794    }
795
796    public void setBarState(int statusBarState, boolean keyguardFadingAway,
797            boolean goingToFullShade) {
798        boolean keyguardShowing = statusBarState == StatusBarState.KEYGUARD
799                || statusBarState == StatusBarState.SHADE_LOCKED;
800        if (!mKeyguardShowing && keyguardShowing) {
801            setQsTranslation(mQsExpansionHeight);
802            mHeader.setTranslationY(0f);
803        }
804        setKeyguardStatusViewVisibility(statusBarState, keyguardFadingAway, goingToFullShade);
805        setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade);
806        if (goingToFullShade) {
807            animateKeyguardStatusBarOut();
808        } else {
809            mKeyguardStatusBar.setAlpha(1f);
810            mKeyguardStatusBar.setVisibility(keyguardShowing ? View.VISIBLE : View.INVISIBLE);
811        }
812        mStatusBarState = statusBarState;
813        mKeyguardShowing = keyguardShowing;
814        updateQsState();
815        if (goingToFullShade) {
816            animateHeaderSlidingIn();
817        }
818    }
819
820    private final Runnable mAnimateKeyguardStatusViewInvisibleEndRunnable = new Runnable() {
821        @Override
822        public void run() {
823            mKeyguardStatusViewAnimating = false;
824            mKeyguardStatusView.setVisibility(View.GONE);
825        }
826    };
827
828    private final Runnable mAnimateKeyguardStatusViewVisibleEndRunnable = new Runnable() {
829        @Override
830        public void run() {
831            mKeyguardStatusViewAnimating = false;
832        }
833    };
834
835    private final Animator.AnimatorListener mAnimateHeaderSlidingInListener
836            = new AnimatorListenerAdapter() {
837        @Override
838        public void onAnimationEnd(Animator animation) {
839            mHeaderAnimatingIn = false;
840            mQsContainerAnimator = null;
841            mQsContainer.removeOnLayoutChangeListener(mQsContainerAnimatorUpdater);
842        }
843    };
844
845    private final OnLayoutChangeListener mQsContainerAnimatorUpdater
846            = new OnLayoutChangeListener() {
847        @Override
848        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
849                int oldTop, int oldRight, int oldBottom) {
850            int oldHeight = oldBottom - oldTop;
851            int height = bottom - top;
852            if (height != oldHeight && mQsContainerAnimator != null) {
853                PropertyValuesHolder[] values = mQsContainerAnimator.getValues();
854                float newEndValue = mHeader.getCollapsedHeight() + mQsPeekHeight - height - top;
855                float newStartValue = -height - top;
856                values[0].setFloatValues(newStartValue, newEndValue);
857                mQsContainerAnimator.setCurrentPlayTime(mQsContainerAnimator.getCurrentPlayTime());
858            }
859        }
860    };
861
862    private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn
863            = new ViewTreeObserver.OnPreDrawListener() {
864        @Override
865        public boolean onPreDraw() {
866            getViewTreeObserver().removeOnPreDrawListener(this);
867            mHeader.setTranslationY(-mHeader.getCollapsedHeight() - mQsPeekHeight);
868            mHeader.animate()
869                    .translationY(0f)
870                    .setStartDelay(mStatusBar.calculateGoingToFullShadeDelay())
871                    .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE)
872                    .setInterpolator(mFastOutSlowInInterpolator)
873                    .start();
874            mQsContainer.setY(-mQsContainer.getHeight());
875            mQsContainerAnimator = ObjectAnimator.ofFloat(mQsContainer, View.TRANSLATION_Y,
876                    mQsContainer.getTranslationY(),
877                    mHeader.getCollapsedHeight() + mQsPeekHeight - mQsContainer.getHeight()
878                            - mQsContainer.getTop());
879            mQsContainerAnimator.setStartDelay(mStatusBar.calculateGoingToFullShadeDelay());
880            mQsContainerAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE);
881            mQsContainerAnimator.setInterpolator(mFastOutSlowInInterpolator);
882            mQsContainerAnimator.addListener(mAnimateHeaderSlidingInListener);
883            mQsContainerAnimator.start();
884            mQsContainer.addOnLayoutChangeListener(mQsContainerAnimatorUpdater);
885            return true;
886        }
887    };
888
889    private void animateHeaderSlidingIn() {
890        mHeaderAnimatingIn = true;
891        getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn);
892
893    }
894
895    private final Runnable mAnimateKeyguardStatusBarInvisibleEndRunnable = new Runnable() {
896        @Override
897        public void run() {
898            mKeyguardStatusBar.setVisibility(View.INVISIBLE);
899        }
900    };
901
902    private void animateKeyguardStatusBarOut() {
903        mKeyguardStatusBar.animate()
904                .alpha(0f)
905                .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
906                .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2)
907                .setInterpolator(PhoneStatusBar.ALPHA_OUT)
908                .withEndAction(mAnimateKeyguardStatusBarInvisibleEndRunnable)
909                .start();
910    }
911
912    private final Runnable mAnimateKeyguardBottomAreaInvisibleEndRunnable = new Runnable() {
913        @Override
914        public void run() {
915            mKeyguardBottomArea.setVisibility(View.GONE);
916        }
917    };
918
919    private void setKeyguardBottomAreaVisibility(int statusBarState,
920            boolean goingToFullShade) {
921        if (goingToFullShade) {
922            mKeyguardBottomArea.animate().cancel();
923            mKeyguardBottomArea.animate()
924                    .alpha(0f)
925                    .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
926                    .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2)
927                    .setInterpolator(PhoneStatusBar.ALPHA_OUT)
928                    .withEndAction(mAnimateKeyguardBottomAreaInvisibleEndRunnable)
929                    .start();
930        } else if (statusBarState == StatusBarState.KEYGUARD
931                || statusBarState == StatusBarState.SHADE_LOCKED) {
932            mKeyguardBottomArea.animate().cancel();
933            mKeyguardBottomArea.setVisibility(View.VISIBLE);
934            mKeyguardBottomArea.setAlpha(1f);
935        } else {
936            mKeyguardBottomArea.animate().cancel();
937            mKeyguardBottomArea.setVisibility(View.GONE);
938            mKeyguardBottomArea.setAlpha(1f);
939        }
940    }
941
942    private void setKeyguardStatusViewVisibility(int statusBarState, boolean keyguardFadingAway,
943            boolean goingToFullShade) {
944        if ((!keyguardFadingAway && mStatusBarState == StatusBarState.KEYGUARD
945                && statusBarState != StatusBarState.KEYGUARD) || goingToFullShade) {
946            mKeyguardStatusView.animate().cancel();
947            mKeyguardStatusViewAnimating = true;
948            mKeyguardStatusView.animate()
949                    .alpha(0f)
950                    .setStartDelay(0)
951                    .setDuration(160)
952                    .setInterpolator(PhoneStatusBar.ALPHA_OUT)
953                    .withEndAction(mAnimateKeyguardStatusViewInvisibleEndRunnable);
954            if (keyguardFadingAway) {
955                mKeyguardStatusView.animate()
956                        .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
957                        .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2)
958                        .start();
959            }
960        } else if (mStatusBarState == StatusBarState.SHADE_LOCKED
961                && statusBarState == StatusBarState.KEYGUARD) {
962            mKeyguardStatusView.animate().cancel();
963            mKeyguardStatusView.setVisibility(View.VISIBLE);
964            mKeyguardStatusViewAnimating = true;
965            mKeyguardStatusView.setAlpha(0f);
966            mKeyguardStatusView.animate()
967                    .alpha(1f)
968                    .setStartDelay(0)
969                    .setDuration(320)
970                    .setInterpolator(PhoneStatusBar.ALPHA_IN)
971                    .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable);
972        } else if (statusBarState == StatusBarState.KEYGUARD) {
973            mKeyguardStatusView.animate().cancel();
974            mKeyguardStatusView.setVisibility(View.VISIBLE);
975            mKeyguardStatusView.setAlpha(1f);
976        } else {
977            mKeyguardStatusView.animate().cancel();
978            mKeyguardStatusView.setVisibility(View.GONE);
979            mKeyguardStatusView.setAlpha(1f);
980        }
981    }
982
983    private void updateQsState() {
984        boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling;
985        mHeader.setVisibility((mQsExpanded || !mKeyguardShowing) ? View.VISIBLE : View.INVISIBLE);
986        mHeader.setExpanded(mKeyguardShowing || (mQsExpanded && !mStackScrollerOverscrolling));
987        mNotificationStackScroller.setScrollingEnabled(
988                mStatusBarState != StatusBarState.KEYGUARD && (!mQsExpanded
989                        || mQsExpansionFromOverscroll));
990        mQsPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE);
991        mQsContainer.setVisibility(
992                mKeyguardShowing && !expandVisually ? View.INVISIBLE : View.VISIBLE);
993        mScrollView.setTouchEnabled(mQsExpanded);
994        updateEmptyShadeView();
995        mQsNavbarScrim.setVisibility(mStatusBarState == StatusBarState.SHADE && mQsExpanded
996                && !mStackScrollerOverscrolling && mQsScrimEnabled
997                        ? View.VISIBLE
998                        : View.INVISIBLE);
999        if (mKeyguardUserSwitcher != null && mQsExpanded && !mStackScrollerOverscrolling) {
1000            mKeyguardUserSwitcher.hide(true /* animate */);
1001        }
1002    }
1003
1004    private void setQsExpansion(float height) {
1005        height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
1006        mQsFullyExpanded = height == mQsMaxExpansionHeight;
1007        if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) {
1008            setQsExpanded(true);
1009        } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
1010            setQsExpanded(false);
1011            if (mLastAnnouncementWasQuickSettings && !mTracking) {
1012                announceForAccessibility(getKeyguardOrLockScreenString());
1013                mLastAnnouncementWasQuickSettings = false;
1014            }
1015        }
1016        mQsExpansionHeight = height;
1017        mHeader.setExpansion(getHeaderExpansionFraction());
1018        setQsTranslation(height);
1019        requestScrollerTopPaddingUpdate(false /* animate */);
1020        updateNotificationScrim(height);
1021        if (mKeyguardShowing) {
1022            updateHeaderKeyguard();
1023        }
1024        if (mStatusBarState == StatusBarState.SHADE && mQsExpanded
1025                && !mStackScrollerOverscrolling && mQsScrimEnabled) {
1026            mQsNavbarScrim.setAlpha(getQsExpansionFraction());
1027        }
1028
1029        // Upon initialisation when we are not layouted yet we don't want to announce that we are
1030        // fully expanded, hence the != 0.0f check.
1031        if (height != 0.0f && mQsFullyExpanded && !mLastAnnouncementWasQuickSettings) {
1032            announceForAccessibility(getContext().getString(
1033                    R.string.accessibility_desc_quick_settings));
1034            mLastAnnouncementWasQuickSettings = true;
1035        }
1036    }
1037
1038    private String getKeyguardOrLockScreenString() {
1039        if (mStatusBarState == StatusBarState.KEYGUARD) {
1040            return getContext().getString(R.string.accessibility_desc_lock_screen);
1041        } else {
1042            return getContext().getString(R.string.accessibility_desc_notification_shade);
1043        }
1044    }
1045
1046    private void updateNotificationScrim(float height) {
1047        int startDistance = mQsMinExpansionHeight + mNotificationScrimWaitDistance;
1048        float progress = (height - startDistance) / (mQsMaxExpansionHeight - startDistance);
1049        progress = Math.max(0.0f, Math.min(progress, 1.0f));
1050    }
1051
1052    private float getHeaderExpansionFraction() {
1053        if (!mKeyguardShowing) {
1054            return getQsExpansionFraction();
1055        } else {
1056            return 1f;
1057        }
1058    }
1059
1060    private void setQsTranslation(float height) {
1061        if (!mHeaderAnimatingIn) {
1062            mQsContainer.setY(height - mQsContainer.getHeight() + getHeaderTranslation());
1063        }
1064        if (mKeyguardShowing) {
1065            mHeader.setY(interpolate(getQsExpansionFraction(), -mHeader.getHeight(), 0));
1066        }
1067    }
1068
1069    private float calculateQsTopPadding() {
1070        // We can only do the smoother transition on Keyguard when we also are not collapsing from a
1071        // scrolled quick settings.
1072        if (mKeyguardShowing && mScrollYOverride == -1) {
1073            return interpolate(getQsExpansionFraction(),
1074                    mNotificationStackScroller.getIntrinsicPadding() - mNotificationTopPadding,
1075                    mQsMaxExpansionHeight);
1076        } else {
1077            return mQsExpansionHeight;
1078        }
1079    }
1080
1081    private void requestScrollerTopPaddingUpdate(boolean animate) {
1082        mNotificationStackScroller.updateTopPadding(calculateQsTopPadding(),
1083                mScrollView.getScrollY(),
1084                mAnimateNextTopPaddingChange || animate);
1085        mAnimateNextTopPaddingChange = false;
1086    }
1087
1088    private void trackMovement(MotionEvent event) {
1089        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
1090        mLastTouchX = event.getX();
1091        mLastTouchY = event.getY();
1092    }
1093
1094    private void initVelocityTracker() {
1095        if (mVelocityTracker != null) {
1096            mVelocityTracker.recycle();
1097        }
1098        mVelocityTracker = VelocityTracker.obtain();
1099    }
1100
1101    private float getCurrentVelocity() {
1102        if (mVelocityTracker == null) {
1103            return 0;
1104        }
1105        mVelocityTracker.computeCurrentVelocity(1000);
1106        return mVelocityTracker.getYVelocity();
1107    }
1108
1109    private void cancelAnimation() {
1110        if (mQsExpansionAnimator != null) {
1111            mQsExpansionAnimator.cancel();
1112        }
1113    }
1114
1115    private void flingSettings(float vel, boolean expand) {
1116        flingSettings(vel, expand, null);
1117    }
1118
1119    private void flingSettings(float vel, boolean expand, final Runnable onFinishRunnable) {
1120        float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight;
1121        if (target == mQsExpansionHeight) {
1122            mScrollYOverride = -1;
1123            if (onFinishRunnable != null) {
1124                onFinishRunnable.run();
1125            }
1126            return;
1127        }
1128        boolean belowFalsingThreshold = isBelowFalsingThreshold();
1129        if (belowFalsingThreshold) {
1130            vel = 0;
1131        }
1132        mScrollView.setBlockFlinging(true);
1133        ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
1134        mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel);
1135        if (belowFalsingThreshold) {
1136            animator.setDuration(350);
1137        }
1138        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1139            @Override
1140            public void onAnimationUpdate(ValueAnimator animation) {
1141                setQsExpansion((Float) animation.getAnimatedValue());
1142            }
1143        });
1144        animator.addListener(new AnimatorListenerAdapter() {
1145            @Override
1146            public void onAnimationEnd(Animator animation) {
1147                mScrollView.setBlockFlinging(false);
1148                mScrollYOverride = -1;
1149                mQsExpansionAnimator = null;
1150                if (onFinishRunnable != null) {
1151                    onFinishRunnable.run();
1152                }
1153            }
1154        });
1155        animator.start();
1156        mQsExpansionAnimator = animator;
1157        mQsAnimatorExpand = expand;
1158    }
1159
1160    /**
1161     * @return Whether we should intercept a gesture to open Quick Settings.
1162     */
1163    private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
1164        if (!mQsExpansionEnabled) {
1165            return false;
1166        }
1167        View header = mKeyguardShowing ? mKeyguardStatusBar : mHeader;
1168        boolean onHeader = x >= header.getLeft() && x <= header.getRight()
1169                && y >= header.getTop() && y <= header.getBottom();
1170        if (mQsExpanded) {
1171            return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0) && isInQsArea(x, y);
1172        } else {
1173            return onHeader;
1174        }
1175    }
1176
1177    @Override
1178    protected boolean isScrolledToBottom() {
1179        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
1180            return true;
1181        }
1182        if (!isInSettings()) {
1183            return mNotificationStackScroller.isScrolledToBottom();
1184        } else {
1185            return mScrollView.isScrolledToBottom();
1186        }
1187    }
1188
1189    @Override
1190    protected int getMaxPanelHeight() {
1191        int min = mStatusBarMinHeight;
1192        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD
1193                && mNotificationStackScroller.getNotGoneChildCount() == 0) {
1194            int minHeight = (int) ((mQsMinExpansionHeight + getOverExpansionAmount())
1195                    * HEADER_RUBBERBAND_FACTOR);
1196            min = Math.max(min, minHeight);
1197        }
1198        int maxHeight;
1199        if (mTwoFingerQsExpand || mQsExpanded || mIsExpanding && mQsExpandedWhenExpandingStarted) {
1200            maxHeight = Math.max(calculatePanelHeightQsExpanded(), calculatePanelHeightShade());
1201        } else {
1202            maxHeight = calculatePanelHeightShade();
1203        }
1204        maxHeight = Math.max(maxHeight, min);
1205        return maxHeight;
1206    }
1207
1208    private boolean isInSettings() {
1209        return mQsExpanded;
1210    }
1211
1212    @Override
1213    protected void onHeightUpdated(float expandedHeight) {
1214        if (!mQsExpanded) {
1215            positionClockAndNotifications();
1216        }
1217        if (mTwoFingerQsExpand || mQsExpanded && !mQsTracking && mQsExpansionAnimator == null
1218                && !mQsExpansionFromOverscroll) {
1219            float panelHeightQsCollapsed = mNotificationStackScroller.getIntrinsicPadding()
1220                    + mNotificationStackScroller.getMinStackHeight()
1221                    + mNotificationStackScroller.getNotificationTopPadding();
1222            float panelHeightQsExpanded = calculatePanelHeightQsExpanded();
1223            float t = (expandedHeight - panelHeightQsCollapsed)
1224                    / (panelHeightQsExpanded - panelHeightQsCollapsed);
1225
1226            setQsExpansion(mQsMinExpansionHeight
1227                    + t * (getTempQsMaxExpansion() - mQsMinExpansionHeight));
1228        }
1229        mNotificationStackScroller.setStackHeight(expandedHeight);
1230        updateHeader();
1231        updateUnlockIcon();
1232        updateNotificationTranslucency();
1233    }
1234
1235    /**
1236     * @return a temporary override of {@link #mQsMaxExpansionHeight}, which is needed when
1237     *         collapsing QS / the panel when QS was scrolled
1238     */
1239    private int getTempQsMaxExpansion() {
1240        int qsTempMaxExpansion = mQsMaxExpansionHeight;
1241        if (mScrollYOverride != -1) {
1242            qsTempMaxExpansion -= mScrollYOverride;
1243        }
1244        return qsTempMaxExpansion;
1245    }
1246
1247    private int calculatePanelHeightShade() {
1248        int emptyBottomMargin = mNotificationStackScroller.getEmptyBottomMargin();
1249        int maxHeight = mNotificationStackScroller.getHeight() - emptyBottomMargin
1250                - mTopPaddingAdjustment;
1251        maxHeight += mNotificationStackScroller.getTopPaddingOverflow();
1252        return maxHeight;
1253    }
1254
1255    private int calculatePanelHeightQsExpanded() {
1256        float notificationHeight = mNotificationStackScroller.getHeight()
1257                - mNotificationStackScroller.getEmptyBottomMargin()
1258                - mNotificationStackScroller.getTopPadding();
1259        float totalHeight = mQsMaxExpansionHeight + notificationHeight
1260                + mNotificationStackScroller.getNotificationTopPadding();
1261        if (totalHeight > mNotificationStackScroller.getHeight()) {
1262            float fullyCollapsedHeight = mQsMaxExpansionHeight
1263                    + mNotificationStackScroller.getMinStackHeight()
1264                    + mNotificationStackScroller.getNotificationTopPadding()
1265                    - getScrollViewScrollY();
1266            totalHeight = Math.max(fullyCollapsedHeight, mNotificationStackScroller.getHeight());
1267        }
1268        return (int) totalHeight;
1269    }
1270
1271    private int getScrollViewScrollY() {
1272        if (mScrollYOverride != -1) {
1273            return mScrollYOverride;
1274        } else {
1275            return mScrollView.getScrollY();
1276        }
1277    }
1278    private void updateNotificationTranslucency() {
1279        float alpha = (getNotificationsTopY() + mNotificationStackScroller.getItemHeight())
1280                / (mQsMinExpansionHeight + mNotificationStackScroller.getBottomStackPeekSize()
1281                        - mNotificationStackScroller.getCollapseSecondCardPadding());
1282        alpha = Math.max(0, Math.min(alpha, 1));
1283        alpha = (float) Math.pow(alpha, 0.75);
1284        if (alpha != 1f && mNotificationStackScroller.getLayerType() != LAYER_TYPE_HARDWARE) {
1285            mNotificationStackScroller.setLayerType(LAYER_TYPE_HARDWARE, null);
1286        } else if (alpha == 1f
1287                && mNotificationStackScroller.getLayerType() == LAYER_TYPE_HARDWARE) {
1288            mNotificationStackScroller.setLayerType(LAYER_TYPE_NONE, null);
1289        }
1290        mNotificationStackScroller.setAlpha(alpha);
1291    }
1292
1293    @Override
1294    protected float getOverExpansionAmount() {
1295        return mNotificationStackScroller.getCurrentOverScrollAmount(true /* top */);
1296    }
1297
1298    @Override
1299    protected float getOverExpansionPixels() {
1300        return mNotificationStackScroller.getCurrentOverScrolledPixels(true /* top */);
1301    }
1302
1303    private void updateUnlockIcon() {
1304        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1305                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
1306            boolean active = getMaxPanelHeight() - getExpandedHeight() > mUnlockMoveDistance;
1307            KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon();
1308            if (active && !mUnlockIconActive && mTracking) {
1309                lockIcon.setImageAlpha(1.0f, true, 150, mFastOutLinearInterpolator, null);
1310                lockIcon.setImageScale(LOCK_ICON_ACTIVE_SCALE, true, 150,
1311                        mFastOutLinearInterpolator);
1312            } else if (!active && mUnlockIconActive && mTracking) {
1313                lockIcon.setImageAlpha(KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT, true,
1314                        150, mFastOutLinearInterpolator, null);
1315                lockIcon.setImageScale(1.0f, true, 150,
1316                        mFastOutLinearInterpolator);
1317            }
1318            mUnlockIconActive = active;
1319        }
1320    }
1321
1322    /**
1323     * Hides the header when notifications are colliding with it.
1324     */
1325    private void updateHeader() {
1326        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1327                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
1328            updateHeaderKeyguard();
1329        } else {
1330            updateHeaderShade();
1331        }
1332
1333    }
1334
1335    private void updateHeaderShade() {
1336        if (!mHeaderAnimatingIn) {
1337            mHeader.setTranslationY(getHeaderTranslation());
1338        }
1339        setQsTranslation(mQsExpansionHeight);
1340    }
1341
1342    private float getHeaderTranslation() {
1343        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1344                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
1345            return 0;
1346        }
1347        if (mNotificationStackScroller.getNotGoneChildCount() == 0) {
1348            if (mExpandedHeight / HEADER_RUBBERBAND_FACTOR >= mQsMinExpansionHeight) {
1349                return 0;
1350            } else {
1351                return mExpandedHeight / HEADER_RUBBERBAND_FACTOR - mQsMinExpansionHeight;
1352            }
1353        }
1354        return Math.min(0, mNotificationStackScroller.getTranslationY()) / HEADER_RUBBERBAND_FACTOR;
1355    }
1356
1357    private void updateHeaderKeyguard() {
1358        float alphaNotifications;
1359        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
1360
1361            // When on Keyguard, we hide the header as soon as the top card of the notification
1362            // stack scroller is close enough (collision distance) to the bottom of the header.
1363            alphaNotifications = getNotificationsTopY()
1364                    /
1365                    (mKeyguardStatusBar.getHeight() + mNotificationsHeaderCollideDistance);
1366        } else {
1367
1368            // In SHADE_LOCKED, the top card is already really close to the header. Hide it as
1369            // soon as we start translating the stack.
1370            alphaNotifications = getNotificationsTopY() / mKeyguardStatusBar.getHeight();
1371        }
1372        alphaNotifications = MathUtils.constrain(alphaNotifications, 0, 1);
1373        alphaNotifications = (float) Math.pow(alphaNotifications, 0.75);
1374        float alphaQsExpansion = 1 - Math.min(1, getQsExpansionFraction() * 2);
1375        mKeyguardStatusBar.setAlpha(Math.min(alphaNotifications, alphaQsExpansion));
1376        mKeyguardBottomArea.setAlpha(Math.min(1 - getQsExpansionFraction(), alphaNotifications));
1377        setQsTranslation(mQsExpansionHeight);
1378    }
1379
1380    private float getNotificationsTopY() {
1381        if (mNotificationStackScroller.getNotGoneChildCount() == 0) {
1382            return getExpandedHeight();
1383        }
1384        return mNotificationStackScroller.getNotificationsTopY();
1385    }
1386
1387    @Override
1388    protected void onExpandingStarted() {
1389        super.onExpandingStarted();
1390        mNotificationStackScroller.onExpansionStarted();
1391        mIsExpanding = true;
1392        mQsExpandedWhenExpandingStarted = mQsExpanded;
1393        if (mQsExpanded) {
1394            onQsExpansionStarted();
1395        }
1396    }
1397
1398    @Override
1399    protected void onExpandingFinished() {
1400        super.onExpandingFinished();
1401        mNotificationStackScroller.onExpansionStopped();
1402        mIsExpanding = false;
1403        mScrollYOverride = -1;
1404        if (mExpandedHeight == 0f) {
1405            setListening(false);
1406        } else {
1407            setListening(true);
1408        }
1409        mTwoFingerQsExpand = false;
1410        mTwoFingerQsExpandPossible = false;
1411    }
1412
1413    private void setListening(boolean listening) {
1414        mHeader.setListening(listening);
1415        mKeyguardStatusBar.setListening(listening);
1416        mQsPanel.setListening(listening);
1417    }
1418
1419    @Override
1420    public void instantExpand() {
1421        super.instantExpand();
1422        setListening(true);
1423    }
1424
1425    @Override
1426    protected void setOverExpansion(float overExpansion, boolean isPixels) {
1427        if (mConflictingQsExpansionGesture || mTwoFingerQsExpand) {
1428            return;
1429        }
1430        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) {
1431            mNotificationStackScroller.setOnHeightChangedListener(null);
1432            if (isPixels) {
1433                mNotificationStackScroller.setOverScrolledPixels(
1434                        overExpansion, true /* onTop */, false /* animate */);
1435            } else {
1436                mNotificationStackScroller.setOverScrollAmount(
1437                        overExpansion, true /* onTop */, false /* animate */);
1438            }
1439            mNotificationStackScroller.setOnHeightChangedListener(this);
1440        }
1441    }
1442
1443    @Override
1444    protected void onTrackingStarted() {
1445        super.onTrackingStarted();
1446        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1447                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
1448            mAfforanceHelper.animateHideLeftRightIcon();
1449        }
1450        if (mQsExpanded) {
1451            mTwoFingerQsExpand = true;
1452        }
1453    }
1454
1455    @Override
1456    protected void onTrackingStopped(boolean expand) {
1457        super.onTrackingStopped(expand);
1458        if (expand) {
1459            mNotificationStackScroller.setOverScrolledPixels(
1460                    0.0f, true /* onTop */, true /* animate */);
1461        }
1462        if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1463                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
1464            if (!mHintAnimationRunning) {
1465                mAfforanceHelper.reset(true);
1466            }
1467        }
1468        if (!expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1469                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
1470            KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon();
1471            lockIcon.setImageAlpha(0.0f, true, 100, mFastOutLinearInterpolator, null);
1472            lockIcon.setImageScale(2.0f, true, 100, mFastOutLinearInterpolator);
1473        }
1474    }
1475
1476    @Override
1477    public void onHeightChanged(ExpandableView view) {
1478
1479        // Block update if we are in quick settings and just the top padding changed
1480        // (i.e. view == null).
1481        if (view == null && mQsExpanded) {
1482            return;
1483        }
1484        requestPanelHeightUpdate();
1485    }
1486
1487    @Override
1488    public void onReset(ExpandableView view) {
1489    }
1490
1491    @Override
1492    public void onScrollChanged() {
1493        if (mQsExpanded) {
1494            requestScrollerTopPaddingUpdate(false /* animate */);
1495            requestPanelHeightUpdate();
1496        }
1497    }
1498
1499    @Override
1500    protected void onConfigurationChanged(Configuration newConfig) {
1501        super.onConfigurationChanged(newConfig);
1502        mAfforanceHelper.onConfigurationChanged();
1503    }
1504
1505    @Override
1506    public void onClick(View v) {
1507        if (v == mHeader) {
1508            onQsExpansionStarted();
1509            if (mQsExpanded) {
1510                flingSettings(0 /* vel */, false /* expand */);
1511            } else if (mQsExpansionEnabled) {
1512                flingSettings(0 /* vel */, true /* expand */);
1513            }
1514        }
1515    }
1516
1517    @Override
1518    public void onAnimationToSideStarted(boolean rightPage) {
1519        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage;
1520        mIsLaunchTransitionRunning = true;
1521        mLaunchAnimationEndRunnable = null;
1522        if (start) {
1523            mKeyguardBottomArea.launchPhone();
1524        } else {
1525            mSecureCameraLaunchManager.startSecureCameraLaunch();
1526        }
1527        mBlockTouches = true;
1528    }
1529
1530    @Override
1531    public void onAnimationToSideEnded() {
1532        mIsLaunchTransitionRunning = false;
1533        mIsLaunchTransitionFinished = true;
1534        if (mLaunchAnimationEndRunnable != null) {
1535            mLaunchAnimationEndRunnable.run();
1536            mLaunchAnimationEndRunnable = null;
1537        }
1538    }
1539
1540    @Override
1541    protected void onEdgeClicked(boolean right) {
1542        if ((right && getRightIcon().getVisibility() != View.VISIBLE)
1543                || (!right && getLeftIcon().getVisibility() != View.VISIBLE)) {
1544            return;
1545        }
1546        mHintAnimationRunning = true;
1547        mAfforanceHelper.startHintAnimation(right, new Runnable() {
1548            @Override
1549            public void run() {
1550                mHintAnimationRunning = false;
1551                mStatusBar.onHintFinished();
1552            }
1553        });
1554        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? right : !right;
1555        if (start) {
1556            mStatusBar.onPhoneHintStarted();
1557        } else {
1558            mStatusBar.onCameraHintStarted();
1559        }
1560    }
1561
1562    @Override
1563    protected void startUnlockHintAnimation() {
1564        super.startUnlockHintAnimation();
1565        startHighlightIconAnimation(getCenterIcon());
1566    }
1567
1568    /**
1569     * Starts the highlight (making it fully opaque) animation on an icon.
1570     */
1571    private void startHighlightIconAnimation(final KeyguardAffordanceView icon) {
1572        icon.setImageAlpha(1.0f, true, KeyguardAffordanceHelper.HINT_PHASE1_DURATION,
1573                mFastOutSlowInInterpolator, new Runnable() {
1574                    @Override
1575                    public void run() {
1576                        icon.setImageAlpha(KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT,
1577                                true, KeyguardAffordanceHelper.HINT_PHASE1_DURATION,
1578                                mFastOutSlowInInterpolator, null);
1579                    }
1580                });
1581    }
1582
1583    @Override
1584    public float getPageWidth() {
1585        return getWidth();
1586    }
1587
1588    @Override
1589    public void onSwipingStarted() {
1590        mSecureCameraLaunchManager.onSwipingStarted();
1591        requestDisallowInterceptTouchEvent(true);
1592        mOnlyAffordanceInThisMotion = true;
1593    }
1594
1595    @Override
1596    public KeyguardAffordanceView getLeftIcon() {
1597        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1598                ? mKeyguardBottomArea.getCameraView()
1599                : mKeyguardBottomArea.getPhoneView();
1600    }
1601
1602    @Override
1603    public KeyguardAffordanceView getCenterIcon() {
1604        return mKeyguardBottomArea.getLockIcon();
1605    }
1606
1607    @Override
1608    public KeyguardAffordanceView getRightIcon() {
1609        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1610                ? mKeyguardBottomArea.getPhoneView()
1611                : mKeyguardBottomArea.getCameraView();
1612    }
1613
1614    @Override
1615    public View getLeftPreview() {
1616        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1617                ? mKeyguardBottomArea.getCameraPreview()
1618                : mKeyguardBottomArea.getPhonePreview();
1619    }
1620
1621    @Override
1622    public View getRightPreview() {
1623        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1624                ? mKeyguardBottomArea.getPhonePreview()
1625                : mKeyguardBottomArea.getCameraPreview();
1626    }
1627
1628    @Override
1629    protected float getPeekHeight() {
1630        if (mNotificationStackScroller.getNotGoneChildCount() > 0) {
1631            return mNotificationStackScroller.getPeekHeight();
1632        } else {
1633            return mQsMinExpansionHeight * HEADER_RUBBERBAND_FACTOR;
1634        }
1635    }
1636
1637    @Override
1638    protected float getCannedFlingDurationFactor() {
1639        if (mQsExpanded) {
1640            return 0.7f;
1641        } else {
1642            return 0.6f;
1643        }
1644    }
1645
1646    @Override
1647    protected boolean fullyExpandedClearAllVisible() {
1648        return mNotificationStackScroller.isDismissViewNotGone()
1649                && mNotificationStackScroller.isScrolledToBottom() && !mTwoFingerQsExpand;
1650    }
1651
1652    @Override
1653    protected boolean isClearAllVisible() {
1654        return mNotificationStackScroller.isDismissViewVisible();
1655    }
1656
1657    @Override
1658    protected int getClearAllHeight() {
1659        return mNotificationStackScroller.getDismissViewHeight();
1660    }
1661
1662    @Override
1663    protected boolean isTrackingBlocked() {
1664        return mConflictingQsExpansionGesture && mQsExpanded;
1665    }
1666
1667    public void notifyVisibleChildrenChanged() {
1668        if (mNotificationStackScroller.getNotGoneChildCount() != 0) {
1669            mReserveNotificationSpace.setVisibility(View.VISIBLE);
1670        } else {
1671            mReserveNotificationSpace.setVisibility(View.GONE);
1672        }
1673    }
1674
1675    public boolean isQsExpanded() {
1676        return mQsExpanded;
1677    }
1678
1679    public boolean isQsDetailShowing() {
1680        return mQsPanel.isShowingDetail();
1681    }
1682
1683    public void closeQsDetail() {
1684        mQsPanel.closeDetail();
1685    }
1686
1687    @Override
1688    public boolean shouldDelayChildPressedState() {
1689        return true;
1690    }
1691
1692    public boolean isLaunchTransitionFinished() {
1693        return mIsLaunchTransitionFinished;
1694    }
1695
1696    public boolean isLaunchTransitionRunning() {
1697        return mIsLaunchTransitionRunning;
1698    }
1699
1700    public void setLaunchTransitionEndRunnable(Runnable r) {
1701        mLaunchAnimationEndRunnable = r;
1702    }
1703
1704    public void setEmptyDragAmount(float amount) {
1705        float factor = 0.8f;
1706        if (mNotificationStackScroller.getNotGoneChildCount() > 0) {
1707            factor = 0.4f;
1708        } else if (!mStatusBar.hasActiveNotifications()) {
1709            factor = 0.4f;
1710        }
1711        mEmptyDragAmount = amount * factor;
1712        positionClockAndNotifications();
1713    }
1714
1715    private static float interpolate(float t, float start, float end) {
1716        return (1 - t) * start + t * end;
1717    }
1718
1719    private void updateKeyguardStatusBarVisibility() {
1720        mKeyguardStatusBar.setVisibility(mKeyguardShowing && !mDozing ? VISIBLE : INVISIBLE);
1721    }
1722
1723    public void setDozing(boolean dozing) {
1724        if (dozing == mDozing) return;
1725        mDozing = dozing;
1726        if (mDozing) {
1727            setBackgroundColor(0xff000000);
1728        } else {
1729            setBackground(null);
1730        }
1731        updateKeyguardStatusBarVisibility();
1732    }
1733
1734    public void setShadeEmpty(boolean shadeEmpty) {
1735        mShadeEmpty = shadeEmpty;
1736        updateEmptyShadeView();
1737    }
1738
1739    private void updateEmptyShadeView() {
1740
1741        // Hide "No notifications" in QS.
1742        mNotificationStackScroller.updateEmptyShadeView(mShadeEmpty && !mQsExpanded);
1743    }
1744
1745    public void setQsScrimEnabled(boolean qsScrimEnabled) {
1746        boolean changed = mQsScrimEnabled != qsScrimEnabled;
1747        mQsScrimEnabled = qsScrimEnabled;
1748        if (changed) {
1749            updateQsState();
1750        }
1751    }
1752
1753    public void setKeyguardUserSwitcher(KeyguardUserSwitcher keyguardUserSwitcher) {
1754        mKeyguardUserSwitcher = keyguardUserSwitcher;
1755    }
1756
1757    private final Runnable mUpdateHeader = new Runnable() {
1758        @Override
1759        public void run() {
1760            mHeader.updateEverything();
1761        }
1762    };
1763
1764    public void onScreenTurnedOn() {
1765        mKeyguardStatusView.refreshTime();
1766    }
1767}
1768