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