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