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