1 /*
2  * Copyright (C) 2011 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;
18 
19 import static com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS;
20 
21 import android.animation.Animator;
22 import android.animation.AnimatorListenerAdapter;
23 import android.animation.ObjectAnimator;
24 import android.animation.ValueAnimator;
25 import android.animation.ValueAnimator.AnimatorUpdateListener;
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.app.Notification;
29 import android.app.PendingIntent;
30 import android.content.res.Resources;
31 import android.graphics.RectF;
32 import android.os.Handler;
33 import android.util.ArrayMap;
34 import android.util.Log;
35 import android.view.MotionEvent;
36 import android.view.VelocityTracker;
37 import android.view.View;
38 import android.view.ViewConfiguration;
39 import android.view.accessibility.AccessibilityEvent;
40 
41 import com.android.systemui.animation.Interpolators;
42 import com.android.systemui.plugins.FalsingManager;
43 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
44 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
45 import com.android.wm.shell.animation.FlingAnimationUtils;
46 
47 public class SwipeHelper implements Gefingerpoken {
48     static final String TAG = "com.android.systemui.SwipeHelper";
49     private static final boolean DEBUG = false;
50     private static final boolean DEBUG_INVALIDATE = false;
51     private static final boolean SLOW_ANIMATIONS = false; // DEBUG;
52     private static final boolean CONSTRAIN_SWIPE = true;
53     private static final boolean FADE_OUT_DURING_SWIPE = true;
54     private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
55 
56     public static final int X = 0;
57     public static final int Y = 1;
58 
59     private static final float SWIPE_ESCAPE_VELOCITY = 500f; // dp/sec
60     private static final int DEFAULT_ESCAPE_ANIMATION_DURATION = 200; // ms
61     private static final int MAX_ESCAPE_ANIMATION_DURATION = 400; // ms
62     private static final int MAX_DISMISS_VELOCITY = 4000; // dp/sec
63     private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms
64 
65     static final float SWIPE_PROGRESS_FADE_END = 0.5f; // fraction of thumbnail width
66                                               // beyond which swipe progress->0
67     public static final float SWIPED_FAR_ENOUGH_SIZE_FRACTION = 0.6f;
68     static final float MAX_SCROLL_SIZE_FRACTION = 0.3f;
69 
70     protected final Handler mHandler;
71 
72     private float mMinSwipeProgress = 0f;
73     private float mMaxSwipeProgress = 1f;
74 
75     private final FlingAnimationUtils mFlingAnimationUtils;
76     private float mPagingTouchSlop;
77     private final float mSlopMultiplier;
78     private int mTouchSlop;
79     private float mTouchSlopMultiplier;
80 
81     private final Callback mCallback;
82     private final int mSwipeDirection;
83     private final VelocityTracker mVelocityTracker;
84     private final FalsingManager mFalsingManager;
85 
86     private float mInitialTouchPos;
87     private float mPerpendicularInitialTouchPos;
88     private boolean mIsSwiping;
89     private boolean mSnappingChild;
90     private View mTouchedView;
91     private boolean mCanCurrViewBeDimissed;
92     private float mDensityScale;
93     private float mTranslation = 0;
94 
95     private boolean mMenuRowIntercepting;
96     private final long mLongPressTimeout;
97     private boolean mLongPressSent;
98     private final float[] mDownLocation = new float[2];
99     private final Runnable mPerformLongPress = new Runnable() {
100 
101         private final int[] mViewOffset = new int[2];
102 
103         @Override
104         public void run() {
105             if (mTouchedView != null && !mLongPressSent) {
106                 mLongPressSent = true;
107                 if (mTouchedView instanceof ExpandableNotificationRow) {
108                     mTouchedView.getLocationOnScreen(mViewOffset);
109                     final int x = (int) mDownLocation[0] - mViewOffset[0];
110                     final int y = (int) mDownLocation[1] - mViewOffset[1];
111                     mTouchedView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
112                     ((ExpandableNotificationRow) mTouchedView).doLongClickCallback(x, y);
113 
114                     if (isAvailableToDragAndDrop(mTouchedView)) {
115                         mCallback.onLongPressSent(mTouchedView);
116                     }
117                 }
118             }
119         }
120     };
121 
122     private final int mFalsingThreshold;
123     private boolean mTouchAboveFalsingThreshold;
124     private boolean mDisableHwLayers;
125     private final boolean mFadeDependingOnAmountSwiped;
126 
127     private final ArrayMap<View, Animator> mDismissPendingMap = new ArrayMap<>();
128 
SwipeHelper( int swipeDirection, Callback callback, Resources resources, ViewConfiguration viewConfiguration, FalsingManager falsingManager)129     public SwipeHelper(
130             int swipeDirection, Callback callback, Resources resources,
131             ViewConfiguration viewConfiguration, FalsingManager falsingManager) {
132         mCallback = callback;
133         mHandler = new Handler();
134         mSwipeDirection = swipeDirection;
135         mVelocityTracker = VelocityTracker.obtain();
136         mPagingTouchSlop = viewConfiguration.getScaledPagingTouchSlop();
137         mSlopMultiplier = viewConfiguration.getScaledAmbiguousGestureMultiplier();
138         mTouchSlop = viewConfiguration.getScaledTouchSlop();
139         mTouchSlopMultiplier = viewConfiguration.getAmbiguousGestureMultiplier();
140 
141         // Extra long-press!
142         mLongPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f);
143 
144         mDensityScale =  resources.getDisplayMetrics().density;
145         mFalsingThreshold = resources.getDimensionPixelSize(R.dimen.swipe_helper_falsing_threshold);
146         mFadeDependingOnAmountSwiped = resources.getBoolean(
147                 R.bool.config_fadeDependingOnAmountSwiped);
148         mFalsingManager = falsingManager;
149         mFlingAnimationUtils = new FlingAnimationUtils(resources.getDisplayMetrics(),
150                 getMaxEscapeAnimDuration() / 1000f);
151     }
152 
setDensityScale(float densityScale)153     public void setDensityScale(float densityScale) {
154         mDensityScale = densityScale;
155     }
156 
setPagingTouchSlop(float pagingTouchSlop)157     public void setPagingTouchSlop(float pagingTouchSlop) {
158         mPagingTouchSlop = pagingTouchSlop;
159     }
160 
setDisableHardwareLayers(boolean disableHwLayers)161     public void setDisableHardwareLayers(boolean disableHwLayers) {
162         mDisableHwLayers = disableHwLayers;
163     }
164 
getPos(MotionEvent ev)165     private float getPos(MotionEvent ev) {
166         return mSwipeDirection == X ? ev.getX() : ev.getY();
167     }
168 
getPerpendicularPos(MotionEvent ev)169     private float getPerpendicularPos(MotionEvent ev) {
170         return mSwipeDirection == X ? ev.getY() : ev.getX();
171     }
172 
getTranslation(View v)173     protected float getTranslation(View v) {
174         return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY();
175     }
176 
getVelocity(VelocityTracker vt)177     private float getVelocity(VelocityTracker vt) {
178         return mSwipeDirection == X ? vt.getXVelocity() :
179                 vt.getYVelocity();
180     }
181 
createTranslationAnimation(View v, float newPos)182     protected ObjectAnimator createTranslationAnimation(View v, float newPos) {
183         ObjectAnimator anim = ObjectAnimator.ofFloat(v,
184                 mSwipeDirection == X ? View.TRANSLATION_X : View.TRANSLATION_Y, newPos);
185         return anim;
186     }
187 
getPerpendicularVelocity(VelocityTracker vt)188     private float getPerpendicularVelocity(VelocityTracker vt) {
189         return mSwipeDirection == X ? vt.getYVelocity() :
190                 vt.getXVelocity();
191     }
192 
getViewTranslationAnimator(View v, float target, AnimatorUpdateListener listener)193     protected Animator getViewTranslationAnimator(View v, float target,
194             AnimatorUpdateListener listener) {
195         ObjectAnimator anim = createTranslationAnimation(v, target);
196         if (listener != null) {
197             anim.addUpdateListener(listener);
198         }
199         return anim;
200     }
201 
setTranslation(View v, float translate)202     protected void setTranslation(View v, float translate) {
203         if (v == null) {
204             return;
205         }
206         if (mSwipeDirection == X) {
207             v.setTranslationX(translate);
208         } else {
209             v.setTranslationY(translate);
210         }
211     }
212 
getSize(View v)213     protected float getSize(View v) {
214         return mSwipeDirection == X ? v.getMeasuredWidth() : v.getMeasuredHeight();
215     }
216 
setMinSwipeProgress(float minSwipeProgress)217     public void setMinSwipeProgress(float minSwipeProgress) {
218         mMinSwipeProgress = minSwipeProgress;
219     }
220 
setMaxSwipeProgress(float maxSwipeProgress)221     public void setMaxSwipeProgress(float maxSwipeProgress) {
222         mMaxSwipeProgress = maxSwipeProgress;
223     }
224 
getSwipeProgressForOffset(View view, float translation)225     private float getSwipeProgressForOffset(View view, float translation) {
226         float viewSize = getSize(view);
227         float result = Math.abs(translation / viewSize);
228         return Math.min(Math.max(mMinSwipeProgress, result), mMaxSwipeProgress);
229     }
230 
getSwipeAlpha(float progress)231     private float getSwipeAlpha(float progress) {
232         if (mFadeDependingOnAmountSwiped) {
233             // The more progress has been fade, the lower the alpha value so that the view fades.
234             return Math.max(1 - progress, 0);
235         }
236 
237         return 1f - Math.max(0, Math.min(1, progress / SWIPE_PROGRESS_FADE_END));
238     }
239 
updateSwipeProgressFromOffset(View animView, boolean dismissable)240     private void updateSwipeProgressFromOffset(View animView, boolean dismissable) {
241         updateSwipeProgressFromOffset(animView, dismissable, getTranslation(animView));
242     }
243 
updateSwipeProgressFromOffset(View animView, boolean dismissable, float translation)244     private void updateSwipeProgressFromOffset(View animView, boolean dismissable,
245             float translation) {
246         float swipeProgress = getSwipeProgressForOffset(animView, translation);
247         if (!mCallback.updateSwipeProgress(animView, dismissable, swipeProgress)) {
248             if (FADE_OUT_DURING_SWIPE && dismissable) {
249                 if (!mDisableHwLayers) {
250                     if (swipeProgress != 0f && swipeProgress != 1f) {
251                         animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
252                     } else {
253                         animView.setLayerType(View.LAYER_TYPE_NONE, null);
254                     }
255                 }
256                 animView.setAlpha(getSwipeAlpha(swipeProgress));
257             }
258         }
259         invalidateGlobalRegion(animView);
260     }
261 
262     // invalidate the view's own bounds all the way up the view hierarchy
invalidateGlobalRegion(View view)263     public static void invalidateGlobalRegion(View view) {
264         invalidateGlobalRegion(
265             view,
266             new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
267     }
268 
269     // invalidate a rectangle relative to the view's coordinate system all the way up the view
270     // hierarchy
invalidateGlobalRegion(View view, RectF childBounds)271     public static void invalidateGlobalRegion(View view, RectF childBounds) {
272         //childBounds.offset(view.getTranslationX(), view.getTranslationY());
273         if (DEBUG_INVALIDATE)
274             Log.v(TAG, "-------------");
275         while (view.getParent() != null && view.getParent() instanceof View) {
276             view = (View) view.getParent();
277             view.getMatrix().mapRect(childBounds);
278             view.invalidate((int) Math.floor(childBounds.left),
279                             (int) Math.floor(childBounds.top),
280                             (int) Math.ceil(childBounds.right),
281                             (int) Math.ceil(childBounds.bottom));
282             if (DEBUG_INVALIDATE) {
283                 Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
284                         + "," + (int) Math.floor(childBounds.top)
285                         + "," + (int) Math.ceil(childBounds.right)
286                         + "," + (int) Math.ceil(childBounds.bottom));
287             }
288         }
289     }
290 
cancelLongPress()291     public void cancelLongPress() {
292         mHandler.removeCallbacks(mPerformLongPress);
293     }
294 
295     @Override
onInterceptTouchEvent(final MotionEvent ev)296     public boolean onInterceptTouchEvent(final MotionEvent ev) {
297         if (mTouchedView instanceof ExpandableNotificationRow) {
298             NotificationMenuRowPlugin nmr = ((ExpandableNotificationRow) mTouchedView).getProvider();
299             if (nmr != null) {
300                 mMenuRowIntercepting = nmr.onInterceptTouchEvent(mTouchedView, ev);
301             }
302         }
303         final int action = ev.getAction();
304 
305         switch (action) {
306             case MotionEvent.ACTION_DOWN:
307                 mTouchAboveFalsingThreshold = false;
308                 mIsSwiping = false;
309                 mSnappingChild = false;
310                 mLongPressSent = false;
311                 mCallback.onLongPressSent(null);
312                 mVelocityTracker.clear();
313                 cancelLongPress();
314                 mTouchedView = mCallback.getChildAtPosition(ev);
315 
316                 if (mTouchedView != null) {
317                     onDownUpdate(mTouchedView, ev);
318                     mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mTouchedView);
319                     mVelocityTracker.addMovement(ev);
320                     mInitialTouchPos = getPos(ev);
321                     mPerpendicularInitialTouchPos = getPerpendicularPos(ev);
322                     mTranslation = getTranslation(mTouchedView);
323                     mDownLocation[0] = ev.getRawX();
324                     mDownLocation[1] = ev.getRawY();
325                     mHandler.postDelayed(mPerformLongPress, mLongPressTimeout);
326                 }
327                 break;
328 
329             case MotionEvent.ACTION_MOVE:
330                 if (mTouchedView != null && !mLongPressSent) {
331                     mVelocityTracker.addMovement(ev);
332                     float pos = getPos(ev);
333                     float perpendicularPos = getPerpendicularPos(ev);
334                     float delta = pos - mInitialTouchPos;
335                     float deltaPerpendicular = perpendicularPos - mPerpendicularInitialTouchPos;
336                     // Adjust the touch slop if another gesture may be being performed.
337                     final float pagingTouchSlop =
338                             ev.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
339                             ? mPagingTouchSlop * mSlopMultiplier
340                             : mPagingTouchSlop;
341                     if (Math.abs(delta) > pagingTouchSlop
342                             && Math.abs(delta) > Math.abs(deltaPerpendicular)) {
343                         if (mCallback.canChildBeDragged(mTouchedView)) {
344                             mIsSwiping = true;
345                             mCallback.onBeginDrag(mTouchedView);
346                             mInitialTouchPos = getPos(ev);
347                             mTranslation = getTranslation(mTouchedView);
348                         }
349                         cancelLongPress();
350                     } else if (ev.getClassification() == MotionEvent.CLASSIFICATION_DEEP_PRESS
351                                     && mHandler.hasCallbacks(mPerformLongPress)) {
352                         // Accelerate the long press signal.
353                         cancelLongPress();
354                         mPerformLongPress.run();
355                     }
356                 }
357                 break;
358 
359             case MotionEvent.ACTION_UP:
360             case MotionEvent.ACTION_CANCEL:
361                 final boolean captured = (mIsSwiping || mLongPressSent || mMenuRowIntercepting);
362                 mIsSwiping = false;
363                 mTouchedView = null;
364                 mLongPressSent = false;
365                 mCallback.onLongPressSent(null);
366                 mMenuRowIntercepting = false;
367                 cancelLongPress();
368                 if (captured) return true;
369                 break;
370         }
371         return mIsSwiping || mLongPressSent || mMenuRowIntercepting;
372     }
373 
374     /**
375      * @param view The view to be dismissed
376      * @param velocity The desired pixels/second speed at which the view should move
377      * @param useAccelerateInterpolator Should an accelerating Interpolator be used
378      */
dismissChild(final View view, float velocity, boolean useAccelerateInterpolator)379     public void dismissChild(final View view, float velocity, boolean useAccelerateInterpolator) {
380         dismissChild(view, velocity, null /* endAction */, 0 /* delay */,
381                 useAccelerateInterpolator, 0 /* fixedDuration */, false /* isDismissAll */);
382     }
383 
384     /**
385      * @param animView The view to be dismissed
386      * @param velocity The desired pixels/second speed at which the view should move
387      * @param endAction The action to perform at the end
388      * @param delay The delay after which we should start
389      * @param useAccelerateInterpolator Should an accelerating Interpolator be used
390      * @param fixedDuration If not 0, this exact duration will be taken
391      */
dismissChild(final View animView, float velocity, final Runnable endAction, long delay, boolean useAccelerateInterpolator, long fixedDuration, boolean isDismissAll)392     public void dismissChild(final View animView, float velocity, final Runnable endAction,
393             long delay, boolean useAccelerateInterpolator, long fixedDuration,
394             boolean isDismissAll) {
395         final boolean canBeDismissed = mCallback.canChildBeDismissed(animView);
396         float newPos;
397         boolean isLayoutRtl = animView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
398 
399         // if we use the Menu to dismiss an item in landscape, animate up
400         boolean animateUpForMenu = velocity == 0 && (getTranslation(animView) == 0 || isDismissAll)
401                 && mSwipeDirection == Y;
402         // if the language is rtl we prefer swiping to the left
403         boolean animateLeftForRtl = velocity == 0 && (getTranslation(animView) == 0 || isDismissAll)
404                 && isLayoutRtl;
405         boolean animateLeft = (Math.abs(velocity) > getEscapeVelocity() && velocity < 0) ||
406                 (getTranslation(animView) < 0 && !isDismissAll);
407         if (animateLeft || animateLeftForRtl || animateUpForMenu) {
408             newPos = -getTotalTranslationLength(animView);
409         } else {
410             newPos = getTotalTranslationLength(animView);
411         }
412         long duration;
413         if (fixedDuration == 0) {
414             duration = MAX_ESCAPE_ANIMATION_DURATION;
415             if (velocity != 0) {
416                 duration = Math.min(duration,
417                         (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math
418                                 .abs(velocity))
419                 );
420             } else {
421                 duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
422             }
423         } else {
424             duration = fixedDuration;
425         }
426 
427         if (!mDisableHwLayers) {
428             animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
429         }
430         AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
431             @Override
432             public void onAnimationUpdate(ValueAnimator animation) {
433                 onTranslationUpdate(animView, (float) animation.getAnimatedValue(), canBeDismissed);
434             }
435         };
436 
437         Animator anim = getViewTranslationAnimator(animView, newPos, updateListener);
438         if (anim == null) {
439             return;
440         }
441         if (useAccelerateInterpolator) {
442             anim.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
443             anim.setDuration(duration);
444         } else {
445             mFlingAnimationUtils.applyDismissing(anim, getTranslation(animView),
446                     newPos, velocity, getSize(animView));
447         }
448         if (delay > 0) {
449             anim.setStartDelay(delay);
450         }
451         anim.addListener(new AnimatorListenerAdapter() {
452             private boolean mCancelled;
453 
454             @Override
455             public void onAnimationStart(Animator animation) {
456                 super.onAnimationStart(animation);
457                 mCallback.onBeginDrag(animView);
458             }
459 
460             @Override
461             public void onAnimationCancel(Animator animation) {
462                 mCancelled = true;
463             }
464 
465             @Override
466             public void onAnimationEnd(Animator animation) {
467                 updateSwipeProgressFromOffset(animView, canBeDismissed);
468                 mDismissPendingMap.remove(animView);
469                 boolean wasRemoved = false;
470                 if (animView instanceof ExpandableNotificationRow) {
471                     ExpandableNotificationRow row = (ExpandableNotificationRow) animView;
472                     wasRemoved = row.isRemoved();
473                 }
474                 if (!mCancelled || wasRemoved) {
475                     mCallback.onChildDismissed(animView);
476                     resetSwipeState();
477                 }
478                 if (endAction != null) {
479                     endAction.run();
480                 }
481                 if (!mDisableHwLayers) {
482                     animView.setLayerType(View.LAYER_TYPE_NONE, null);
483                 }
484             }
485         });
486 
487         prepareDismissAnimation(animView, anim);
488         mDismissPendingMap.put(animView, anim);
489         anim.start();
490     }
491 
492     /**
493      * Get the total translation length where we want to swipe to when dismissing the view. By
494      * default this is the size of the view, but can also be larger.
495      * @param animView the view to ask about
496      */
getTotalTranslationLength(View animView)497     protected float getTotalTranslationLength(View animView) {
498         return getSize(animView);
499     }
500 
501     /**
502      * Called to update the dismiss animation.
503      */
prepareDismissAnimation(View view, Animator anim)504     protected void prepareDismissAnimation(View view, Animator anim) {
505         // Do nothing
506     }
507 
snapChild(final View animView, final float targetLeft, float velocity)508     public void snapChild(final View animView, final float targetLeft, float velocity) {
509         final boolean canBeDismissed = mCallback.canChildBeDismissed(animView);
510         AnimatorUpdateListener updateListener = animation -> onTranslationUpdate(animView,
511                 (float) animation.getAnimatedValue(), canBeDismissed);
512 
513         Animator anim = getViewTranslationAnimator(animView, targetLeft, updateListener);
514         if (anim == null) {
515             return;
516         }
517         anim.addListener(new AnimatorListenerAdapter() {
518             boolean wasCancelled = false;
519 
520             @Override
521             public void onAnimationCancel(Animator animator) {
522                 wasCancelled = true;
523             }
524 
525             @Override
526             public void onAnimationEnd(Animator animator) {
527                 mSnappingChild = false;
528                 if (!wasCancelled) {
529                     updateSwipeProgressFromOffset(animView, canBeDismissed);
530                     resetSwipeState();
531                 }
532             }
533         });
534         prepareSnapBackAnimation(animView, anim);
535         mSnappingChild = true;
536         float maxDistance = Math.abs(targetLeft - getTranslation(animView));
537         mFlingAnimationUtils.apply(anim, getTranslation(animView), targetLeft, velocity,
538                 maxDistance);
539         anim.start();
540         mCallback.onChildSnappedBack(animView, targetLeft);
541     }
542 
543     /**
544      * Give the swipe helper itself a chance to do something on snap back so NSSL doesn't have
545      * to tell us what to do
546      */
onChildSnappedBack(View animView, float targetLeft)547     protected void onChildSnappedBack(View animView, float targetLeft) {
548     }
549 
550     /**
551      * Called to update the snap back animation.
552      */
prepareSnapBackAnimation(View view, Animator anim)553     protected void prepareSnapBackAnimation(View view, Animator anim) {
554         // Do nothing
555     }
556 
557     /**
558      * Called when there's a down event.
559      */
onDownUpdate(View currView, MotionEvent ev)560     public void onDownUpdate(View currView, MotionEvent ev) {
561         // Do nothing
562     }
563 
564     /**
565      * Called on a move event.
566      */
onMoveUpdate(View view, MotionEvent ev, float totalTranslation, float delta)567     protected void onMoveUpdate(View view, MotionEvent ev, float totalTranslation, float delta) {
568         // Do nothing
569     }
570 
571     /**
572      * Called in {@link AnimatorUpdateListener#onAnimationUpdate(ValueAnimator)} when the current
573      * view is being animated to dismiss or snap.
574      */
onTranslationUpdate(View animView, float value, boolean canBeDismissed)575     public void onTranslationUpdate(View animView, float value, boolean canBeDismissed) {
576         updateSwipeProgressFromOffset(animView, canBeDismissed, value);
577     }
578 
snapChildInstantly(final View view)579     private void snapChildInstantly(final View view) {
580         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
581         setTranslation(view, 0);
582         updateSwipeProgressFromOffset(view, canAnimViewBeDismissed);
583     }
584 
585     /**
586      * Called when a view is updated to be non-dismissable, if the view was being dismissed before
587      * the update this will handle snapping it back into place.
588      *
589      * @param view the view to snap if necessary.
590      * @param animate whether to animate the snap or not.
591      * @param targetLeft the target to snap to.
592      */
snapChildIfNeeded(final View view, boolean animate, float targetLeft)593     public void snapChildIfNeeded(final View view, boolean animate, float targetLeft) {
594         if ((mIsSwiping && mTouchedView == view) || mSnappingChild) {
595             return;
596         }
597         boolean needToSnap = false;
598         Animator dismissPendingAnim = mDismissPendingMap.get(view);
599         if (dismissPendingAnim != null) {
600             needToSnap = true;
601             dismissPendingAnim.cancel();
602         } else if (getTranslation(view) != 0) {
603             needToSnap = true;
604         }
605         if (needToSnap) {
606             if (animate) {
607                 snapChild(view, targetLeft, 0.0f /* velocity */);
608             } else {
609                 snapChildInstantly(view);
610             }
611         }
612     }
613 
614     @Override
onTouchEvent(MotionEvent ev)615     public boolean onTouchEvent(MotionEvent ev) {
616         if (!mIsSwiping && !mMenuRowIntercepting && !mLongPressSent) {
617             if (mCallback.getChildAtPosition(ev) != null) {
618                 // We are dragging directly over a card, make sure that we also catch the gesture
619                 // even if nobody else wants the touch event.
620                 mTouchedView = mCallback.getChildAtPosition(ev);
621                 onInterceptTouchEvent(ev);
622                 return true;
623             } else {
624                 // We are not doing anything, make sure the long press callback
625                 // is not still ticking like a bomb waiting to go off.
626                 cancelLongPress();
627                 return false;
628             }
629         }
630 
631         mVelocityTracker.addMovement(ev);
632         final int action = ev.getAction();
633         switch (action) {
634             case MotionEvent.ACTION_OUTSIDE:
635             case MotionEvent.ACTION_MOVE:
636                 if (mTouchedView != null) {
637                     float delta = getPos(ev) - mInitialTouchPos;
638                     float absDelta = Math.abs(delta);
639                     if (absDelta >= getFalsingThreshold()) {
640                         mTouchAboveFalsingThreshold = true;
641                     }
642 
643                     if (mLongPressSent) {
644                         if (absDelta >= getTouchSlop(ev)) {
645                             if (mTouchedView instanceof ExpandableNotificationRow) {
646                                 ((ExpandableNotificationRow) mTouchedView)
647                                         .doDragCallback(ev.getX(), ev.getY());
648                             }
649                         }
650                     } else {
651                         // don't let items that can't be dismissed be dragged more than
652                         // maxScrollDistance
653                         if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissedInDirection(
654                                 mTouchedView,
655                                 delta > 0)) {
656                             float size = getSize(mTouchedView);
657                             float maxScrollDistance = MAX_SCROLL_SIZE_FRACTION * size;
658                             if (absDelta >= size) {
659                                 delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
660                             } else {
661                                 int startPosition = mCallback.getConstrainSwipeStartPosition();
662                                 if (absDelta > startPosition) {
663                                     int signedStartPosition =
664                                             (int) (startPosition * Math.signum(delta));
665                                     delta = signedStartPosition
666                                             + maxScrollDistance * (float) Math.sin(
667                                             ((delta - signedStartPosition) / size) * (Math.PI / 2));
668                                 }
669                             }
670                         }
671 
672                         setTranslation(mTouchedView, mTranslation + delta);
673                         updateSwipeProgressFromOffset(mTouchedView, mCanCurrViewBeDimissed);
674                         onMoveUpdate(mTouchedView, ev, mTranslation + delta, delta);
675                     }
676                 }
677                 break;
678             case MotionEvent.ACTION_UP:
679             case MotionEvent.ACTION_CANCEL:
680                 if (mTouchedView == null) {
681                     break;
682                 }
683                 mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, getMaxVelocity());
684                 float velocity = getVelocity(mVelocityTracker);
685 
686                 if (!handleUpEvent(ev, mTouchedView, velocity, getTranslation(mTouchedView))) {
687                     if (isDismissGesture(ev)) {
688                         dismissChild(mTouchedView, velocity,
689                                 !swipedFastEnough() /* useAccelerateInterpolator */);
690                     } else {
691                         mCallback.onDragCancelled(mTouchedView);
692                         snapChild(mTouchedView, 0 /* leftTarget */, velocity);
693                     }
694                     mTouchedView = null;
695                 }
696                 mIsSwiping = false;
697                 break;
698         }
699         return true;
700     }
701 
getFalsingThreshold()702     private int getFalsingThreshold() {
703         float factor = mCallback.getFalsingThresholdFactor();
704         return (int) (mFalsingThreshold * factor);
705     }
706 
getMaxVelocity()707     private float getMaxVelocity() {
708         return MAX_DISMISS_VELOCITY * mDensityScale;
709     }
710 
getEscapeVelocity()711     protected float getEscapeVelocity() {
712         return getUnscaledEscapeVelocity() * mDensityScale;
713     }
714 
getUnscaledEscapeVelocity()715     protected float getUnscaledEscapeVelocity() {
716         return SWIPE_ESCAPE_VELOCITY;
717     }
718 
getMaxEscapeAnimDuration()719     protected long getMaxEscapeAnimDuration() {
720         return MAX_ESCAPE_ANIMATION_DURATION;
721     }
722 
swipedFarEnough()723     protected boolean swipedFarEnough() {
724         float translation = getTranslation(mTouchedView);
725         return DISMISS_IF_SWIPED_FAR_ENOUGH
726                 && Math.abs(translation) > SWIPED_FAR_ENOUGH_SIZE_FRACTION * getSize(
727                 mTouchedView);
728     }
729 
isDismissGesture(MotionEvent ev)730     public boolean isDismissGesture(MotionEvent ev) {
731         float translation = getTranslation(mTouchedView);
732         return ev.getActionMasked() == MotionEvent.ACTION_UP
733                 && !mFalsingManager.isUnlockingDisabled()
734                 && !isFalseGesture() && (swipedFastEnough() || swipedFarEnough())
735                 && mCallback.canChildBeDismissedInDirection(mTouchedView, translation > 0);
736     }
737 
738     /** Returns true if the gesture should be rejected. */
isFalseGesture()739     public boolean isFalseGesture() {
740         boolean falsingDetected = mCallback.isAntiFalsingNeeded();
741         if (mFalsingManager.isClassifierEnabled()) {
742             falsingDetected = falsingDetected && mFalsingManager.isFalseTouch(NOTIFICATION_DISMISS);
743         } else {
744             falsingDetected = falsingDetected && !mTouchAboveFalsingThreshold;
745         }
746         return falsingDetected;
747     }
748 
swipedFastEnough()749     protected boolean swipedFastEnough() {
750         float velocity = getVelocity(mVelocityTracker);
751         float translation = getTranslation(mTouchedView);
752         boolean ret = (Math.abs(velocity) > getEscapeVelocity())
753                 && (velocity > 0) == (translation > 0);
754         return ret;
755     }
756 
handleUpEvent(MotionEvent ev, View animView, float velocity, float translation)757     protected boolean handleUpEvent(MotionEvent ev, View animView, float velocity,
758             float translation) {
759         return false;
760     }
761 
isSwiping()762     public boolean isSwiping() {
763         return mIsSwiping;
764     }
765 
766     @Nullable
getSwipedView()767     public View getSwipedView() {
768         return mIsSwiping ? mTouchedView : null;
769     }
770 
resetSwipeState()771     public void resetSwipeState() {
772         mTouchedView = null;
773         mIsSwiping = false;
774     }
775 
getTouchSlop(MotionEvent event)776     private float getTouchSlop(MotionEvent event) {
777         // Adjust the touch slop if another gesture may be being performed.
778         return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE
779                 ? mTouchSlop * mTouchSlopMultiplier
780                 : mTouchSlop;
781     }
782 
isAvailableToDragAndDrop(View v)783     private boolean isAvailableToDragAndDrop(View v) {
784         if (v.getResources().getBoolean(R.bool.config_notificationToContents)) {
785             if (v instanceof ExpandableNotificationRow) {
786                 ExpandableNotificationRow enr = (ExpandableNotificationRow) v;
787                 boolean canBubble = enr.getEntry().canBubble();
788                 Notification notif = enr.getEntry().getSbn().getNotification();
789                 PendingIntent dragIntent = notif.contentIntent != null ? notif.contentIntent
790                         : notif.fullScreenIntent;
791                 if (dragIntent != null && dragIntent.isActivity() && !canBubble) {
792                     return true;
793                 }
794             }
795         }
796         return false;
797     }
798 
799     public interface Callback {
getChildAtPosition(MotionEvent ev)800         View getChildAtPosition(MotionEvent ev);
801 
canChildBeDismissed(View v)802         boolean canChildBeDismissed(View v);
803 
804         /**
805          * Returns true if the provided child can be dismissed by a swipe in the given direction.
806          *
807          * @param isRightOrDown {@code true} if the swipe direction is right or down,
808          *                      {@code false} if it is left or up.
809          */
canChildBeDismissedInDirection(View v, boolean isRightOrDown)810         default boolean canChildBeDismissedInDirection(View v, boolean isRightOrDown) {
811             return canChildBeDismissed(v);
812         }
813 
isAntiFalsingNeeded()814         boolean isAntiFalsingNeeded();
815 
onBeginDrag(View v)816         void onBeginDrag(View v);
817 
onChildDismissed(View v)818         void onChildDismissed(View v);
819 
onDragCancelled(View v)820         void onDragCancelled(View v);
821 
822         /**
823          * Called when the child is long pressed and available to start drag and drop.
824          *
825          * @param v the view that was long pressed.
826          */
onLongPressSent(View v)827         void onLongPressSent(View v);
828 
829         /**
830          * Called when the child is snapped to a position.
831          *
832          * @param animView the view that was snapped.
833          * @param targetLeft the left position the view was snapped to.
834          */
onChildSnappedBack(View animView, float targetLeft)835         void onChildSnappedBack(View animView, float targetLeft);
836 
837         /**
838          * Updates the swipe progress on a child.
839          *
840          * @return if true, prevents the default alpha fading.
841          */
updateSwipeProgress(View animView, boolean dismissable, float swipeProgress)842         boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress);
843 
844         /**
845          * @return The factor the falsing threshold should be multiplied with
846          */
getFalsingThresholdFactor()847         float getFalsingThresholdFactor();
848 
849         /**
850          * @return The position, in pixels, at which a constrained swipe should start being
851          * constrained.
852          */
getConstrainSwipeStartPosition()853         default int getConstrainSwipeStartPosition() {
854             return 0;
855         }
856 
857         /**
858          * @return If true, the given view is draggable.
859          */
canChildBeDragged(@onNull View animView)860         default boolean canChildBeDragged(@NonNull View animView) { return true; }
861     }
862 }
863