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 
17 package com.android.systemui.statusbar.phone;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ObjectAnimator;
22 import android.animation.ValueAnimator;
23 import android.content.Context;
24 import android.content.res.Configuration;
25 import android.content.res.Resources;
26 import android.os.SystemClock;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 import android.view.InputDevice;
30 import android.view.MotionEvent;
31 import android.view.ViewConfiguration;
32 import android.view.ViewTreeObserver;
33 import android.view.animation.Interpolator;
34 import android.widget.FrameLayout;
35 
36 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
37 import com.android.keyguard.LatencyTracker;
38 import com.android.systemui.DejankUtils;
39 import com.android.systemui.Interpolators;
40 import com.android.systemui.R;
41 import com.android.systemui.classifier.FalsingManager;
42 import com.android.systemui.doze.DozeLog;
43 import com.android.systemui.statusbar.FlingAnimationUtils;
44 import com.android.systemui.statusbar.StatusBarState;
45 import com.android.systemui.statusbar.policy.HeadsUpManager;
46 
47 import java.io.FileDescriptor;
48 import java.io.PrintWriter;
49 
50 public abstract class PanelView extends FrameLayout {
51     public static final boolean DEBUG = PanelBar.DEBUG;
52     public static final String TAG = PanelView.class.getSimpleName();
53     private static final int INITIAL_OPENING_PEEK_DURATION = 200;
54     private static final int PEEK_ANIMATION_DURATION = 360;
55     private long mDownTime;
56     private float mMinExpandHeight;
57     private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
58     private boolean mPanelUpdateWhenAnimatorEnds;
59 
logf(String fmt, Object... args)60     private final void logf(String fmt, Object... args) {
61         Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args));
62     }
63 
64     protected StatusBar mStatusBar;
65     protected HeadsUpManager mHeadsUpManager;
66 
67     private float mPeekHeight;
68     private float mHintDistance;
69     private float mInitialOffsetOnTouch;
70     private boolean mCollapsedAndHeadsUpOnDown;
71     private float mExpandedFraction = 0;
72     protected float mExpandedHeight = 0;
73     private boolean mPanelClosedOnDown;
74     private boolean mHasLayoutedSinceDown;
75     private float mUpdateFlingVelocity;
76     private boolean mUpdateFlingOnLayout;
77     private boolean mPeekTouching;
78     private boolean mJustPeeked;
79     private boolean mClosing;
80     protected boolean mTracking;
81     private boolean mTouchSlopExceeded;
82     private int mTrackingPointer;
83     protected int mTouchSlop;
84     protected boolean mHintAnimationRunning;
85     private boolean mOverExpandedBeforeFling;
86     private boolean mTouchAboveFalsingThreshold;
87     private int mUnlockFalsingThreshold;
88     private boolean mTouchStartedInEmptyArea;
89     private boolean mMotionAborted;
90     private boolean mUpwardsWhenTresholdReached;
91     private boolean mAnimatingOnDown;
92 
93     private ValueAnimator mHeightAnimator;
94     private ObjectAnimator mPeekAnimator;
95     private VelocityTrackerInterface mVelocityTracker;
96     private FlingAnimationUtils mFlingAnimationUtils;
97     private FlingAnimationUtils mFlingAnimationUtilsClosing;
98     private FlingAnimationUtils mFlingAnimationUtilsDismissing;
99     private FalsingManager mFalsingManager;
100 
101     /**
102      * Whether an instant expand request is currently pending and we are just waiting for layout.
103      */
104     private boolean mInstantExpanding;
105     private boolean mAnimateAfterExpanding;
106 
107     PanelBar mBar;
108 
109     private String mViewName;
110     private float mInitialTouchY;
111     private float mInitialTouchX;
112     private boolean mTouchDisabled;
113 
114     /**
115      * Whether or not the PanelView can be expanded or collapsed with a drag.
116      */
117     private boolean mNotificationsDragEnabled;
118 
119     private Interpolator mBounceInterpolator;
120     protected KeyguardBottomAreaView mKeyguardBottomArea;
121 
122     /**
123      * Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time.
124      */
125     private float mNextCollapseSpeedUpFactor = 1.0f;
126 
127     protected boolean mExpanding;
128     private boolean mGestureWaitForTouchSlop;
129     private boolean mIgnoreXTouchSlop;
130     private boolean mExpandLatencyTracking;
131 
onExpandingFinished()132     protected void onExpandingFinished() {
133         mBar.onExpandingFinished();
134     }
135 
onExpandingStarted()136     protected void onExpandingStarted() {
137     }
138 
notifyExpandingStarted()139     private void notifyExpandingStarted() {
140         if (!mExpanding) {
141             mExpanding = true;
142             onExpandingStarted();
143         }
144     }
145 
notifyExpandingFinished()146     protected final void notifyExpandingFinished() {
147         endClosing();
148         if (mExpanding) {
149             mExpanding = false;
150             onExpandingFinished();
151         }
152     }
153 
runPeekAnimation(long duration, float peekHeight, boolean collapseWhenFinished)154     private void runPeekAnimation(long duration, float peekHeight, boolean collapseWhenFinished) {
155         mPeekHeight = peekHeight;
156         if (DEBUG) logf("peek to height=%.1f", mPeekHeight);
157         if (mHeightAnimator != null) {
158             return;
159         }
160         if (mPeekAnimator != null) {
161             mPeekAnimator.cancel();
162         }
163         mPeekAnimator = ObjectAnimator.ofFloat(this, "expandedHeight", mPeekHeight)
164                 .setDuration(duration);
165         mPeekAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
166         mPeekAnimator.addListener(new AnimatorListenerAdapter() {
167             private boolean mCancelled;
168 
169             @Override
170             public void onAnimationCancel(Animator animation) {
171                 mCancelled = true;
172             }
173 
174             @Override
175             public void onAnimationEnd(Animator animation) {
176                 mPeekAnimator = null;
177                 if (!mCancelled && collapseWhenFinished) {
178                     postOnAnimation(mPostCollapseRunnable);
179                 }
180 
181             }
182         });
183         notifyExpandingStarted();
184         mPeekAnimator.start();
185         mJustPeeked = true;
186     }
187 
PanelView(Context context, AttributeSet attrs)188     public PanelView(Context context, AttributeSet attrs) {
189         super(context, attrs);
190         mFlingAnimationUtils = new FlingAnimationUtils(context, 0.6f /* maxLengthSeconds */,
191                 0.6f /* speedUpFactor */);
192         mFlingAnimationUtilsClosing = new FlingAnimationUtils(context, 0.5f /* maxLengthSeconds */,
193                 0.6f /* speedUpFactor */);
194         mFlingAnimationUtilsDismissing = new FlingAnimationUtils(context,
195                 0.5f /* maxLengthSeconds */, 0.2f /* speedUpFactor */, 0.6f /* x2 */,
196                 0.84f /* y2 */);
197         mBounceInterpolator = new BounceInterpolator();
198         mFalsingManager = FalsingManager.getInstance(context);
199         mNotificationsDragEnabled =
200                 getResources().getBoolean(R.bool.config_enableNotificationShadeDrag);
201     }
202 
loadDimens()203     protected void loadDimens() {
204         final Resources res = getContext().getResources();
205         final ViewConfiguration configuration = ViewConfiguration.get(getContext());
206         mTouchSlop = configuration.getScaledTouchSlop();
207         mHintDistance = res.getDimension(R.dimen.hint_move_distance);
208         mUnlockFalsingThreshold = res.getDimensionPixelSize(R.dimen.unlock_falsing_threshold);
209     }
210 
trackMovement(MotionEvent event)211     private void trackMovement(MotionEvent event) {
212         // Add movement to velocity tracker using raw screen X and Y coordinates instead
213         // of window coordinates because the window frame may be moving at the same time.
214         float deltaX = event.getRawX() - event.getX();
215         float deltaY = event.getRawY() - event.getY();
216         event.offsetLocation(deltaX, deltaY);
217         if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
218         event.offsetLocation(-deltaX, -deltaY);
219     }
220 
setTouchDisabled(boolean disabled)221     public void setTouchDisabled(boolean disabled) {
222         mTouchDisabled = disabled;
223         if (mTouchDisabled) {
224             cancelHeightAnimator();
225             if (mTracking) {
226                 onTrackingStopped(true /* expanded */);
227             }
228             notifyExpandingFinished();
229         }
230     }
231 
startExpandLatencyTracking()232     public void startExpandLatencyTracking() {
233         if (LatencyTracker.isEnabled(mContext)) {
234             LatencyTracker.getInstance(mContext).onActionStart(
235                     LatencyTracker.ACTION_EXPAND_PANEL);
236             mExpandLatencyTracking = true;
237         }
238     }
239 
240     @Override
onTouchEvent(MotionEvent event)241     public boolean onTouchEvent(MotionEvent event) {
242         if (mInstantExpanding || mTouchDisabled
243                 || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
244             return false;
245         }
246 
247         // If dragging should not expand the notifications shade, then return false.
248         if (!mNotificationsDragEnabled) {
249             if (mTracking) {
250                 // Turn off tracking if it's on or the shade can get stuck in the down position.
251                 onTrackingStopped(true /* expand */);
252             }
253             return false;
254         }
255 
256         // On expanding, single mouse click expands the panel instead of dragging.
257         if (isFullyCollapsed() && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
258             if (event.getAction() == MotionEvent.ACTION_UP) {
259                 expand(true);
260             }
261             return true;
262         }
263 
264         /*
265          * We capture touch events here and update the expand height here in case according to
266          * the users fingers. This also handles multi-touch.
267          *
268          * If the user just clicks shortly, we show a quick peek of the shade.
269          *
270          * Flinging is also enabled in order to open or close the shade.
271          */
272 
273         int pointerIndex = event.findPointerIndex(mTrackingPointer);
274         if (pointerIndex < 0) {
275             pointerIndex = 0;
276             mTrackingPointer = event.getPointerId(pointerIndex);
277         }
278         final float x = event.getX(pointerIndex);
279         final float y = event.getY(pointerIndex);
280 
281         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
282             mGestureWaitForTouchSlop = isFullyCollapsed() || hasConflictingGestures();
283             mIgnoreXTouchSlop = isFullyCollapsed() || shouldGestureIgnoreXTouchSlop(x, y);
284         }
285 
286         switch (event.getActionMasked()) {
287             case MotionEvent.ACTION_DOWN:
288                 startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
289                 mJustPeeked = false;
290                 mMinExpandHeight = 0.0f;
291                 mPanelClosedOnDown = isFullyCollapsed();
292                 mHasLayoutedSinceDown = false;
293                 mUpdateFlingOnLayout = false;
294                 mMotionAborted = false;
295                 mPeekTouching = mPanelClosedOnDown;
296                 mDownTime = SystemClock.uptimeMillis();
297                 mTouchAboveFalsingThreshold = false;
298                 mCollapsedAndHeadsUpOnDown = isFullyCollapsed()
299                         && mHeadsUpManager.hasPinnedHeadsUp();
300                 if (mVelocityTracker == null) {
301                     initVelocityTracker();
302                 }
303                 trackMovement(event);
304                 if (!mGestureWaitForTouchSlop || (mHeightAnimator != null && !mHintAnimationRunning)
305                         || mPeekAnimator != null) {
306                     mTouchSlopExceeded = (mHeightAnimator != null && !mHintAnimationRunning)
307                             || mPeekAnimator != null;
308                     cancelHeightAnimator();
309                     cancelPeek();
310                     onTrackingStarted();
311                 }
312                 if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp()) {
313                     startOpening();
314                 }
315                 break;
316 
317             case MotionEvent.ACTION_POINTER_UP:
318                 final int upPointer = event.getPointerId(event.getActionIndex());
319                 if (mTrackingPointer == upPointer) {
320                     // gesture is ongoing, find a new pointer to track
321                     final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
322                     final float newY = event.getY(newIndex);
323                     final float newX = event.getX(newIndex);
324                     mTrackingPointer = event.getPointerId(newIndex);
325                     startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight);
326                 }
327                 break;
328             case MotionEvent.ACTION_POINTER_DOWN:
329                 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
330                     mMotionAborted = true;
331                     endMotionEvent(event, x, y, true /* forceCancel */);
332                     return false;
333                 }
334                 break;
335             case MotionEvent.ACTION_MOVE:
336                 trackMovement(event);
337                 float h = y - mInitialTouchY;
338 
339                 // If the panel was collapsed when touching, we only need to check for the
340                 // y-component of the gesture, as we have no conflicting horizontal gesture.
341                 if (Math.abs(h) > mTouchSlop
342                         && (Math.abs(h) > Math.abs(x - mInitialTouchX)
343                         || mIgnoreXTouchSlop)) {
344                     mTouchSlopExceeded = true;
345                     if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) {
346                         if (!mJustPeeked && mInitialOffsetOnTouch != 0f) {
347                             startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
348                             h = 0;
349                         }
350                         cancelHeightAnimator();
351                         onTrackingStarted();
352                     }
353                 }
354                 float newHeight = Math.max(0, h + mInitialOffsetOnTouch);
355                 if (newHeight > mPeekHeight) {
356                     if (mPeekAnimator != null) {
357                         mPeekAnimator.cancel();
358                     }
359                     mJustPeeked = false;
360                 } else if (mPeekAnimator == null && mJustPeeked) {
361                     // The initial peek has finished, but we haven't dragged as far yet, lets
362                     // speed it up by starting at the peek height.
363                     mInitialOffsetOnTouch = mExpandedHeight;
364                     mInitialTouchY = y;
365                     mMinExpandHeight = mExpandedHeight;
366                     mJustPeeked = false;
367                 }
368                 newHeight = Math.max(newHeight, mMinExpandHeight);
369                 if (-h >= getFalsingThreshold()) {
370                     mTouchAboveFalsingThreshold = true;
371                     mUpwardsWhenTresholdReached = isDirectionUpwards(x, y);
372                 }
373                 if (!mJustPeeked && (!mGestureWaitForTouchSlop || mTracking) &&
374                         !isTrackingBlocked()) {
375                     setExpandedHeightInternal(newHeight);
376                 }
377                 break;
378 
379             case MotionEvent.ACTION_UP:
380             case MotionEvent.ACTION_CANCEL:
381                 trackMovement(event);
382                 endMotionEvent(event, x, y, false /* forceCancel */);
383                 break;
384         }
385         return !mGestureWaitForTouchSlop || mTracking;
386     }
387 
startOpening()388     private void startOpening() {;
389         runPeekAnimation(INITIAL_OPENING_PEEK_DURATION, getOpeningHeight(),
390                 false /* collapseWhenFinished */);
391         notifyBarPanelExpansionChanged();
392     }
393 
getOpeningHeight()394     protected abstract float getOpeningHeight();
395 
396     /**
397      * @return whether the swiping direction is upwards and above a 45 degree angle compared to the
398      * horizontal direction
399      */
isDirectionUpwards(float x, float y)400     private boolean isDirectionUpwards(float x, float y) {
401         float xDiff = x - mInitialTouchX;
402         float yDiff = y - mInitialTouchY;
403         if (yDiff >= 0) {
404             return false;
405         }
406         return Math.abs(yDiff) >= Math.abs(xDiff);
407     }
408 
startExpandingFromPeek()409     protected void startExpandingFromPeek() {
410         mStatusBar.handlePeekToExpandTransistion();
411     }
412 
startExpandMotion(float newX, float newY, boolean startTracking, float expandedHeight)413     protected void startExpandMotion(float newX, float newY, boolean startTracking,
414             float expandedHeight) {
415         mInitialOffsetOnTouch = expandedHeight;
416         mInitialTouchY = newY;
417         mInitialTouchX = newX;
418         if (startTracking) {
419             mTouchSlopExceeded = true;
420             setExpandedHeight(mInitialOffsetOnTouch);
421             onTrackingStarted();
422         }
423     }
424 
endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel)425     private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) {
426         mTrackingPointer = -1;
427         if ((mTracking && mTouchSlopExceeded)
428                 || Math.abs(x - mInitialTouchX) > mTouchSlop
429                 || Math.abs(y - mInitialTouchY) > mTouchSlop
430                 || event.getActionMasked() == MotionEvent.ACTION_CANCEL
431                 || forceCancel) {
432             float vel = 0f;
433             float vectorVel = 0f;
434             if (mVelocityTracker != null) {
435                 mVelocityTracker.computeCurrentVelocity(1000);
436                 vel = mVelocityTracker.getYVelocity();
437                 vectorVel = (float) Math.hypot(
438                         mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
439             }
440             boolean expand = flingExpands(vel, vectorVel, x, y)
441                     || event.getActionMasked() == MotionEvent.ACTION_CANCEL
442                     || forceCancel;
443             DozeLog.traceFling(expand, mTouchAboveFalsingThreshold,
444                     mStatusBar.isFalsingThresholdNeeded(),
445                     mStatusBar.isWakeUpComingFromTouch());
446                     // Log collapse gesture if on lock screen.
447                     if (!expand && mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
448                         float displayDensity = mStatusBar.getDisplayDensity();
449                         int heightDp = (int) Math.abs((y - mInitialTouchY) / displayDensity);
450                         int velocityDp = (int) Math.abs(vel / displayDensity);
451                         mLockscreenGestureLogger.write(
452                                 MetricsEvent.ACTION_LS_UNLOCK,
453                                 heightDp, velocityDp);
454                     }
455             fling(vel, expand, isFalseTouch(x, y));
456             onTrackingStopped(expand);
457             mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown;
458             if (mUpdateFlingOnLayout) {
459                 mUpdateFlingVelocity = vel;
460             }
461         } else if (mPanelClosedOnDown && !mHeadsUpManager.hasPinnedHeadsUp() && !mTracking) {
462             long timePassed = SystemClock.uptimeMillis() - mDownTime;
463             if (timePassed < ViewConfiguration.getLongPressTimeout()) {
464                 // Lets show the user that he can actually expand the panel
465                 runPeekAnimation(PEEK_ANIMATION_DURATION, getPeekHeight(), true /* collapseWhenFinished */);
466             } else {
467                 // We need to collapse the panel since we peeked to the small height.
468                 postOnAnimation(mPostCollapseRunnable);
469             }
470         } else {
471             boolean expands = onEmptySpaceClick(mInitialTouchX);
472             onTrackingStopped(expands);
473         }
474 
475         if (mVelocityTracker != null) {
476             mVelocityTracker.recycle();
477             mVelocityTracker = null;
478         }
479         mPeekTouching = false;
480     }
481 
getCurrentExpandVelocity()482     protected float getCurrentExpandVelocity() {
483         if (mVelocityTracker == null) {
484             return 0;
485         }
486         mVelocityTracker.computeCurrentVelocity(1000);
487         return mVelocityTracker.getYVelocity();
488     }
489 
getFalsingThreshold()490     private int getFalsingThreshold() {
491         float factor = mStatusBar.isWakeUpComingFromTouch() ? 1.5f : 1.0f;
492         return (int) (mUnlockFalsingThreshold * factor);
493     }
494 
hasConflictingGestures()495     protected abstract boolean hasConflictingGestures();
496 
shouldGestureIgnoreXTouchSlop(float x, float y)497     protected abstract boolean shouldGestureIgnoreXTouchSlop(float x, float y);
498 
onTrackingStopped(boolean expand)499     protected void onTrackingStopped(boolean expand) {
500         mTracking = false;
501         mBar.onTrackingStopped(expand);
502         notifyBarPanelExpansionChanged();
503     }
504 
onTrackingStarted()505     protected void onTrackingStarted() {
506         endClosing();
507         mTracking = true;
508         mBar.onTrackingStarted();
509         notifyExpandingStarted();
510         notifyBarPanelExpansionChanged();
511     }
512 
513     @Override
onInterceptTouchEvent(MotionEvent event)514     public boolean onInterceptTouchEvent(MotionEvent event) {
515         if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled
516                 || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
517             return false;
518         }
519 
520         /*
521          * If the user drags anywhere inside the panel we intercept it if the movement is
522          * upwards. This allows closing the shade from anywhere inside the panel.
523          *
524          * We only do this if the current content is scrolled to the bottom,
525          * i.e isScrolledToBottom() is true and therefore there is no conflicting scrolling gesture
526          * possible.
527          */
528         int pointerIndex = event.findPointerIndex(mTrackingPointer);
529         if (pointerIndex < 0) {
530             pointerIndex = 0;
531             mTrackingPointer = event.getPointerId(pointerIndex);
532         }
533         final float x = event.getX(pointerIndex);
534         final float y = event.getY(pointerIndex);
535         boolean scrolledToBottom = isScrolledToBottom();
536 
537         switch (event.getActionMasked()) {
538             case MotionEvent.ACTION_DOWN:
539                 mStatusBar.userActivity();
540                 mAnimatingOnDown = mHeightAnimator != null;
541                 mMinExpandHeight = 0.0f;
542                 mDownTime = SystemClock.uptimeMillis();
543                 if (mAnimatingOnDown && mClosing && !mHintAnimationRunning
544                         || mPeekAnimator != null) {
545                     cancelHeightAnimator();
546                     cancelPeek();
547                     mTouchSlopExceeded = true;
548                     return true;
549                 }
550                 mInitialTouchY = y;
551                 mInitialTouchX = x;
552                 mTouchStartedInEmptyArea = !isInContentBounds(x, y);
553                 mTouchSlopExceeded = false;
554                 mJustPeeked = false;
555                 mMotionAborted = false;
556                 mPanelClosedOnDown = isFullyCollapsed();
557                 mCollapsedAndHeadsUpOnDown = false;
558                 mHasLayoutedSinceDown = false;
559                 mUpdateFlingOnLayout = false;
560                 mTouchAboveFalsingThreshold = false;
561                 initVelocityTracker();
562                 trackMovement(event);
563                 break;
564             case MotionEvent.ACTION_POINTER_UP:
565                 final int upPointer = event.getPointerId(event.getActionIndex());
566                 if (mTrackingPointer == upPointer) {
567                     // gesture is ongoing, find a new pointer to track
568                     final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
569                     mTrackingPointer = event.getPointerId(newIndex);
570                     mInitialTouchX = event.getX(newIndex);
571                     mInitialTouchY = event.getY(newIndex);
572                 }
573                 break;
574             case MotionEvent.ACTION_POINTER_DOWN:
575                 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
576                     mMotionAborted = true;
577                     if (mVelocityTracker != null) {
578                         mVelocityTracker.recycle();
579                         mVelocityTracker = null;
580                     }
581                 }
582                 break;
583             case MotionEvent.ACTION_MOVE:
584                 final float h = y - mInitialTouchY;
585                 trackMovement(event);
586                 if (scrolledToBottom || mTouchStartedInEmptyArea || mAnimatingOnDown) {
587                     float hAbs = Math.abs(h);
588                     if ((h < -mTouchSlop || (mAnimatingOnDown && hAbs > mTouchSlop))
589                             && hAbs > Math.abs(x - mInitialTouchX)) {
590                         cancelHeightAnimator();
591                         startExpandMotion(x, y, true /* startTracking */, mExpandedHeight);
592                         return true;
593                     }
594                 }
595                 break;
596             case MotionEvent.ACTION_CANCEL:
597             case MotionEvent.ACTION_UP:
598                 if (mVelocityTracker != null) {
599                     mVelocityTracker.recycle();
600                     mVelocityTracker = null;
601                 }
602                 break;
603         }
604         return false;
605     }
606 
607     /**
608      * @return Whether a pair of coordinates are inside the visible view content bounds.
609      */
isInContentBounds(float x, float y)610     protected abstract boolean isInContentBounds(float x, float y);
611 
cancelHeightAnimator()612     protected void cancelHeightAnimator() {
613         if (mHeightAnimator != null) {
614             if (mHeightAnimator.isRunning()) {
615                 mPanelUpdateWhenAnimatorEnds = false;
616             }
617             mHeightAnimator.cancel();
618         }
619         endClosing();
620     }
621 
endClosing()622     private void endClosing() {
623         if (mClosing) {
624             mClosing = false;
625             onClosingFinished();
626         }
627     }
628 
initVelocityTracker()629     private void initVelocityTracker() {
630         if (mVelocityTracker != null) {
631             mVelocityTracker.recycle();
632         }
633         mVelocityTracker = VelocityTrackerFactory.obtain(getContext());
634     }
635 
isScrolledToBottom()636     protected boolean isScrolledToBottom() {
637         return true;
638     }
639 
getContentHeight()640     protected float getContentHeight() {
641         return mExpandedHeight;
642     }
643 
644     @Override
onFinishInflate()645     protected void onFinishInflate() {
646         super.onFinishInflate();
647         loadDimens();
648     }
649 
650     @Override
onConfigurationChanged(Configuration newConfig)651     protected void onConfigurationChanged(Configuration newConfig) {
652         super.onConfigurationChanged(newConfig);
653         loadDimens();
654     }
655 
656     /**
657      * @param vel the current vertical velocity of the motion
658      * @param vectorVel the length of the vectorial velocity
659      * @return whether a fling should expands the panel; contracts otherwise
660      */
flingExpands(float vel, float vectorVel, float x, float y)661     protected boolean flingExpands(float vel, float vectorVel, float x, float y) {
662         if (isFalseTouch(x, y)) {
663             return true;
664         }
665         if (Math.abs(vectorVel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
666             return getExpandedFraction() > 0.5f;
667         } else {
668             return vel > 0;
669         }
670     }
671 
672     /**
673      * @param x the final x-coordinate when the finger was lifted
674      * @param y the final y-coordinate when the finger was lifted
675      * @return whether this motion should be regarded as a false touch
676      */
isFalseTouch(float x, float y)677     private boolean isFalseTouch(float x, float y) {
678         if (!mStatusBar.isFalsingThresholdNeeded()) {
679             return false;
680         }
681         if (mFalsingManager.isClassiferEnabled()) {
682             return mFalsingManager.isFalseTouch();
683         }
684         if (!mTouchAboveFalsingThreshold) {
685             return true;
686         }
687         if (mUpwardsWhenTresholdReached) {
688             return false;
689         }
690         return !isDirectionUpwards(x, y);
691     }
692 
fling(float vel, boolean expand)693     protected void fling(float vel, boolean expand) {
694         fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false);
695     }
696 
fling(float vel, boolean expand, boolean expandBecauseOfFalsing)697     protected void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) {
698         fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing);
699     }
700 
fling(float vel, boolean expand, float collapseSpeedUpFactor, boolean expandBecauseOfFalsing)701     protected void fling(float vel, boolean expand, float collapseSpeedUpFactor,
702             boolean expandBecauseOfFalsing) {
703         cancelPeek();
704         float target = expand ? getMaxPanelHeight() : 0;
705         if (!expand) {
706             mClosing = true;
707         }
708         flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing);
709     }
710 
flingToHeight(float vel, boolean expand, float target, float collapseSpeedUpFactor, boolean expandBecauseOfFalsing)711     protected void flingToHeight(float vel, boolean expand, float target,
712             float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
713         // Hack to make the expand transition look nice when clear all button is visible - we make
714         // the animation only to the last notification, and then jump to the maximum panel height so
715         // clear all just fades in and the decelerating motion is towards the last notification.
716         final boolean clearAllExpandHack = expand && fullyExpandedClearAllVisible()
717                 && mExpandedHeight < getMaxPanelHeight() - getClearAllHeight()
718                 && !isClearAllVisible();
719         if (clearAllExpandHack) {
720             target = getMaxPanelHeight() - getClearAllHeight();
721         }
722         if (target == mExpandedHeight || getOverExpansionAmount() > 0f && expand) {
723             notifyExpandingFinished();
724             return;
725         }
726         mOverExpandedBeforeFling = getOverExpansionAmount() > 0f;
727         ValueAnimator animator = createHeightAnimator(target);
728         if (expand) {
729             if (expandBecauseOfFalsing && vel < 0) {
730                 vel = 0;
731             }
732             mFlingAnimationUtils.apply(animator, mExpandedHeight, target, vel, getHeight());
733             if (vel == 0) {
734                 animator.setDuration(350);
735             }
736         } else {
737             if (shouldUseDismissingAnimation()) {
738                 if (vel == 0) {
739                     animator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED);
740                     long duration = (long) (200 + mExpandedHeight / getHeight() * 100);
741                     animator.setDuration(duration);
742                 } else {
743                     mFlingAnimationUtilsDismissing.apply(animator, mExpandedHeight, target, vel,
744                             getHeight());
745                 }
746             } else {
747                 mFlingAnimationUtilsClosing
748                         .apply(animator, mExpandedHeight, target, vel, getHeight());
749             }
750 
751             // Make it shorter if we run a canned animation
752             if (vel == 0) {
753                 animator.setDuration((long) (animator.getDuration() / collapseSpeedUpFactor));
754             }
755         }
756         animator.addListener(new AnimatorListenerAdapter() {
757             private boolean mCancelled;
758 
759             @Override
760             public void onAnimationCancel(Animator animation) {
761                 mCancelled = true;
762             }
763 
764             @Override
765             public void onAnimationEnd(Animator animation) {
766                 if (clearAllExpandHack && !mCancelled) {
767                     setExpandedHeightInternal(getMaxPanelHeight());
768                 }
769                 setAnimator(null);
770                 if (!mCancelled) {
771                     notifyExpandingFinished();
772                 }
773                 notifyBarPanelExpansionChanged();
774             }
775         });
776         setAnimator(animator);
777         animator.start();
778     }
779 
shouldUseDismissingAnimation()780     protected abstract boolean shouldUseDismissingAnimation();
781 
782     @Override
onAttachedToWindow()783     protected void onAttachedToWindow() {
784         super.onAttachedToWindow();
785         mViewName = getResources().getResourceName(getId());
786     }
787 
getName()788     public String getName() {
789         return mViewName;
790     }
791 
setExpandedHeight(float height)792     public void setExpandedHeight(float height) {
793         if (DEBUG) logf("setExpandedHeight(%.1f)", height);
794         setExpandedHeightInternal(height + getOverExpansionPixels());
795     }
796 
797     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)798     protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
799         super.onLayout(changed, left, top, right, bottom);
800         mStatusBar.onPanelLaidOut();
801         requestPanelHeightUpdate();
802         mHasLayoutedSinceDown = true;
803         if (mUpdateFlingOnLayout) {
804             abortAnimations();
805             fling(mUpdateFlingVelocity, true /* expands */);
806             mUpdateFlingOnLayout = false;
807         }
808     }
809 
requestPanelHeightUpdate()810     protected void requestPanelHeightUpdate() {
811         float currentMaxPanelHeight = getMaxPanelHeight();
812 
813         if (isFullyCollapsed()) {
814             return;
815         }
816 
817         if (currentMaxPanelHeight == mExpandedHeight) {
818             return;
819         }
820 
821         if (mPeekAnimator != null || mPeekTouching) {
822             return;
823         }
824 
825         if (mTracking && !isTrackingBlocked()) {
826             return;
827         }
828 
829         if (mHeightAnimator != null) {
830             mPanelUpdateWhenAnimatorEnds = true;
831             return;
832         }
833 
834         setExpandedHeight(currentMaxPanelHeight);
835     }
836 
setExpandedHeightInternal(float h)837     public void setExpandedHeightInternal(float h) {
838         if (mExpandLatencyTracking && h != 0f) {
839             DejankUtils.postAfterTraversal(() -> LatencyTracker.getInstance(mContext).onActionEnd(
840                     LatencyTracker.ACTION_EXPAND_PANEL));
841             mExpandLatencyTracking = false;
842         }
843         float fhWithoutOverExpansion = getMaxPanelHeight() - getOverExpansionAmount();
844         if (mHeightAnimator == null) {
845             float overExpansionPixels = Math.max(0, h - fhWithoutOverExpansion);
846             if (getOverExpansionPixels() != overExpansionPixels && mTracking) {
847                 setOverExpansion(overExpansionPixels, true /* isPixels */);
848             }
849             mExpandedHeight = Math.min(h, fhWithoutOverExpansion) + getOverExpansionAmount();
850         } else {
851             mExpandedHeight = h;
852             if (mOverExpandedBeforeFling) {
853                 setOverExpansion(Math.max(0, h - fhWithoutOverExpansion), false /* isPixels */);
854             }
855         }
856 
857         // If we are closing the panel and we are almost there due to a slow decelerating
858         // interpolator, abort the animation.
859         if (mExpandedHeight < 1f && mExpandedHeight != 0f && mClosing) {
860             mExpandedHeight = 0f;
861             if (mHeightAnimator != null) {
862                 mHeightAnimator.end();
863             }
864         }
865         mExpandedFraction = Math.min(1f,
866                 fhWithoutOverExpansion == 0 ? 0 : mExpandedHeight / fhWithoutOverExpansion);
867         onHeightUpdated(mExpandedHeight);
868         notifyBarPanelExpansionChanged();
869     }
870 
871     /**
872      * @return true if the panel tracking should be temporarily blocked; this is used when a
873      *         conflicting gesture (opening QS) is happening
874      */
isTrackingBlocked()875     protected abstract boolean isTrackingBlocked();
876 
setOverExpansion(float overExpansion, boolean isPixels)877     protected abstract void setOverExpansion(float overExpansion, boolean isPixels);
878 
onHeightUpdated(float expandedHeight)879     protected abstract void onHeightUpdated(float expandedHeight);
880 
getOverExpansionAmount()881     protected abstract float getOverExpansionAmount();
882 
getOverExpansionPixels()883     protected abstract float getOverExpansionPixels();
884 
885     /**
886      * This returns the maximum height of the panel. Children should override this if their
887      * desired height is not the full height.
888      *
889      * @return the default implementation simply returns the maximum height.
890      */
getMaxPanelHeight()891     protected abstract int getMaxPanelHeight();
892 
setExpandedFraction(float frac)893     public void setExpandedFraction(float frac) {
894         setExpandedHeight(getMaxPanelHeight() * frac);
895     }
896 
getExpandedHeight()897     public float getExpandedHeight() {
898         return mExpandedHeight;
899     }
900 
getExpandedFraction()901     public float getExpandedFraction() {
902         return mExpandedFraction;
903     }
904 
isFullyExpanded()905     public boolean isFullyExpanded() {
906         return mExpandedHeight >= getMaxPanelHeight();
907     }
908 
isFullyCollapsed()909     public boolean isFullyCollapsed() {
910         return mExpandedFraction <= 0.0f;
911     }
912 
isCollapsing()913     public boolean isCollapsing() {
914         return mClosing;
915     }
916 
isTracking()917     public boolean isTracking() {
918         return mTracking;
919     }
920 
setBar(PanelBar panelBar)921     public void setBar(PanelBar panelBar) {
922         mBar = panelBar;
923     }
924 
collapse(boolean delayed, float speedUpFactor)925     public void collapse(boolean delayed, float speedUpFactor) {
926         if (DEBUG) logf("collapse: " + this);
927         if (canPanelBeCollapsed()) {
928             cancelHeightAnimator();
929             notifyExpandingStarted();
930 
931             // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state.
932             mClosing = true;
933             if (delayed) {
934                 mNextCollapseSpeedUpFactor = speedUpFactor;
935                 postDelayed(mFlingCollapseRunnable, 120);
936             } else {
937                 fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */);
938             }
939         }
940     }
941 
canPanelBeCollapsed()942     public boolean canPanelBeCollapsed() {
943         return !isFullyCollapsed() && !mTracking && !mClosing;
944     }
945 
946     private final Runnable mFlingCollapseRunnable = new Runnable() {
947         @Override
948         public void run() {
949             fling(0, false /* expand */, mNextCollapseSpeedUpFactor,
950                     false /* expandBecauseOfFalsing */);
951         }
952     };
953 
cancelPeek()954     public void cancelPeek() {
955         boolean cancelled = false;
956         if (mPeekAnimator != null) {
957             cancelled = true;
958             mPeekAnimator.cancel();
959         }
960 
961         if (cancelled) {
962             // When peeking, we already tell mBar that we expanded ourselves. Make sure that we also
963             // notify mBar that we might have closed ourselves.
964             notifyBarPanelExpansionChanged();
965         }
966     }
967 
expand(final boolean animate)968     public void expand(final boolean animate) {
969         if (!isFullyCollapsed() && !isCollapsing()) {
970             return;
971         }
972 
973         mInstantExpanding = true;
974         mAnimateAfterExpanding = animate;
975         mUpdateFlingOnLayout = false;
976         abortAnimations();
977         cancelPeek();
978         if (mTracking) {
979             onTrackingStopped(true /* expands */); // The panel is expanded after this call.
980         }
981         if (mExpanding) {
982             notifyExpandingFinished();
983         }
984         notifyBarPanelExpansionChanged();
985 
986         // Wait for window manager to pickup the change, so we know the maximum height of the panel
987         // then.
988         getViewTreeObserver().addOnGlobalLayoutListener(
989                 new ViewTreeObserver.OnGlobalLayoutListener() {
990                     @Override
991                     public void onGlobalLayout() {
992                         if (!mInstantExpanding) {
993                             getViewTreeObserver().removeOnGlobalLayoutListener(this);
994                             return;
995                         }
996                         if (mStatusBar.getStatusBarWindow().getHeight()
997                                 != mStatusBar.getStatusBarHeight()) {
998                             getViewTreeObserver().removeOnGlobalLayoutListener(this);
999                             if (mAnimateAfterExpanding) {
1000                                 notifyExpandingStarted();
1001                                 fling(0, true /* expand */);
1002                             } else {
1003                                 setExpandedFraction(1f);
1004                             }
1005                             mInstantExpanding = false;
1006                         }
1007                     }
1008                 });
1009 
1010         // Make sure a layout really happens.
1011         requestLayout();
1012     }
1013 
instantCollapse()1014     public void instantCollapse() {
1015         abortAnimations();
1016         setExpandedFraction(0f);
1017         if (mExpanding) {
1018             notifyExpandingFinished();
1019         }
1020         if (mInstantExpanding) {
1021             mInstantExpanding = false;
1022             notifyBarPanelExpansionChanged();
1023         }
1024     }
1025 
abortAnimations()1026     private void abortAnimations() {
1027         cancelPeek();
1028         cancelHeightAnimator();
1029         removeCallbacks(mPostCollapseRunnable);
1030         removeCallbacks(mFlingCollapseRunnable);
1031     }
1032 
onClosingFinished()1033     protected void onClosingFinished() {
1034         mBar.onClosingFinished();
1035     }
1036 
1037 
startUnlockHintAnimation()1038     protected void startUnlockHintAnimation() {
1039 
1040         // We don't need to hint the user if an animation is already running or the user is changing
1041         // the expansion.
1042         if (mHeightAnimator != null || mTracking) {
1043             return;
1044         }
1045         cancelPeek();
1046         notifyExpandingStarted();
1047         startUnlockHintAnimationPhase1(new Runnable() {
1048             @Override
1049             public void run() {
1050                 notifyExpandingFinished();
1051                 onUnlockHintFinished();
1052                 mHintAnimationRunning = false;
1053             }
1054         });
1055         onUnlockHintStarted();
1056         mHintAnimationRunning = true;
1057     }
1058 
onUnlockHintFinished()1059     protected void onUnlockHintFinished() {
1060         mStatusBar.onHintFinished();
1061     }
1062 
onUnlockHintStarted()1063     protected void onUnlockHintStarted() {
1064         mStatusBar.onUnlockHintStarted();
1065     }
1066 
1067     /**
1068      * Phase 1: Move everything upwards.
1069      */
startUnlockHintAnimationPhase1(final Runnable onAnimationFinished)1070     private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) {
1071         float target = Math.max(0, getMaxPanelHeight() - mHintDistance);
1072         ValueAnimator animator = createHeightAnimator(target);
1073         animator.setDuration(250);
1074         animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
1075         animator.addListener(new AnimatorListenerAdapter() {
1076             private boolean mCancelled;
1077 
1078             @Override
1079             public void onAnimationCancel(Animator animation) {
1080                 mCancelled = true;
1081             }
1082 
1083             @Override
1084             public void onAnimationEnd(Animator animation) {
1085                 if (mCancelled) {
1086                     setAnimator(null);
1087                     onAnimationFinished.run();
1088                 } else {
1089                     startUnlockHintAnimationPhase2(onAnimationFinished);
1090                 }
1091             }
1092         });
1093         animator.start();
1094         setAnimator(animator);
1095         mKeyguardBottomArea.getIndicationArea().animate()
1096                 .translationY(-mHintDistance)
1097                 .setDuration(250)
1098                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
1099                 .withEndAction(new Runnable() {
1100                     @Override
1101                     public void run() {
1102                         mKeyguardBottomArea.getIndicationArea().animate()
1103                                 .translationY(0)
1104                                 .setDuration(450)
1105                                 .setInterpolator(mBounceInterpolator)
1106                                 .start();
1107                     }
1108                 })
1109                 .start();
1110     }
1111 
setAnimator(ValueAnimator animator)1112     private void setAnimator(ValueAnimator animator) {
1113         mHeightAnimator = animator;
1114         if (animator == null && mPanelUpdateWhenAnimatorEnds) {
1115             mPanelUpdateWhenAnimatorEnds = false;
1116             requestPanelHeightUpdate();
1117         }
1118     }
1119 
1120     /**
1121      * Phase 2: Bounce down.
1122      */
startUnlockHintAnimationPhase2(final Runnable onAnimationFinished)1123     private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) {
1124         ValueAnimator animator = createHeightAnimator(getMaxPanelHeight());
1125         animator.setDuration(450);
1126         animator.setInterpolator(mBounceInterpolator);
1127         animator.addListener(new AnimatorListenerAdapter() {
1128             @Override
1129             public void onAnimationEnd(Animator animation) {
1130                 setAnimator(null);
1131                 onAnimationFinished.run();
1132                 notifyBarPanelExpansionChanged();
1133             }
1134         });
1135         animator.start();
1136         setAnimator(animator);
1137     }
1138 
createHeightAnimator(float targetHeight)1139     private ValueAnimator createHeightAnimator(float targetHeight) {
1140         ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight);
1141         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1142             @Override
1143             public void onAnimationUpdate(ValueAnimator animation) {
1144                 setExpandedHeightInternal((Float) animation.getAnimatedValue());
1145             }
1146         });
1147         return animator;
1148     }
1149 
notifyBarPanelExpansionChanged()1150     protected void notifyBarPanelExpansionChanged() {
1151         mBar.panelExpansionChanged(mExpandedFraction, mExpandedFraction > 0f
1152                 || mPeekAnimator != null || mInstantExpanding || isPanelVisibleBecauseOfHeadsUp()
1153                 || mTracking || mHeightAnimator != null);
1154     }
1155 
isPanelVisibleBecauseOfHeadsUp()1156     protected abstract boolean isPanelVisibleBecauseOfHeadsUp();
1157 
1158     /**
1159      * Gets called when the user performs a click anywhere in the empty area of the panel.
1160      *
1161      * @return whether the panel will be expanded after the action performed by this method
1162      */
onEmptySpaceClick(float x)1163     protected boolean onEmptySpaceClick(float x) {
1164         if (mHintAnimationRunning) {
1165             return true;
1166         }
1167         return onMiddleClicked();
1168     }
1169 
1170     protected final Runnable mPostCollapseRunnable = new Runnable() {
1171         @Override
1172         public void run() {
1173             collapse(false /* delayed */, 1.0f /* speedUpFactor */);
1174         }
1175     };
1176 
onMiddleClicked()1177     protected abstract boolean onMiddleClicked();
1178 
isDozing()1179     protected abstract boolean isDozing();
1180 
dump(FileDescriptor fd, PrintWriter pw, String[] args)1181     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1182         pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s"
1183                 + " tracking=%s justPeeked=%s peekAnim=%s%s timeAnim=%s%s touchDisabled=%s"
1184                 + "]",
1185                 this.getClass().getSimpleName(),
1186                 getExpandedHeight(),
1187                 getMaxPanelHeight(),
1188                 mClosing?"T":"f",
1189                 mTracking?"T":"f",
1190                 mJustPeeked?"T":"f",
1191                 mPeekAnimator, ((mPeekAnimator!=null && mPeekAnimator.isStarted())?" (started)":""),
1192                 mHeightAnimator, ((mHeightAnimator !=null && mHeightAnimator.isStarted())?" (started)":""),
1193                 mTouchDisabled?"T":"f"
1194         ));
1195     }
1196 
resetViews()1197     public abstract void resetViews();
1198 
getPeekHeight()1199     protected abstract float getPeekHeight();
1200     /**
1201      * @return whether "Clear all" button will be visible when the panel is fully expanded
1202      */
fullyExpandedClearAllVisible()1203     protected abstract boolean fullyExpandedClearAllVisible();
1204 
isClearAllVisible()1205     protected abstract boolean isClearAllVisible();
1206 
1207     /**
1208      * @return the height of the clear all button, in pixels
1209      */
getClearAllHeight()1210     protected abstract int getClearAllHeight();
1211 
setHeadsUpManager(HeadsUpManager headsUpManager)1212     public void setHeadsUpManager(HeadsUpManager headsUpManager) {
1213         mHeadsUpManager = headsUpManager;
1214     }
1215 }
1216