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