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