Home | History | Annotate | Download | only in answermethod
      1 /*
      2  * Copyright (C) 2016 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.incallui.answer.impl.answermethod;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ValueAnimator;
     22 import android.animation.ValueAnimator.AnimatorUpdateListener;
     23 import android.annotation.SuppressLint;
     24 import android.content.Context;
     25 import android.support.annotation.FloatRange;
     26 import android.support.annotation.IntDef;
     27 import android.support.annotation.NonNull;
     28 import android.support.annotation.Nullable;
     29 import android.view.MotionEvent;
     30 import android.view.VelocityTracker;
     31 import android.view.View;
     32 import android.view.View.OnTouchListener;
     33 import android.view.ViewConfiguration;
     34 import com.android.dialer.common.DpUtil;
     35 import com.android.dialer.common.LogUtil;
     36 import com.android.dialer.common.MathUtil;
     37 import com.android.incallui.answer.impl.classifier.FalsingManager;
     38 import com.android.incallui.answer.impl.utils.FlingAnimationUtils;
     39 import java.lang.annotation.Retention;
     40 import java.lang.annotation.RetentionPolicy;
     41 
     42 /** Touch handler that keeps track of flings for {@link FlingUpDownMethod}. */
     43 @SuppressLint("ClickableViewAccessibility")
     44 class FlingUpDownTouchHandler implements OnTouchListener {
     45 
     46   /** Callback interface for significant events with this touch handler */
     47   interface OnProgressChangedListener {
     48 
     49     /**
     50      * Called when the visible answer progress has changed. Implementations should use this for
     51      * animation, but should not perform accepts or rejects until {@link #onMoveFinish(boolean)} is
     52      * called.
     53      *
     54      * @param progress float representation of the progress with +1f fully accepted, -1f fully
     55      *     rejected, and 0 neutral.
     56      */
     57     void onProgressChanged(@FloatRange(from = -1f, to = 1f) float progress);
     58 
     59     /** Called when a touch event has started being tracked. */
     60     void onTrackingStart();
     61 
     62     /** Called when touch events stop being tracked. */
     63     void onTrackingStopped();
     64 
     65     /**
     66      * Called when the progress has fully animated back to neutral. Normal resting animation should
     67      * resume, possibly with a hint animation first.
     68      *
     69      * @param showHint {@code true} iff the hint animation should be run before resuming normal
     70      *     animation.
     71      */
     72     void onMoveReset(boolean showHint);
     73 
     74     /**
     75      * Called when the progress has animated fully to accept or reject.
     76      *
     77      * @param accept {@code true} if the call has been accepted, {@code false} if it has been
     78      *     rejected.
     79      */
     80     void onMoveFinish(boolean accept);
     81 
     82     /**
     83      * Determine whether this gesture should use the {@link FalsingManager} to reject accidental
     84      * touches
     85      *
     86      * @param downEvent the MotionEvent corresponding to the start of the gesture
     87      * @return {@code true} if the {@link FalsingManager} should be used to reject accidental
     88      *     touches for this gesture
     89      */
     90     boolean shouldUseFalsing(@NonNull MotionEvent downEvent);
     91   }
     92 
     93   // Progress that must be moved through to not show the hint animation after gesture completes
     94   private static final float HINT_MOVE_THRESHOLD_RATIO = .1f;
     95   // Dp touch needs to move upward to be considered fully accepted
     96   private static final int ACCEPT_THRESHOLD_DP = 150;
     97   // Dp touch needs to move downward to be considered fully rejected
     98   private static final int REJECT_THRESHOLD_DP = 150;
     99   // Dp touch needs to move for it to not be considered a false touch (if FalsingManager is not
    100   // enabled)
    101   private static final int FALSING_THRESHOLD_DP = 40;
    102 
    103   // Progress at which a fling in the opposite direction will recenter instead of
    104   // accepting/rejecting
    105   private static final float PROGRESS_FLING_RECENTER = .1f;
    106 
    107   // Progress at which a slow swipe would continue toward accept/reject after the
    108   // touch has been let go, otherwise will recenter
    109   private static final float PROGRESS_SWIPE_RECENTER = .8f;
    110 
    111   private static final float REJECT_FLING_THRESHOLD_MODIFIER = 2f;
    112 
    113   @Retention(RetentionPolicy.SOURCE)
    114   @IntDef({FlingTarget.CENTER, FlingTarget.ACCEPT, FlingTarget.REJECT})
    115   private @interface FlingTarget {
    116     int CENTER = 0;
    117     int ACCEPT = 1;
    118     int REJECT = -1;
    119   }
    120 
    121   /**
    122    * Create a new FlingUpDownTouchHandler and attach it to the target. Will call {@link
    123    * View#setOnTouchListener(OnTouchListener)} before returning.
    124    *
    125    * @param target View whose touches are to be listened to
    126    * @param listener Callback to listen to major events
    127    * @param falsingManager FalsingManager to identify false touches
    128    * @return the instance of FlingUpDownTouchHandler that has been added as a touch listener
    129    */
    130   public static FlingUpDownTouchHandler attach(
    131       @NonNull View target,
    132       @NonNull OnProgressChangedListener listener,
    133       @Nullable FalsingManager falsingManager) {
    134     FlingUpDownTouchHandler handler = new FlingUpDownTouchHandler(target, listener, falsingManager);
    135     target.setOnTouchListener(handler);
    136     return handler;
    137   }
    138 
    139   @NonNull private final View target;
    140   @NonNull private final OnProgressChangedListener listener;
    141 
    142   private VelocityTracker velocityTracker;
    143   private FlingAnimationUtils flingAnimationUtils;
    144 
    145   private boolean touchEnabled = true;
    146   private boolean flingEnabled = true;
    147   private float currentProgress;
    148   private boolean tracking;
    149 
    150   private boolean motionAborted;
    151   private boolean touchSlopExceeded;
    152   private boolean hintDistanceExceeded;
    153   private int trackingPointer;
    154   private Animator progressAnimator;
    155 
    156   private float touchSlop;
    157   private float initialTouchY;
    158   private float acceptThresholdY;
    159   private float rejectThresholdY;
    160   private float zeroY;
    161 
    162   private boolean touchAboveFalsingThreshold;
    163   private float falsingThresholdPx;
    164   private boolean touchUsesFalsing;
    165 
    166   private final float acceptThresholdPx;
    167   private final float rejectThresholdPx;
    168   private final float deadZoneTopPx;
    169 
    170   @Nullable private final FalsingManager falsingManager;
    171 
    172   private FlingUpDownTouchHandler(
    173       @NonNull View target,
    174       @NonNull OnProgressChangedListener listener,
    175       @Nullable FalsingManager falsingManager) {
    176     this.target = target;
    177     this.listener = listener;
    178     Context context = target.getContext();
    179     touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    180     flingAnimationUtils = new FlingAnimationUtils(context, .6f);
    181     falsingThresholdPx = DpUtil.dpToPx(context, FALSING_THRESHOLD_DP);
    182     acceptThresholdPx = DpUtil.dpToPx(context, ACCEPT_THRESHOLD_DP);
    183     rejectThresholdPx = DpUtil.dpToPx(context, REJECT_THRESHOLD_DP);
    184 
    185     deadZoneTopPx =
    186         Math.max(
    187             context.getResources().getDimension(R.dimen.answer_swipe_dead_zone_top),
    188             acceptThresholdPx);
    189     this.falsingManager = falsingManager;
    190   }
    191 
    192   /** Returns {@code true} iff a touch is being tracked */
    193   public boolean isTracking() {
    194     return tracking;
    195   }
    196 
    197   /**
    198    * Sets whether touch events will continue to be listened to
    199    *
    200    * @param touchEnabled whether future touch events will be listened to
    201    */
    202   public void setTouchEnabled(boolean touchEnabled) {
    203     this.touchEnabled = touchEnabled;
    204   }
    205 
    206   /**
    207    * Sets whether fling velocity is used to affect accept/reject behavior
    208    *
    209    * @param flingEnabled whether fling velocity will be used when determining whether to
    210    *     accept/reject or recenter
    211    */
    212   public void setFlingEnabled(boolean flingEnabled) {
    213     this.flingEnabled = flingEnabled;
    214   }
    215 
    216   public void detach() {
    217     cancelProgressAnimator();
    218     setTouchEnabled(false);
    219   }
    220 
    221   @Override
    222   public boolean onTouch(View v, MotionEvent event) {
    223     if (falsingManager != null) {
    224       falsingManager.onTouchEvent(event);
    225     }
    226     if (!touchEnabled) {
    227       return false;
    228     }
    229     if (motionAborted && (event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
    230       return false;
    231     }
    232 
    233     int pointerIndex = event.findPointerIndex(trackingPointer);
    234     if (pointerIndex < 0) {
    235       pointerIndex = 0;
    236       trackingPointer = event.getPointerId(pointerIndex);
    237     }
    238     final float pointerY = event.getY(pointerIndex);
    239 
    240     switch (event.getActionMasked()) {
    241       case MotionEvent.ACTION_DOWN:
    242         if (pointerY < deadZoneTopPx) {
    243           return false;
    244         }
    245         motionAborted = false;
    246         startMotion(pointerY, false, currentProgress);
    247         touchAboveFalsingThreshold = false;
    248         touchUsesFalsing = listener.shouldUseFalsing(event);
    249         if (velocityTracker == null) {
    250           initVelocityTracker();
    251         }
    252         trackMovement(event);
    253         cancelProgressAnimator();
    254         touchSlopExceeded = progressAnimator != null;
    255         onTrackingStarted();
    256         break;
    257       case MotionEvent.ACTION_POINTER_UP:
    258         final int upPointer = event.getPointerId(event.getActionIndex());
    259         if (trackingPointer == upPointer) {
    260           // gesture is ongoing, find a new pointer to track
    261           int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
    262           float newY = event.getY(newIndex);
    263           trackingPointer = event.getPointerId(newIndex);
    264           startMotion(newY, true, currentProgress);
    265         }
    266         break;
    267       case MotionEvent.ACTION_POINTER_DOWN:
    268         motionAborted = true;
    269         endMotionEvent(event, pointerY, true);
    270         return false;
    271       case MotionEvent.ACTION_MOVE:
    272         float deltaY = pointerY - initialTouchY;
    273 
    274         if (Math.abs(deltaY) > touchSlop) {
    275           touchSlopExceeded = true;
    276         }
    277         if (Math.abs(deltaY) >= falsingThresholdPx) {
    278           touchAboveFalsingThreshold = true;
    279         }
    280         setCurrentProgress(pointerYToProgress(pointerY));
    281         trackMovement(event);
    282         break;
    283 
    284       case MotionEvent.ACTION_UP:
    285       case MotionEvent.ACTION_CANCEL:
    286         trackMovement(event);
    287         endMotionEvent(event, pointerY, false);
    288     }
    289     return true;
    290   }
    291 
    292   private void endMotionEvent(MotionEvent event, float pointerY, boolean forceCancel) {
    293     trackingPointer = -1;
    294     if ((tracking && touchSlopExceeded)
    295         || Math.abs(pointerY - initialTouchY) > touchSlop
    296         || event.getActionMasked() == MotionEvent.ACTION_CANCEL
    297         || forceCancel) {
    298       float vel = 0f;
    299       float vectorVel = 0f;
    300       if (velocityTracker != null) {
    301         velocityTracker.computeCurrentVelocity(1000);
    302         vel = velocityTracker.getYVelocity();
    303         vectorVel =
    304             Math.copySign(
    305                 (float) Math.hypot(velocityTracker.getXVelocity(), velocityTracker.getYVelocity()),
    306                 vel);
    307       }
    308 
    309       boolean falseTouch = isFalseTouch();
    310       boolean forceRecenter =
    311           falseTouch
    312               || !touchSlopExceeded
    313               || forceCancel
    314               || event.getActionMasked() == MotionEvent.ACTION_CANCEL;
    315 
    316       @FlingTarget
    317       int target = forceRecenter ? FlingTarget.CENTER : getFlingTarget(pointerY, vectorVel);
    318 
    319       fling(vel, target, falseTouch);
    320       onTrackingStopped();
    321     } else {
    322       onTrackingStopped();
    323       setCurrentProgress(0);
    324       onMoveEnded();
    325     }
    326 
    327     if (velocityTracker != null) {
    328       velocityTracker.recycle();
    329       velocityTracker = null;
    330     }
    331   }
    332 
    333   @FlingTarget
    334   private int getFlingTarget(float pointerY, float vectorVel) {
    335     float progress = pointerYToProgress(pointerY);
    336 
    337     float minVelocityPxPerSecond = flingAnimationUtils.getMinVelocityPxPerSecond();
    338     if (vectorVel > 0) {
    339       minVelocityPxPerSecond *= REJECT_FLING_THRESHOLD_MODIFIER;
    340     }
    341     if (!flingEnabled || Math.abs(vectorVel) < minVelocityPxPerSecond) {
    342       // Not a fling
    343       if (Math.abs(progress) > PROGRESS_SWIPE_RECENTER) {
    344         // Progress near one of the edges
    345         return progress > 0 ? FlingTarget.ACCEPT : FlingTarget.REJECT;
    346       } else {
    347         return FlingTarget.CENTER;
    348       }
    349     }
    350 
    351     boolean sameDirection = vectorVel < 0 == progress > 0;
    352     if (!sameDirection && Math.abs(progress) >= PROGRESS_FLING_RECENTER) {
    353       // Being flung back toward center
    354       return FlingTarget.CENTER;
    355     }
    356     // Flung toward an edge
    357     return vectorVel < 0 ? FlingTarget.ACCEPT : FlingTarget.REJECT;
    358   }
    359 
    360   @FloatRange(from = -1f, to = 1f)
    361   private float pointerYToProgress(float pointerY) {
    362     boolean pointerAboveZero = pointerY > zeroY;
    363     float nearestThreshold = pointerAboveZero ? rejectThresholdY : acceptThresholdY;
    364 
    365     float absoluteProgress = (pointerY - zeroY) / (nearestThreshold - zeroY);
    366     return MathUtil.clamp(absoluteProgress * (pointerAboveZero ? -1 : 1), -1f, 1f);
    367   }
    368 
    369   private boolean isFalseTouch() {
    370     if (falsingManager != null && falsingManager.isEnabled()) {
    371       if (falsingManager.isFalseTouch()) {
    372         if (touchUsesFalsing) {
    373           LogUtil.i("FlingUpDownTouchHandler.isFalseTouch", "rejecting false touch");
    374           return true;
    375         } else {
    376           LogUtil.i(
    377               "FlingUpDownTouchHandler.isFalseTouch",
    378               "Suspected false touch, but not using false touch rejection for this gesture");
    379           return false;
    380         }
    381       } else {
    382         return false;
    383       }
    384     }
    385     return !touchAboveFalsingThreshold;
    386   }
    387 
    388   private void trackMovement(MotionEvent event) {
    389     if (velocityTracker != null) {
    390       velocityTracker.addMovement(event);
    391     }
    392   }
    393 
    394   private void fling(float velocity, @FlingTarget int target, boolean centerBecauseOfFalsing) {
    395     ValueAnimator animator = createProgressAnimator(target);
    396     if (target == FlingTarget.CENTER) {
    397       flingAnimationUtils.apply(animator, currentProgress, target, velocity);
    398     } else {
    399       flingAnimationUtils.applyDismissing(animator, currentProgress, target, velocity, 1);
    400     }
    401     if (target == FlingTarget.CENTER && centerBecauseOfFalsing) {
    402       velocity = 0;
    403     }
    404     if (velocity == 0) {
    405       animator.setDuration(350);
    406     }
    407 
    408     animator.addListener(
    409         new AnimatorListenerAdapter() {
    410           boolean canceled;
    411 
    412           @Override
    413           public void onAnimationCancel(Animator animation) {
    414             canceled = true;
    415           }
    416 
    417           @Override
    418           public void onAnimationEnd(Animator animation) {
    419             progressAnimator = null;
    420             if (!canceled) {
    421               onMoveEnded();
    422             }
    423           }
    424         });
    425     progressAnimator = animator;
    426     animator.start();
    427   }
    428 
    429   private void onMoveEnded() {
    430     if (currentProgress == 0) {
    431       listener.onMoveReset(!hintDistanceExceeded);
    432     } else {
    433       listener.onMoveFinish(currentProgress > 0);
    434     }
    435   }
    436 
    437   private ValueAnimator createProgressAnimator(float targetProgress) {
    438     ValueAnimator animator = ValueAnimator.ofFloat(currentProgress, targetProgress);
    439     animator.addUpdateListener(
    440         new AnimatorUpdateListener() {
    441           @Override
    442           public void onAnimationUpdate(ValueAnimator animation) {
    443             setCurrentProgress((Float) animation.getAnimatedValue());
    444           }
    445         });
    446     return animator;
    447   }
    448 
    449   private void initVelocityTracker() {
    450     if (velocityTracker != null) {
    451       velocityTracker.recycle();
    452     }
    453     velocityTracker = VelocityTracker.obtain();
    454   }
    455 
    456   private void startMotion(float newY, boolean startTracking, float startProgress) {
    457     initialTouchY = newY;
    458     hintDistanceExceeded = false;
    459 
    460     if (startProgress <= .25) {
    461       acceptThresholdY = Math.max(0, initialTouchY - acceptThresholdPx);
    462       rejectThresholdY = Math.min(target.getHeight(), initialTouchY + rejectThresholdPx);
    463       zeroY = initialTouchY;
    464     }
    465 
    466     if (startTracking) {
    467       touchSlopExceeded = true;
    468       onTrackingStarted();
    469       setCurrentProgress(startProgress);
    470     }
    471   }
    472 
    473   private void onTrackingStarted() {
    474     tracking = true;
    475     listener.onTrackingStart();
    476   }
    477 
    478   private void onTrackingStopped() {
    479     tracking = false;
    480     listener.onTrackingStopped();
    481   }
    482 
    483   private void cancelProgressAnimator() {
    484     if (progressAnimator != null) {
    485       progressAnimator.cancel();
    486     }
    487   }
    488 
    489   private void setCurrentProgress(float progress) {
    490     if (Math.abs(progress) > HINT_MOVE_THRESHOLD_RATIO) {
    491       hintDistanceExceeded = true;
    492     }
    493     currentProgress = progress;
    494     listener.onProgressChanged(progress);
    495   }
    496 }
    497