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