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