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