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