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