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