summaryrefslogtreecommitdiff
path: root/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java
diff options
context:
space:
mode:
authorEric Erfanian <erfanian@google.com>2017-02-22 16:32:36 -0800
committerEric Erfanian <erfanian@google.com>2017-03-01 09:56:52 -0800
commitccca31529c07970e89419fb85a9e8153a5396838 (patch)
treea7034c0a01672b97728c13282a2672771cd28baa /java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java
parente7ae4624ba6f25cb8e648db74e0d64c0113a16ba (diff)
Update dialer sources.
Test: Built package and system image. This change clobbers the old source, and is an export from an internal Google repository. The internal repository was forked form Android in March, and this change includes modifications since then, to near the v8 release. Since the fork, we've moved code from monolithic to independent modules. In addition, we've switched to Blaze/Bazel as the build sysetm. This export, however, still uses make. New dependencies have been added: - Dagger - Auto-Value - Glide - Libshortcutbadger Going forward, development will still be in Google3, and the Gerrit release will become an automated export, with the next drop happening in ~ two weeks. Android.mk includes local modifications from ToT. Abridged changelog: Bug fixes ● Not able to mute, add a call when using Phone app in multiwindow mode ● Double tap on keypad triggering multiple key and tones ● Reported spam numbers not showing as spam in the call log ● Crash when user tries to block number while Phone app is not set as default ● Crash when user picks a number from search auto-complete list Visual Voicemail (VVM) improvements ● Share Voicemail audio via standard exporting mechanisms that support file attachment (email, MMS, etc.) ● Make phone number, email and web sites in VVM transcript clickable ● Set PIN before declining VVM Terms of Service {Carrier} ● Set client type for outbound visual voicemail SMS {Carrier} New incoming call and incall UI on older devices (Android M) ● Updated Phone app icon ● New incall UI (large buttons, button labels) ● New and animated Answer/Reject gestures Accessibility ● Add custom answer/decline call buttons on answer screen for touch exploration accessibility services ● Increase size of touch target ● Add verbal feedback when a Voicemail fails to load ● Fix pressing of Phone buttons while in a phone call using Switch Access ● Fix selecting and opening contacts in talkback mode ● Split focus for ‘Learn More’ link in caller id & spam to help distinguish similar text Other ● Backup & Restore for App Preferences ● Prompt user to enable Wi-Fi calling if the call ends due to out of service and Wi-Fi is connected ● Rename “Dialpad” to “Keypad” ● Show "Private number" for restricted calls ● Delete unused items (vcard, add contact, call history) from Phone menu Change-Id: I2a7e53532a24c21bf308bf0a6d178d7ddbca4958
Diffstat (limited to 'java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java')
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java496
1 files changed, 496 insertions, 0 deletions
diff --git a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java
new file mode 100644
index 000000000..a21073d65
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java
@@ -0,0 +1,496 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.answermethod;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.support.annotation.FloatRange;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import com.android.dialer.common.DpUtil;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.MathUtil;
+import com.android.incallui.answer.impl.classifier.FalsingManager;
+import com.android.incallui.answer.impl.utils.FlingAnimationUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Touch handler that keeps track of flings for {@link FlingUpDownMethod}. */
+@SuppressLint("ClickableViewAccessibility")
+class FlingUpDownTouchHandler implements OnTouchListener {
+
+ /** Callback interface for significant events with this touch handler */
+ interface OnProgressChangedListener {
+
+ /**
+ * Called when the visible answer progress has changed. Implementations should use this for
+ * animation, but should not perform accepts or rejects until {@link #onMoveFinish(boolean)} is
+ * called.
+ *
+ * @param progress float representation of the progress with +1f fully accepted, -1f fully
+ * rejected, and 0 neutral.
+ */
+ void onProgressChanged(@FloatRange(from = -1f, to = 1f) float progress);
+
+ /** Called when a touch event has started being tracked. */
+ void onTrackingStart();
+
+ /** Called when touch events stop being tracked. */
+ void onTrackingStopped();
+
+ /**
+ * Called when the progress has fully animated back to neutral. Normal resting animation should
+ * resume, possibly with a hint animation first.
+ *
+ * @param showHint {@code true} iff the hint animation should be run before resuming normal
+ * animation.
+ */
+ void onMoveReset(boolean showHint);
+
+ /**
+ * Called when the progress has animated fully to accept or reject.
+ *
+ * @param accept {@code true} if the call has been accepted, {@code false} if it has been
+ * rejected.
+ */
+ void onMoveFinish(boolean accept);
+
+ /**
+ * Determine whether this gesture should use the {@link FalsingManager} to reject accidental
+ * touches
+ *
+ * @param downEvent the MotionEvent corresponding to the start of the gesture
+ * @return {@code true} if the {@link FalsingManager} should be used to reject accidental
+ * touches for this gesture
+ */
+ boolean shouldUseFalsing(@NonNull MotionEvent downEvent);
+ }
+
+ // Progress that must be moved through to not show the hint animation after gesture completes
+ private static final float HINT_MOVE_THRESHOLD_RATIO = .1f;
+ // Dp touch needs to move upward to be considered fully accepted
+ private static final int ACCEPT_THRESHOLD_DP = 150;
+ // Dp touch needs to move downward to be considered fully rejected
+ private static final int REJECT_THRESHOLD_DP = 150;
+ // Dp touch needs to move for it to not be considered a false touch (if FalsingManager is not
+ // enabled)
+ private static final int FALSING_THRESHOLD_DP = 40;
+
+ // Progress at which a fling in the opposite direction will recenter instead of
+ // accepting/rejecting
+ private static final float PROGRESS_FLING_RECENTER = .1f;
+
+ // Progress at which a slow swipe would continue toward accept/reject after the
+ // touch has been let go, otherwise will recenter
+ private static final float PROGRESS_SWIPE_RECENTER = .8f;
+
+ private static final float REJECT_FLING_THRESHOLD_MODIFIER = 2f;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FlingTarget.CENTER, FlingTarget.ACCEPT, FlingTarget.REJECT})
+ private @interface FlingTarget {
+ int CENTER = 0;
+ int ACCEPT = 1;
+ int REJECT = -1;
+ }
+
+ /**
+ * Create a new FlingUpDownTouchHandler and attach it to the target. Will call {@link
+ * View#setOnTouchListener(OnTouchListener)} before returning.
+ *
+ * @param target View whose touches are to be listened to
+ * @param listener Callback to listen to major events
+ * @param falsingManager FalsingManager to identify false touches
+ * @return the instance of FlingUpDownTouchHandler that has been added as a touch listener
+ */
+ public static FlingUpDownTouchHandler attach(
+ @NonNull View target,
+ @NonNull OnProgressChangedListener listener,
+ @Nullable FalsingManager falsingManager) {
+ FlingUpDownTouchHandler handler = new FlingUpDownTouchHandler(target, listener, falsingManager);
+ target.setOnTouchListener(handler);
+ return handler;
+ }
+
+ @NonNull private final View target;
+ @NonNull private final OnProgressChangedListener listener;
+
+ private VelocityTracker velocityTracker;
+ private FlingAnimationUtils flingAnimationUtils;
+
+ private boolean touchEnabled = true;
+ private boolean flingEnabled = true;
+ private float currentProgress;
+ private boolean tracking;
+
+ private boolean motionAborted;
+ private boolean touchSlopExceeded;
+ private boolean hintDistanceExceeded;
+ private int trackingPointer;
+ private Animator progressAnimator;
+
+ private float touchSlop;
+ private float initialTouchY;
+ private float acceptThresholdY;
+ private float rejectThresholdY;
+ private float zeroY;
+
+ private boolean touchAboveFalsingThreshold;
+ private float falsingThresholdPx;
+ private boolean touchUsesFalsing;
+
+ private final float acceptThresholdPx;
+ private final float rejectThresholdPx;
+ private final float deadZoneTopPx;
+
+ @Nullable private final FalsingManager falsingManager;
+
+ private FlingUpDownTouchHandler(
+ @NonNull View target,
+ @NonNull OnProgressChangedListener listener,
+ @Nullable FalsingManager falsingManager) {
+ this.target = target;
+ this.listener = listener;
+ Context context = target.getContext();
+ touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ flingAnimationUtils = new FlingAnimationUtils(context, .6f);
+ falsingThresholdPx = DpUtil.dpToPx(context, FALSING_THRESHOLD_DP);
+ acceptThresholdPx = DpUtil.dpToPx(context, ACCEPT_THRESHOLD_DP);
+ rejectThresholdPx = DpUtil.dpToPx(context, REJECT_THRESHOLD_DP);
+
+ deadZoneTopPx =
+ Math.max(
+ context.getResources().getDimension(R.dimen.answer_swipe_dead_zone_top),
+ acceptThresholdPx);
+ this.falsingManager = falsingManager;
+ }
+
+ /** Returns {@code true} iff a touch is being tracked */
+ public boolean isTracking() {
+ return tracking;
+ }
+
+ /**
+ * Sets whether touch events will continue to be listened to
+ *
+ * @param touchEnabled whether future touch events will be listened to
+ */
+ public void setTouchEnabled(boolean touchEnabled) {
+ this.touchEnabled = touchEnabled;
+ }
+
+ /**
+ * Sets whether fling velocity is used to affect accept/reject behavior
+ *
+ * @param flingEnabled whether fling velocity will be used when determining whether to
+ * accept/reject or recenter
+ */
+ public void setFlingEnabled(boolean flingEnabled) {
+ this.flingEnabled = flingEnabled;
+ }
+
+ public void detach() {
+ cancelProgressAnimator();
+ setTouchEnabled(false);
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (falsingManager != null) {
+ falsingManager.onTouchEvent(event);
+ }
+ if (!touchEnabled) {
+ return false;
+ }
+ if (motionAborted && (event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
+ return false;
+ }
+
+ int pointerIndex = event.findPointerIndex(trackingPointer);
+ if (pointerIndex < 0) {
+ pointerIndex = 0;
+ trackingPointer = event.getPointerId(pointerIndex);
+ }
+ final float pointerY = event.getY(pointerIndex);
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ if (pointerY < deadZoneTopPx) {
+ return false;
+ }
+ motionAborted = false;
+ startMotion(pointerY, false, currentProgress);
+ touchAboveFalsingThreshold = false;
+ touchUsesFalsing = listener.shouldUseFalsing(event);
+ if (velocityTracker == null) {
+ initVelocityTracker();
+ }
+ trackMovement(event);
+ cancelProgressAnimator();
+ touchSlopExceeded = progressAnimator != null;
+ onTrackingStarted();
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ final int upPointer = event.getPointerId(event.getActionIndex());
+ if (trackingPointer == upPointer) {
+ // gesture is ongoing, find a new pointer to track
+ int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+ float newY = event.getY(newIndex);
+ trackingPointer = event.getPointerId(newIndex);
+ startMotion(newY, true, currentProgress);
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ motionAborted = true;
+ endMotionEvent(event, pointerY, true);
+ return false;
+ case MotionEvent.ACTION_MOVE:
+ float deltaY = pointerY - initialTouchY;
+
+ if (Math.abs(deltaY) > touchSlop) {
+ touchSlopExceeded = true;
+ }
+ if (Math.abs(deltaY) >= falsingThresholdPx) {
+ touchAboveFalsingThreshold = true;
+ }
+ setCurrentProgress(pointerYToProgress(pointerY));
+ trackMovement(event);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ trackMovement(event);
+ endMotionEvent(event, pointerY, false);
+ }
+ return true;
+ }
+
+ private void endMotionEvent(MotionEvent event, float pointerY, boolean forceCancel) {
+ trackingPointer = -1;
+ if ((tracking && touchSlopExceeded)
+ || Math.abs(pointerY - initialTouchY) > touchSlop
+ || event.getActionMasked() == MotionEvent.ACTION_CANCEL
+ || forceCancel) {
+ float vel = 0f;
+ float vectorVel = 0f;
+ if (velocityTracker != null) {
+ velocityTracker.computeCurrentVelocity(1000);
+ vel = velocityTracker.getYVelocity();
+ vectorVel =
+ Math.copySign(
+ (float) Math.hypot(velocityTracker.getXVelocity(), velocityTracker.getYVelocity()),
+ vel);
+ }
+
+ boolean falseTouch = isFalseTouch();
+ boolean forceRecenter =
+ falseTouch
+ || !touchSlopExceeded
+ || forceCancel
+ || event.getActionMasked() == MotionEvent.ACTION_CANCEL;
+
+ @FlingTarget
+ int target = forceRecenter ? FlingTarget.CENTER : getFlingTarget(pointerY, vectorVel);
+
+ fling(vel, target, falseTouch);
+ onTrackingStopped();
+ } else {
+ onTrackingStopped();
+ setCurrentProgress(0);
+ onMoveEnded();
+ }
+
+ if (velocityTracker != null) {
+ velocityTracker.recycle();
+ velocityTracker = null;
+ }
+ }
+
+ @FlingTarget
+ private int getFlingTarget(float pointerY, float vectorVel) {
+ float progress = pointerYToProgress(pointerY);
+
+ float minVelocityPxPerSecond = flingAnimationUtils.getMinVelocityPxPerSecond();
+ if (vectorVel > 0) {
+ minVelocityPxPerSecond *= REJECT_FLING_THRESHOLD_MODIFIER;
+ }
+ if (!flingEnabled || Math.abs(vectorVel) < minVelocityPxPerSecond) {
+ // Not a fling
+ if (Math.abs(progress) > PROGRESS_SWIPE_RECENTER) {
+ // Progress near one of the edges
+ return progress > 0 ? FlingTarget.ACCEPT : FlingTarget.REJECT;
+ } else {
+ return FlingTarget.CENTER;
+ }
+ }
+
+ boolean sameDirection = vectorVel < 0 == progress > 0;
+ if (!sameDirection && Math.abs(progress) >= PROGRESS_FLING_RECENTER) {
+ // Being flung back toward center
+ return FlingTarget.CENTER;
+ }
+ // Flung toward an edge
+ return vectorVel < 0 ? FlingTarget.ACCEPT : FlingTarget.REJECT;
+ }
+
+ @FloatRange(from = -1f, to = 1f)
+ private float pointerYToProgress(float pointerY) {
+ boolean pointerAboveZero = pointerY > zeroY;
+ float nearestThreshold = pointerAboveZero ? rejectThresholdY : acceptThresholdY;
+
+ float absoluteProgress = (pointerY - zeroY) / (nearestThreshold - zeroY);
+ return MathUtil.clamp(absoluteProgress * (pointerAboveZero ? -1 : 1), -1f, 1f);
+ }
+
+ private boolean isFalseTouch() {
+ if (falsingManager != null && falsingManager.isEnabled()) {
+ if (falsingManager.isFalseTouch()) {
+ if (touchUsesFalsing) {
+ LogUtil.i("FlingUpDownTouchHandler.isFalseTouch", "rejecting false touch");
+ return true;
+ } else {
+ LogUtil.i(
+ "FlingUpDownTouchHandler.isFalseTouch",
+ "Suspected false touch, but not using false touch rejection for this gesture");
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ return !touchAboveFalsingThreshold;
+ }
+
+ private void trackMovement(MotionEvent event) {
+ if (velocityTracker != null) {
+ velocityTracker.addMovement(event);
+ }
+ }
+
+ private void fling(float velocity, @FlingTarget int target, boolean centerBecauseOfFalsing) {
+ ValueAnimator animator = createProgressAnimator(target);
+ if (target == FlingTarget.CENTER) {
+ flingAnimationUtils.apply(animator, currentProgress, target, velocity);
+ } else {
+ flingAnimationUtils.applyDismissing(animator, currentProgress, target, velocity, 1);
+ }
+ if (target == FlingTarget.CENTER && centerBecauseOfFalsing) {
+ velocity = 0;
+ }
+ if (velocity == 0) {
+ animator.setDuration(350);
+ }
+
+ animator.addListener(
+ new AnimatorListenerAdapter() {
+ boolean canceled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ canceled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ progressAnimator = null;
+ if (!canceled) {
+ onMoveEnded();
+ }
+ }
+ });
+ progressAnimator = animator;
+ animator.start();
+ }
+
+ private void onMoveEnded() {
+ if (currentProgress == 0) {
+ listener.onMoveReset(!hintDistanceExceeded);
+ } else {
+ listener.onMoveFinish(currentProgress > 0);
+ }
+ }
+
+ private ValueAnimator createProgressAnimator(float targetProgress) {
+ ValueAnimator animator = ValueAnimator.ofFloat(currentProgress, targetProgress);
+ animator.addUpdateListener(
+ new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ setCurrentProgress((Float) animation.getAnimatedValue());
+ }
+ });
+ return animator;
+ }
+
+ private void initVelocityTracker() {
+ if (velocityTracker != null) {
+ velocityTracker.recycle();
+ }
+ velocityTracker = VelocityTracker.obtain();
+ }
+
+ private void startMotion(float newY, boolean startTracking, float startProgress) {
+ initialTouchY = newY;
+ hintDistanceExceeded = false;
+
+ if (startProgress <= .25) {
+ acceptThresholdY = Math.max(0, initialTouchY - acceptThresholdPx);
+ rejectThresholdY = Math.min(target.getHeight(), initialTouchY + rejectThresholdPx);
+ zeroY = initialTouchY;
+ }
+
+ if (startTracking) {
+ touchSlopExceeded = true;
+ onTrackingStarted();
+ setCurrentProgress(startProgress);
+ }
+ }
+
+ private void onTrackingStarted() {
+ tracking = true;
+ listener.onTrackingStart();
+ }
+
+ private void onTrackingStopped() {
+ tracking = false;
+ listener.onTrackingStopped();
+ }
+
+ private void cancelProgressAnimator() {
+ if (progressAnimator != null) {
+ progressAnimator.cancel();
+ }
+ }
+
+ private void setCurrentProgress(float progress) {
+ if (Math.abs(progress) > HINT_MOVE_THRESHOLD_RATIO) {
+ hintDistanceExceeded = true;
+ }
+ currentProgress = progress;
+ listener.onProgressChanged(progress);
+ }
+}