NotificationPanelView.java revision 90c946583d98162cc261f051e836ba84605a70a2
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.15f;
62    private static final float LOCK_ICON_ACTIVE_SCALE = 1.2f;
63
64    private KeyguardAffordanceHelper mAfforanceHelper;
65    private StatusBarHeaderView mHeader;
66    private KeyguardUserSwitcher mKeyguardUserSwitcher;
67    private KeyguardStatusBarView mKeyguardStatusBar;
68    private View mQsContainer;
69    private QSPanel mQsPanel;
70    private View mKeyguardStatusView;
71    private ObservableScrollView mScrollView;
72    private TextView mClockView;
73    private View mReserveNotificationSpace;
74    private MirrorView mSystemIconsCopy;
75    private View mQsNavbarScrim;
76    private View mNotificationContainerParent;
77    private NotificationStackScrollLayout mNotificationStackScroller;
78    private int mNotificationTopPadding;
79    private boolean mAnimateNextTopPaddingChange;
80
81    private int mTrackingPointer;
82    private VelocityTracker mVelocityTracker;
83    private boolean mQsTracking;
84
85    /**
86     * Handles launching the secure camera properly even when other applications may be using the
87     * camera hardware.
88     */
89    private SecureCameraLaunchManager mSecureCameraLaunchManager;
90
91    /**
92     * If set, the ongoing touch gesture might both trigger the expansion in {@link PanelView} and
93     * the expansion for quick settings.
94     */
95    private boolean mConflictingQsExpansionGesture;
96
97    /**
98     * Whether we are currently handling a motion gesture in #onInterceptTouchEvent, but haven't
99     * intercepted yet.
100     */
101    private boolean mIntercepting;
102    private boolean mQsExpanded;
103    private boolean mQsExpandedWhenExpandingStarted;
104    private boolean mQsFullyExpanded;
105    private boolean mKeyguardShowing;
106    private boolean mDozing;
107    private 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
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().add(getKeyguardOrLockScreenString());
429            mLastAnnouncementWasQuickSettings = false;
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        setQsExpansion(mQsExpansionHeight);
738        flingSettings(velocity, open, new Runnable() {
739            @Override
740            public void run() {
741                mStackScrollerOverscrolling = false;
742                mQsExpansionFromOverscroll = false;
743                updateQsState();
744            }
745        });
746    }
747
748    private void onQsExpansionStarted() {
749        onQsExpansionStarted(0);
750    }
751
752    private void onQsExpansionStarted(int overscrollAmount) {
753        cancelAnimation();
754
755        // Reset scroll position and apply that position to the expanded height.
756        float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount;
757        if (mScrollView.getScrollY() != 0) {
758            mScrollYOverride = mScrollView.getScrollY();
759        }
760        mScrollView.scrollTo(0, 0);
761        setQsExpansion(height);
762    }
763
764    private void setQsExpanded(boolean expanded) {
765        boolean changed = mQsExpanded != expanded;
766        if (changed) {
767            mQsExpanded = expanded;
768            updateQsState();
769            requestPanelHeightUpdate();
770            mNotificationStackScroller.setInterceptDelegateEnabled(expanded);
771            mStatusBar.setQsExpanded(expanded);
772        }
773    }
774
775    public void setBarState(int statusBarState, boolean keyguardFadingAway,
776            boolean goingToFullShade) {
777        boolean keyguardShowing = statusBarState == StatusBarState.KEYGUARD
778                || statusBarState == StatusBarState.SHADE_LOCKED;
779        if (!mKeyguardShowing && keyguardShowing) {
780            setQsTranslation(mQsExpansionHeight);
781            mHeader.setTranslationY(0f);
782        }
783        setKeyguardStatusViewVisibility(statusBarState, keyguardFadingAway, goingToFullShade);
784        setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade);
785        if (goingToFullShade) {
786            animateKeyguardStatusBarOut();
787        } else {
788            mKeyguardStatusBar.setAlpha(1f);
789            mKeyguardStatusBar.setVisibility(keyguardShowing ? View.VISIBLE : View.INVISIBLE);
790        }
791        mStatusBarState = statusBarState;
792        mKeyguardShowing = keyguardShowing;
793        updateQsState();
794        if (goingToFullShade) {
795            animateHeaderSlidingIn();
796        }
797    }
798
799    private final Runnable mAnimateKeyguardStatusViewInvisibleEndRunnable = new Runnable() {
800        @Override
801        public void run() {
802            mKeyguardStatusViewAnimating = false;
803            mKeyguardStatusView.setVisibility(View.GONE);
804        }
805    };
806
807    private final Runnable mAnimateKeyguardStatusViewVisibleEndRunnable = new Runnable() {
808        @Override
809        public void run() {
810            mKeyguardStatusViewAnimating = false;
811        }
812    };
813
814    private final Animator.AnimatorListener mAnimateHeaderSlidingInListener
815            = new AnimatorListenerAdapter() {
816        @Override
817        public void onAnimationEnd(Animator animation) {
818            mHeaderAnimatingIn = false;
819            mQsContainerAnimator = null;
820            mQsContainer.removeOnLayoutChangeListener(mQsContainerAnimatorUpdater);
821        }
822    };
823
824    private final OnLayoutChangeListener mQsContainerAnimatorUpdater
825            = new OnLayoutChangeListener() {
826        @Override
827        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
828                int oldTop, int oldRight, int oldBottom) {
829            int oldHeight = oldBottom - oldTop;
830            int height = bottom - top;
831            if (height != oldHeight && mQsContainerAnimator != null) {
832                PropertyValuesHolder[] values = mQsContainerAnimator.getValues();
833                float newEndValue = mHeader.getCollapsedHeight() + mQsPeekHeight - height - top;
834                float newStartValue = -height - top;
835                values[0].setFloatValues(newStartValue, newEndValue);
836                mQsContainerAnimator.setCurrentPlayTime(mQsContainerAnimator.getCurrentPlayTime());
837            }
838        }
839    };
840
841    private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn
842            = new ViewTreeObserver.OnPreDrawListener() {
843        @Override
844        public boolean onPreDraw() {
845            getViewTreeObserver().removeOnPreDrawListener(this);
846            mHeader.setTranslationY(-mHeader.getCollapsedHeight() - mQsPeekHeight);
847            mHeader.animate()
848                    .translationY(0f)
849                    .setStartDelay(mStatusBar.calculateGoingToFullShadeDelay())
850                    .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE)
851                    .setInterpolator(mFastOutSlowInInterpolator)
852                    .start();
853            mQsContainer.setY(-mQsContainer.getHeight());
854            mQsContainerAnimator = ObjectAnimator.ofFloat(mQsContainer, View.TRANSLATION_Y,
855                    mQsContainer.getTranslationY(),
856                    mHeader.getCollapsedHeight() + mQsPeekHeight - mQsContainer.getHeight()
857                            - mQsContainer.getTop());
858            mQsContainerAnimator.setStartDelay(mStatusBar.calculateGoingToFullShadeDelay());
859            mQsContainerAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE);
860            mQsContainerAnimator.setInterpolator(mFastOutSlowInInterpolator);
861            mQsContainerAnimator.addListener(mAnimateHeaderSlidingInListener);
862            mQsContainerAnimator.start();
863            mQsContainer.addOnLayoutChangeListener(mQsContainerAnimatorUpdater);
864            return true;
865        }
866    };
867
868    private void animateHeaderSlidingIn() {
869        mHeaderAnimatingIn = true;
870        getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn);
871
872    }
873
874    private final Runnable mAnimateKeyguardStatusBarInvisibleEndRunnable = new Runnable() {
875        @Override
876        public void run() {
877            mKeyguardStatusBar.setVisibility(View.INVISIBLE);
878        }
879    };
880
881    private void animateKeyguardStatusBarOut() {
882        mKeyguardStatusBar.animate()
883                .alpha(0f)
884                .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
885                .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2)
886                .setInterpolator(PhoneStatusBar.ALPHA_OUT)
887                .withEndAction(mAnimateKeyguardStatusBarInvisibleEndRunnable)
888                .start();
889    }
890
891    private final Runnable mAnimateKeyguardBottomAreaInvisibleEndRunnable = new Runnable() {
892        @Override
893        public void run() {
894            mKeyguardBottomArea.setVisibility(View.GONE);
895        }
896    };
897
898    private void setKeyguardBottomAreaVisibility(int statusBarState,
899            boolean goingToFullShade) {
900        if (goingToFullShade) {
901            mKeyguardBottomArea.animate().cancel();
902            mKeyguardBottomArea.animate()
903                    .alpha(0f)
904                    .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
905                    .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2)
906                    .setInterpolator(PhoneStatusBar.ALPHA_OUT)
907                    .withEndAction(mAnimateKeyguardBottomAreaInvisibleEndRunnable)
908                    .start();
909        } else if (statusBarState == StatusBarState.KEYGUARD
910                || statusBarState == StatusBarState.SHADE_LOCKED) {
911            mKeyguardBottomArea.animate().cancel();
912            mKeyguardBottomArea.setVisibility(View.VISIBLE);
913            mKeyguardBottomArea.setAlpha(1f);
914        } else {
915            mKeyguardBottomArea.animate().cancel();
916            mKeyguardBottomArea.setVisibility(View.GONE);
917            mKeyguardBottomArea.setAlpha(1f);
918        }
919    }
920
921    private void setKeyguardStatusViewVisibility(int statusBarState, boolean keyguardFadingAway,
922            boolean goingToFullShade) {
923        if ((!keyguardFadingAway && mStatusBarState == StatusBarState.KEYGUARD
924                && statusBarState != StatusBarState.KEYGUARD) || goingToFullShade) {
925            mKeyguardStatusView.animate().cancel();
926            mKeyguardStatusViewAnimating = true;
927            mKeyguardStatusView.animate()
928                    .alpha(0f)
929                    .setStartDelay(0)
930                    .setDuration(160)
931                    .setInterpolator(PhoneStatusBar.ALPHA_OUT)
932                    .withEndAction(mAnimateKeyguardStatusViewInvisibleEndRunnable);
933            if (keyguardFadingAway) {
934                mKeyguardStatusView.animate()
935                        .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
936                        .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2)
937                        .start();
938            }
939        } else if (mStatusBarState == StatusBarState.SHADE_LOCKED
940                && statusBarState == StatusBarState.KEYGUARD) {
941            mKeyguardStatusView.animate().cancel();
942            mKeyguardStatusView.setVisibility(View.VISIBLE);
943            mKeyguardStatusViewAnimating = true;
944            mKeyguardStatusView.setAlpha(0f);
945            mKeyguardStatusView.animate()
946                    .alpha(1f)
947                    .setStartDelay(0)
948                    .setDuration(320)
949                    .setInterpolator(PhoneStatusBar.ALPHA_IN)
950                    .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable);
951        } else if (statusBarState == StatusBarState.KEYGUARD) {
952            mKeyguardStatusView.animate().cancel();
953            mKeyguardStatusView.setVisibility(View.VISIBLE);
954            mKeyguardStatusView.setAlpha(1f);
955        } else {
956            mKeyguardStatusView.animate().cancel();
957            mKeyguardStatusView.setVisibility(View.GONE);
958            mKeyguardStatusView.setAlpha(1f);
959        }
960    }
961
962    private void updateQsState() {
963        boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling;
964        mHeader.setVisibility((mQsExpanded || !mKeyguardShowing) ? View.VISIBLE : View.INVISIBLE);
965        mHeader.setExpanded(mKeyguardShowing || (mQsExpanded && !mStackScrollerOverscrolling));
966        mNotificationStackScroller.setScrollingEnabled(
967                mStatusBarState != StatusBarState.KEYGUARD && (!mQsExpanded
968                        || mQsExpansionFromOverscroll));
969        mQsPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE);
970        mQsContainer.setVisibility(
971                mKeyguardShowing && !expandVisually ? View.INVISIBLE : View.VISIBLE);
972        mScrollView.setTouchEnabled(mQsExpanded);
973        updateEmptyShadeView();
974        mQsNavbarScrim.setVisibility(mStatusBarState == StatusBarState.SHADE && mQsExpanded
975                && !mStackScrollerOverscrolling && mQsScrimEnabled
976                        ? View.VISIBLE
977                        : View.INVISIBLE);
978        if (mKeyguardUserSwitcher != null && mQsExpanded && !mStackScrollerOverscrolling) {
979            mKeyguardUserSwitcher.hide(true /* animate */);
980        }
981    }
982
983    private void setQsExpansion(float height) {
984        height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
985        mQsFullyExpanded = height == mQsMaxExpansionHeight;
986        if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) {
987            setQsExpanded(true);
988        } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
989            setQsExpanded(false);
990            if (mLastAnnouncementWasQuickSettings && !mTracking) {
991                announceForAccessibility(getKeyguardOrLockScreenString());
992                mLastAnnouncementWasQuickSettings = false;
993            }
994        }
995        mQsExpansionHeight = height;
996        mHeader.setExpansion(getHeaderExpansionFraction());
997        setQsTranslation(height);
998        requestScrollerTopPaddingUpdate(false /* animate */);
999        updateNotificationScrim(height);
1000        if (mKeyguardShowing) {
1001            updateHeaderKeyguard();
1002        }
1003        if (mStatusBarState == StatusBarState.SHADE && mQsExpanded
1004                && !mStackScrollerOverscrolling && mQsScrimEnabled) {
1005            mQsNavbarScrim.setAlpha(getQsExpansionFraction());
1006        }
1007
1008        // Upon initialisation when we are not layouted yet we don't want to announce that we are
1009        // fully expanded, hence the != 0.0f check.
1010        if (height != 0.0f && mQsFullyExpanded && !mLastAnnouncementWasQuickSettings) {
1011            announceForAccessibility(getContext().getString(
1012                    R.string.accessibility_desc_quick_settings));
1013            mLastAnnouncementWasQuickSettings = true;
1014        }
1015    }
1016
1017    private String getKeyguardOrLockScreenString() {
1018        if (mStatusBarState == StatusBarState.KEYGUARD) {
1019            return getContext().getString(R.string.accessibility_desc_lock_screen);
1020        } else {
1021            return getContext().getString(R.string.accessibility_desc_notification_shade);
1022        }
1023    }
1024
1025    private void updateNotificationScrim(float height) {
1026        int startDistance = mQsMinExpansionHeight + mNotificationScrimWaitDistance;
1027        float progress = (height - startDistance) / (mQsMaxExpansionHeight - startDistance);
1028        progress = Math.max(0.0f, Math.min(progress, 1.0f));
1029        mNotificationStackScroller.setScrimAlpha(progress);
1030    }
1031
1032    private float getHeaderExpansionFraction() {
1033        if (!mKeyguardShowing) {
1034            return getQsExpansionFraction();
1035        } else {
1036            return 1f;
1037        }
1038    }
1039
1040    private void setQsTranslation(float height) {
1041        if (!mHeaderAnimatingIn) {
1042            mQsContainer.setY(height - mQsContainer.getHeight() + getHeaderTranslation());
1043        }
1044        if (mKeyguardShowing) {
1045            mHeader.setY(interpolate(getQsExpansionFraction(), -mHeader.getHeight(), 0));
1046        }
1047    }
1048
1049    private float calculateQsTopPadding() {
1050        // We can only do the smoother transition on Keyguard when we also are not collapsing from a
1051        // scrolled quick settings.
1052        if (mKeyguardShowing && mScrollYOverride == -1) {
1053            return interpolate(getQsExpansionFraction(),
1054                    mNotificationStackScroller.getIntrinsicPadding() - mNotificationTopPadding,
1055                    mQsMaxExpansionHeight);
1056        } else {
1057            return mQsExpansionHeight;
1058        }
1059    }
1060
1061    private void requestScrollerTopPaddingUpdate(boolean animate) {
1062        mNotificationStackScroller.updateTopPadding(calculateQsTopPadding(),
1063                mScrollView.getScrollY(),
1064                mAnimateNextTopPaddingChange || animate);
1065        mAnimateNextTopPaddingChange = false;
1066    }
1067
1068    private void trackMovement(MotionEvent event) {
1069        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
1070        mLastTouchX = event.getX();
1071        mLastTouchY = event.getY();
1072    }
1073
1074    private void initVelocityTracker() {
1075        if (mVelocityTracker != null) {
1076            mVelocityTracker.recycle();
1077        }
1078        mVelocityTracker = VelocityTracker.obtain();
1079    }
1080
1081    private float getCurrentVelocity() {
1082        if (mVelocityTracker == null) {
1083            return 0;
1084        }
1085        mVelocityTracker.computeCurrentVelocity(1000);
1086        return mVelocityTracker.getYVelocity();
1087    }
1088
1089    private void cancelAnimation() {
1090        if (mQsExpansionAnimator != null) {
1091            mQsExpansionAnimator.cancel();
1092        }
1093    }
1094
1095    private void flingSettings(float vel, boolean expand) {
1096        flingSettings(vel, expand, null);
1097    }
1098
1099    private void flingSettings(float vel, boolean expand, final Runnable onFinishRunnable) {
1100        float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight;
1101        if (target == mQsExpansionHeight) {
1102            mScrollYOverride = -1;
1103            if (onFinishRunnable != null) {
1104                onFinishRunnable.run();
1105            }
1106            return;
1107        }
1108        mScrollView.setBlockFlinging(true);
1109        ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
1110        mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel);
1111        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1112            @Override
1113            public void onAnimationUpdate(ValueAnimator animation) {
1114                setQsExpansion((Float) animation.getAnimatedValue());
1115            }
1116        });
1117        animator.addListener(new AnimatorListenerAdapter() {
1118            @Override
1119            public void onAnimationEnd(Animator animation) {
1120                mScrollView.setBlockFlinging(false);
1121                mScrollYOverride = -1;
1122                mQsExpansionAnimator = null;
1123                if (onFinishRunnable != null) {
1124                    onFinishRunnable.run();
1125                }
1126            }
1127        });
1128        animator.start();
1129        mQsExpansionAnimator = animator;
1130        mQsAnimatorExpand = expand;
1131    }
1132
1133    /**
1134     * @return Whether we should intercept a gesture to open Quick Settings.
1135     */
1136    private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
1137        if (!mQsExpansionEnabled) {
1138            return false;
1139        }
1140        View header = mKeyguardShowing ? mKeyguardStatusBar : mHeader;
1141        boolean onHeader = x >= header.getLeft() && x <= header.getRight()
1142                && y >= header.getTop() && y <= header.getBottom();
1143        if (mQsExpanded) {
1144            return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0) && isInQsArea(x, y);
1145        } else {
1146            return onHeader;
1147        }
1148    }
1149
1150    @Override
1151    public void setVisibility(int visibility) {
1152        int oldVisibility = getVisibility();
1153        super.setVisibility(visibility);
1154        if (visibility != oldVisibility) {
1155            reparentStatusIcons(visibility == VISIBLE);
1156        }
1157    }
1158
1159    /**
1160     * When the notification panel gets expanded, we need to move the status icons in the header
1161     * card.
1162     */
1163    private void reparentStatusIcons(boolean toHeader) {
1164        if (mStatusBar == null) {
1165            return;
1166        }
1167        LinearLayout systemIcons = mStatusBar.getSystemIcons();
1168        ViewGroup parent = ((ViewGroup) systemIcons.getParent());
1169        if (toHeader) {
1170            int index = parent.indexOfChild(systemIcons);
1171            parent.removeView(systemIcons);
1172            mSystemIconsCopy.setMirroredView(
1173                    systemIcons, systemIcons.getWidth(), systemIcons.getHeight());
1174            parent.addView(mSystemIconsCopy, index);
1175            mHeader.attachSystemIcons(systemIcons);
1176        } else {
1177            ViewGroup newParent = mStatusBar.getSystemIconArea();
1178            int index = newParent.indexOfChild(mSystemIconsCopy);
1179            parent.removeView(systemIcons);
1180            mHeader.onSystemIconsDetached();
1181            mSystemIconsCopy.setMirroredView(null, 0, 0);
1182            newParent.removeView(mSystemIconsCopy);
1183            newParent.addView(systemIcons, index);
1184        }
1185    }
1186
1187    @Override
1188    protected boolean isScrolledToBottom() {
1189        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
1190            return true;
1191        }
1192        if (!isInSettings()) {
1193            return mNotificationStackScroller.isScrolledToBottom();
1194        } else {
1195            return mScrollView.isScrolledToBottom();
1196        }
1197    }
1198
1199    @Override
1200    protected int getMaxPanelHeight() {
1201        int min = mStatusBarMinHeight;
1202        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD
1203                && mNotificationStackScroller.getNotGoneChildCount() == 0) {
1204            int minHeight = (int) ((mQsMinExpansionHeight + getOverExpansionAmount())
1205                    * HEADER_RUBBERBAND_FACTOR);
1206            min = Math.max(min, minHeight);
1207        }
1208        int maxHeight;
1209        if (mTwoFingerQsExpand || mQsExpanded || mIsExpanding && mQsExpandedWhenExpandingStarted) {
1210            maxHeight = Math.max(calculatePanelHeightQsExpanded(), calculatePanelHeightShade());
1211        } else {
1212            maxHeight = calculatePanelHeightShade();
1213        }
1214        maxHeight = Math.max(maxHeight, min);
1215        return maxHeight;
1216    }
1217
1218    private boolean isInSettings() {
1219        return mQsExpanded;
1220    }
1221
1222    @Override
1223    protected void onHeightUpdated(float expandedHeight) {
1224        if (!mQsExpanded) {
1225            positionClockAndNotifications();
1226        }
1227        if (mTwoFingerQsExpand || mQsExpanded && !mQsTracking && mQsExpansionAnimator == null
1228                && !mQsExpansionFromOverscroll) {
1229            float panelHeightQsCollapsed = mNotificationStackScroller.getIntrinsicPadding()
1230                    + mNotificationStackScroller.getMinStackHeight()
1231                    + mNotificationStackScroller.getNotificationTopPadding();
1232            float panelHeightQsExpanded = calculatePanelHeightQsExpanded();
1233            float t = (expandedHeight - panelHeightQsCollapsed)
1234                    / (panelHeightQsExpanded - panelHeightQsCollapsed);
1235
1236            setQsExpansion(mQsMinExpansionHeight
1237                    + t * (getTempQsMaxExpansion() - mQsMinExpansionHeight));
1238        }
1239        mNotificationStackScroller.setStackHeight(expandedHeight);
1240        updateHeader();
1241        updateUnlockIcon();
1242        updateNotificationTranslucency();
1243    }
1244
1245    /**
1246     * @return a temporary override of {@link #mQsMaxExpansionHeight}, which is needed when
1247     *         collapsing QS / the panel when QS was scrolled
1248     */
1249    private int getTempQsMaxExpansion() {
1250        int qsTempMaxExpansion = mQsMaxExpansionHeight;
1251        if (mScrollYOverride != -1) {
1252            qsTempMaxExpansion -= mScrollYOverride;
1253        }
1254        return qsTempMaxExpansion;
1255    }
1256
1257    private int calculatePanelHeightShade() {
1258        int emptyBottomMargin = mNotificationStackScroller.getEmptyBottomMargin();
1259        int maxHeight = mNotificationStackScroller.getHeight() - emptyBottomMargin
1260                - mTopPaddingAdjustment;
1261        maxHeight += mNotificationStackScroller.getTopPaddingOverflow();
1262        return maxHeight;
1263    }
1264
1265    private int calculatePanelHeightQsExpanded() {
1266        float notificationHeight = mNotificationStackScroller.getHeight()
1267                - mNotificationStackScroller.getEmptyBottomMargin()
1268                - mNotificationStackScroller.getTopPadding();
1269        float totalHeight = mQsMaxExpansionHeight + notificationHeight
1270                + mNotificationStackScroller.getNotificationTopPadding();
1271        if (totalHeight > mNotificationStackScroller.getHeight()) {
1272            float fullyCollapsedHeight = mQsMaxExpansionHeight
1273                    + mNotificationStackScroller.getMinStackHeight()
1274                    + mNotificationStackScroller.getNotificationTopPadding()
1275                    - getScrollViewScrollY();
1276            totalHeight = Math.max(fullyCollapsedHeight, mNotificationStackScroller.getHeight());
1277        }
1278        return (int) totalHeight;
1279    }
1280
1281    private int getScrollViewScrollY() {
1282        if (mScrollYOverride != -1) {
1283            return mScrollYOverride;
1284        } else {
1285            return mScrollView.getScrollY();
1286        }
1287    }
1288    private void updateNotificationTranslucency() {
1289        float alpha = (getNotificationsTopY() + mNotificationStackScroller.getItemHeight())
1290                / (mQsMinExpansionHeight + mNotificationStackScroller.getBottomStackPeekSize()
1291                        + mNotificationStackScroller.getCollapseSecondCardPadding());
1292        alpha = Math.max(0, Math.min(alpha, 1));
1293        alpha = (float) Math.pow(alpha, 0.75);
1294        if (alpha != 1f && mNotificationStackScroller.getLayerType() != LAYER_TYPE_HARDWARE) {
1295            mNotificationStackScroller.setLayerType(LAYER_TYPE_HARDWARE, null);
1296        } else if (alpha == 1f
1297                && mNotificationStackScroller.getLayerType() == LAYER_TYPE_HARDWARE) {
1298            mNotificationStackScroller.setLayerType(LAYER_TYPE_NONE, null);
1299        }
1300        mNotificationStackScroller.setAlpha(alpha);
1301    }
1302
1303    @Override
1304    protected float getOverExpansionAmount() {
1305        return mNotificationStackScroller.getCurrentOverScrollAmount(true /* top */);
1306    }
1307
1308    @Override
1309    protected float getOverExpansionPixels() {
1310        return mNotificationStackScroller.getCurrentOverScrolledPixels(true /* top */);
1311    }
1312
1313    private void updateUnlockIcon() {
1314        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1315                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
1316            boolean active = getMaxPanelHeight() - getExpandedHeight() > mUnlockMoveDistance;
1317            KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon();
1318            if (active && !mUnlockIconActive && mTracking) {
1319                lockIcon.setImageAlpha(1.0f, true, 150, mFastOutLinearInterpolator, null);
1320                lockIcon.setImageScale(LOCK_ICON_ACTIVE_SCALE, true, 150,
1321                        mFastOutLinearInterpolator);
1322            } else if (!active && mUnlockIconActive && mTracking) {
1323                lockIcon.setImageAlpha(KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT, true,
1324                        150, mFastOutLinearInterpolator, null);
1325                lockIcon.setImageScale(1.0f, true, 150,
1326                        mFastOutLinearInterpolator);
1327            }
1328            mUnlockIconActive = active;
1329        }
1330    }
1331
1332    /**
1333     * Hides the header when notifications are colliding with it.
1334     */
1335    private void updateHeader() {
1336        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1337                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
1338            updateHeaderKeyguard();
1339        } else {
1340            updateHeaderShade();
1341        }
1342
1343    }
1344
1345    private void updateHeaderShade() {
1346        if (!mHeaderAnimatingIn) {
1347            mHeader.setTranslationY(getHeaderTranslation());
1348        }
1349        setQsTranslation(mQsExpansionHeight);
1350    }
1351
1352    private float getHeaderTranslation() {
1353        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1354                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
1355            return 0;
1356        }
1357        if (mNotificationStackScroller.getNotGoneChildCount() == 0) {
1358            if (mExpandedHeight / HEADER_RUBBERBAND_FACTOR >= mQsMinExpansionHeight) {
1359                return 0;
1360            } else {
1361                return mExpandedHeight / HEADER_RUBBERBAND_FACTOR - mQsMinExpansionHeight;
1362            }
1363        }
1364        return Math.min(0, mNotificationStackScroller.getTranslationY()) / HEADER_RUBBERBAND_FACTOR;
1365    }
1366
1367    private void updateHeaderKeyguard() {
1368        float alphaNotifications;
1369        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
1370
1371            // When on Keyguard, we hide the header as soon as the top card of the notification
1372            // stack scroller is close enough (collision distance) to the bottom of the header.
1373            alphaNotifications = getNotificationsTopY()
1374                    /
1375                    (mKeyguardStatusBar.getHeight() + mNotificationsHeaderCollideDistance);
1376        } else {
1377
1378            // In SHADE_LOCKED, the top card is already really close to the header. Hide it as
1379            // soon as we start translating the stack.
1380            alphaNotifications = getNotificationsTopY() / mKeyguardStatusBar.getHeight();
1381        }
1382        alphaNotifications = MathUtils.constrain(alphaNotifications, 0, 1);
1383        alphaNotifications = (float) Math.pow(alphaNotifications, 0.75);
1384        float alphaQsExpansion = 1 - Math.min(1, getQsExpansionFraction() * 2);
1385        mKeyguardStatusBar.setAlpha(Math.min(alphaNotifications, alphaQsExpansion));
1386        mKeyguardBottomArea.setAlpha(Math.min(1 - getQsExpansionFraction(), alphaNotifications));
1387        setQsTranslation(mQsExpansionHeight);
1388    }
1389
1390    private float getNotificationsTopY() {
1391        if (mNotificationStackScroller.getNotGoneChildCount() == 0) {
1392            return getExpandedHeight();
1393        }
1394        return mNotificationStackScroller.getNotificationsTopY();
1395    }
1396
1397    @Override
1398    protected void onExpandingStarted() {
1399        super.onExpandingStarted();
1400        mNotificationStackScroller.onExpansionStarted();
1401        mIsExpanding = true;
1402        mQsExpandedWhenExpandingStarted = mQsExpanded;
1403        if (mQsExpanded) {
1404            onQsExpansionStarted();
1405        }
1406    }
1407
1408    @Override
1409    protected void onExpandingFinished() {
1410        super.onExpandingFinished();
1411        mNotificationStackScroller.onExpansionStopped();
1412        mIsExpanding = false;
1413        mScrollYOverride = -1;
1414        if (mExpandedHeight == 0f) {
1415            setListening(false);
1416        } else {
1417            setListening(true);
1418        }
1419        mTwoFingerQsExpand = false;
1420        mTwoFingerQsExpandPossible = false;
1421    }
1422
1423    private void setListening(boolean listening) {
1424        mHeader.setListening(listening);
1425        mKeyguardStatusBar.setListening(listening);
1426        mQsPanel.setListening(listening);
1427    }
1428
1429    @Override
1430    public void instantExpand() {
1431        super.instantExpand();
1432        setListening(true);
1433    }
1434
1435    @Override
1436    protected void setOverExpansion(float overExpansion, boolean isPixels) {
1437        if (mConflictingQsExpansionGesture || mTwoFingerQsExpand) {
1438            return;
1439        }
1440        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) {
1441            mNotificationStackScroller.setOnHeightChangedListener(null);
1442            if (isPixels) {
1443                mNotificationStackScroller.setOverScrolledPixels(
1444                        overExpansion, true /* onTop */, false /* animate */);
1445            } else {
1446                mNotificationStackScroller.setOverScrollAmount(
1447                        overExpansion, true /* onTop */, false /* animate */);
1448            }
1449            mNotificationStackScroller.setOnHeightChangedListener(this);
1450        }
1451    }
1452
1453    @Override
1454    protected void onTrackingStarted() {
1455        super.onTrackingStarted();
1456        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1457                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
1458            mAfforanceHelper.animateHideLeftRightIcon();
1459        }
1460        if (mQsExpanded) {
1461            mTwoFingerQsExpand = true;
1462        }
1463    }
1464
1465    @Override
1466    protected void onTrackingStopped(boolean expand) {
1467        super.onTrackingStopped(expand);
1468        if (expand) {
1469            mNotificationStackScroller.setOverScrolledPixels(
1470                    0.0f, true /* onTop */, true /* animate */);
1471        }
1472        if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1473                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
1474            if (!mHintAnimationRunning) {
1475                mAfforanceHelper.reset(true);
1476            }
1477        }
1478        if (!expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1479                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
1480            KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon();
1481            lockIcon.setImageAlpha(0.0f, true, 100, mFastOutLinearInterpolator, null);
1482            lockIcon.setImageScale(2.0f, true, 100, mFastOutLinearInterpolator);
1483        }
1484    }
1485
1486    @Override
1487    public void onHeightChanged(ExpandableView view) {
1488
1489        // Block update if we are in quick settings and just the top padding changed
1490        // (i.e. view == null).
1491        if (view == null && mQsExpanded) {
1492            return;
1493        }
1494        requestPanelHeightUpdate();
1495    }
1496
1497    @Override
1498    public void onReset(ExpandableView view) {
1499    }
1500
1501    @Override
1502    public void onScrollChanged() {
1503        if (mQsExpanded) {
1504            requestScrollerTopPaddingUpdate(false /* animate */);
1505            requestPanelHeightUpdate();
1506        }
1507    }
1508
1509    @Override
1510    protected void onConfigurationChanged(Configuration newConfig) {
1511        super.onConfigurationChanged(newConfig);
1512        mAfforanceHelper.onConfigurationChanged();
1513    }
1514
1515    @Override
1516    public void onClick(View v) {
1517        if (v == mHeader) {
1518            onQsExpansionStarted();
1519            if (mQsExpanded) {
1520                flingSettings(0 /* vel */, false /* expand */);
1521            } else if (mQsExpansionEnabled) {
1522                flingSettings(0 /* vel */, true /* expand */);
1523            }
1524        }
1525    }
1526
1527    @Override
1528    public void onAnimationToSideStarted(boolean rightPage) {
1529        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage;
1530        mIsLaunchTransitionRunning = true;
1531        mLaunchAnimationEndRunnable = null;
1532        if (start) {
1533            mKeyguardBottomArea.launchPhone();
1534        } else {
1535            mSecureCameraLaunchManager.startSecureCameraLaunch();
1536        }
1537        mBlockTouches = true;
1538    }
1539
1540    @Override
1541    public void onAnimationToSideEnded() {
1542        mIsLaunchTransitionRunning = false;
1543        mIsLaunchTransitionFinished = true;
1544        if (mLaunchAnimationEndRunnable != null) {
1545            mLaunchAnimationEndRunnable.run();
1546            mLaunchAnimationEndRunnable = null;
1547        }
1548    }
1549
1550    @Override
1551    protected void onEdgeClicked(boolean right) {
1552        if ((right && getRightIcon().getVisibility() != View.VISIBLE)
1553                || (!right && getLeftIcon().getVisibility() != View.VISIBLE)) {
1554            return;
1555        }
1556        mHintAnimationRunning = true;
1557        mAfforanceHelper.startHintAnimation(right, new Runnable() {
1558            @Override
1559            public void run() {
1560                mHintAnimationRunning = false;
1561                mStatusBar.onHintFinished();
1562            }
1563        });
1564        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? right : !right;
1565        if (start) {
1566            mStatusBar.onPhoneHintStarted();
1567        } else {
1568            mStatusBar.onCameraHintStarted();
1569        }
1570    }
1571
1572    @Override
1573    protected void startUnlockHintAnimation() {
1574        super.startUnlockHintAnimation();
1575        startHighlightIconAnimation(getCenterIcon());
1576    }
1577
1578    /**
1579     * Starts the highlight (making it fully opaque) animation on an icon.
1580     */
1581    private void startHighlightIconAnimation(final KeyguardAffordanceView icon) {
1582        icon.setImageAlpha(1.0f, true, KeyguardAffordanceHelper.HINT_PHASE1_DURATION,
1583                mFastOutSlowInInterpolator, new Runnable() {
1584                    @Override
1585                    public void run() {
1586                        icon.setImageAlpha(KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT,
1587                                true, KeyguardAffordanceHelper.HINT_PHASE1_DURATION,
1588                                mFastOutSlowInInterpolator, null);
1589                    }
1590                });
1591    }
1592
1593    @Override
1594    public float getPageWidth() {
1595        return getWidth();
1596    }
1597
1598    @Override
1599    public void onSwipingStarted() {
1600        mSecureCameraLaunchManager.onSwipingStarted();
1601        requestDisallowInterceptTouchEvent(true);
1602        mOnlyAffordanceInThisMotion = true;
1603    }
1604
1605    @Override
1606    public KeyguardAffordanceView getLeftIcon() {
1607        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1608                ? mKeyguardBottomArea.getCameraView()
1609                : mKeyguardBottomArea.getPhoneView();
1610    }
1611
1612    @Override
1613    public KeyguardAffordanceView getCenterIcon() {
1614        return mKeyguardBottomArea.getLockIcon();
1615    }
1616
1617    @Override
1618    public KeyguardAffordanceView getRightIcon() {
1619        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1620                ? mKeyguardBottomArea.getPhoneView()
1621                : mKeyguardBottomArea.getCameraView();
1622    }
1623
1624    @Override
1625    public View getLeftPreview() {
1626        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1627                ? mKeyguardBottomArea.getCameraPreview()
1628                : mKeyguardBottomArea.getPhonePreview();
1629    }
1630
1631    @Override
1632    public View getRightPreview() {
1633        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1634                ? mKeyguardBottomArea.getPhonePreview()
1635                : mKeyguardBottomArea.getCameraPreview();
1636    }
1637
1638    @Override
1639    protected float getPeekHeight() {
1640        if (mNotificationStackScroller.getNotGoneChildCount() > 0) {
1641            return mNotificationStackScroller.getPeekHeight();
1642        } else {
1643            return mQsMinExpansionHeight * HEADER_RUBBERBAND_FACTOR;
1644        }
1645    }
1646
1647    @Override
1648    protected float getCannedFlingDurationFactor() {
1649        if (mQsExpanded) {
1650            return 0.7f;
1651        } else {
1652            return 0.6f;
1653        }
1654    }
1655
1656    @Override
1657    protected boolean fullyExpandedClearAllVisible() {
1658        return mNotificationStackScroller.isDismissViewNotGone()
1659                && mNotificationStackScroller.isScrolledToBottom();
1660    }
1661
1662    @Override
1663    protected boolean isClearAllVisible() {
1664        return mNotificationStackScroller.isDismissViewVisible();
1665    }
1666
1667    @Override
1668    protected int getClearAllHeight() {
1669        return mNotificationStackScroller.getDismissViewHeight();
1670    }
1671
1672    @Override
1673    protected boolean isTrackingBlocked() {
1674        return mConflictingQsExpansionGesture && mQsExpanded;
1675    }
1676
1677    public void notifyVisibleChildrenChanged() {
1678        if (mNotificationStackScroller.getNotGoneChildCount() != 0) {
1679            mReserveNotificationSpace.setVisibility(View.VISIBLE);
1680        } else {
1681            mReserveNotificationSpace.setVisibility(View.GONE);
1682        }
1683    }
1684
1685    public boolean isQsExpanded() {
1686        return mQsExpanded;
1687    }
1688
1689    public boolean isQsDetailShowing() {
1690        return mQsPanel.isShowingDetail();
1691    }
1692
1693    public void closeQsDetail() {
1694        mQsPanel.closeDetail();
1695    }
1696
1697    @Override
1698    public boolean shouldDelayChildPressedState() {
1699        return true;
1700    }
1701
1702    public boolean isLaunchTransitionFinished() {
1703        return mIsLaunchTransitionFinished;
1704    }
1705
1706    public boolean isLaunchTransitionRunning() {
1707        return mIsLaunchTransitionRunning;
1708    }
1709
1710    public void setLaunchTransitionEndRunnable(Runnable r) {
1711        mLaunchAnimationEndRunnable = r;
1712    }
1713
1714    public void setEmptyDragAmount(float amount) {
1715        float factor = 1f;
1716        if (mNotificationStackScroller.getNotGoneChildCount() > 0) {
1717            factor = 0.6f;
1718        } else if (!mStatusBar.hasActiveNotifications()) {
1719            factor = 0.4f;
1720        }
1721        mEmptyDragAmount = amount * factor;
1722        positionClockAndNotifications();
1723    }
1724
1725    private static float interpolate(float t, float start, float end) {
1726        return (1 - t) * start + t * end;
1727    }
1728
1729    private void updateKeyguardStatusBarVisibility() {
1730        mKeyguardStatusBar.setVisibility(mKeyguardShowing && !mDozing ? VISIBLE : INVISIBLE);
1731    }
1732
1733    public void setDozing(boolean dozing) {
1734        if (dozing == mDozing) return;
1735        mDozing = dozing;
1736        if (mDozing) {
1737            setBackgroundColor(0xff000000);
1738        } else {
1739            setBackground(null);
1740        }
1741        updateKeyguardStatusBarVisibility();
1742    }
1743
1744    public void setShadeEmpty(boolean shadeEmpty) {
1745        mShadeEmpty = shadeEmpty;
1746        updateEmptyShadeView();
1747    }
1748
1749    private void updateEmptyShadeView() {
1750
1751        // Hide "No notifications" in QS.
1752        mNotificationStackScroller.updateEmptyShadeView(mShadeEmpty && !mQsExpanded);
1753    }
1754
1755    public void setQsScrimEnabled(boolean qsScrimEnabled) {
1756        boolean changed = mQsScrimEnabled != qsScrimEnabled;
1757        mQsScrimEnabled = qsScrimEnabled;
1758        if (changed) {
1759            updateQsState();
1760        }
1761    }
1762
1763    public void setKeyguardUserSwitcher(KeyguardUserSwitcher keyguardUserSwitcher) {
1764        mKeyguardUserSwitcher = keyguardUserSwitcher;
1765    }
1766
1767    private final Runnable mUpdateHeader = new Runnable() {
1768        @Override
1769        public void run() {
1770            mHeader.updateEverything();
1771        }
1772    };
1773}
1774