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.systemui.pip.phone; 18 19 import android.graphics.PointF; 20 import android.os.Handler; 21 import android.util.Log; 22 import android.view.MotionEvent; 23 import android.view.VelocityTracker; 24 import android.view.ViewConfiguration; 25 26 import com.android.internal.annotations.VisibleForTesting; 27 28 import java.io.PrintWriter; 29 30 /** 31 * This keeps track of the touch state throughout the current touch gesture. 32 */ 33 public class PipTouchState { 34 private static final String TAG = "PipTouchHandler"; 35 private static final boolean DEBUG = false; 36 37 @VisibleForTesting 38 static final long DOUBLE_TAP_TIMEOUT = 200; 39 40 private final Handler mHandler; 41 private final ViewConfiguration mViewConfig; 42 private final Runnable mDoubleTapTimeoutCallback; 43 44 private VelocityTracker mVelocityTracker; 45 private long mDownTouchTime = 0; 46 private long mLastDownTouchTime = 0; 47 private long mUpTouchTime = 0; 48 private final PointF mDownTouch = new PointF(); 49 private final PointF mDownDelta = new PointF(); 50 private final PointF mLastTouch = new PointF(); 51 private final PointF mLastDelta = new PointF(); 52 private final PointF mVelocity = new PointF(); 53 private boolean mAllowTouches = true; 54 private boolean mIsUserInteracting = false; 55 // Set to true only if the multiple taps occur within the double tap timeout 56 private boolean mIsDoubleTap = false; 57 // Set to true only if a gesture 58 private boolean mIsWaitingForDoubleTap = false; 59 private boolean mIsDragging = false; 60 // The previous gesture was a drag 61 private boolean mPreviouslyDragging = false; 62 private boolean mStartedDragging = false; 63 private boolean mAllowDraggingOffscreen = false; 64 private int mActivePointerId; 65 PipTouchState(ViewConfiguration viewConfig, Handler handler, Runnable doubleTapTimeoutCallback)66 public PipTouchState(ViewConfiguration viewConfig, Handler handler, 67 Runnable doubleTapTimeoutCallback) { 68 mViewConfig = viewConfig; 69 mHandler = handler; 70 mDoubleTapTimeoutCallback = doubleTapTimeoutCallback; 71 } 72 73 /** 74 * Resets this state. 75 */ reset()76 public void reset() { 77 mAllowDraggingOffscreen = false; 78 mIsDragging = false; 79 mStartedDragging = false; 80 mIsUserInteracting = false; 81 } 82 83 /** 84 * Processes a given touch event and updates the state. 85 */ onTouchEvent(MotionEvent ev)86 public void onTouchEvent(MotionEvent ev) { 87 switch (ev.getActionMasked()) { 88 case MotionEvent.ACTION_DOWN: { 89 if (!mAllowTouches) { 90 return; 91 } 92 93 // Initialize the velocity tracker 94 initOrResetVelocityTracker(); 95 addMovement(ev); 96 97 mActivePointerId = ev.getPointerId(0); 98 if (DEBUG) { 99 Log.e(TAG, "Setting active pointer id on DOWN: " + mActivePointerId); 100 } 101 mLastTouch.set(ev.getRawX(), ev.getRawY()); 102 mDownTouch.set(mLastTouch); 103 mAllowDraggingOffscreen = true; 104 mIsUserInteracting = true; 105 mDownTouchTime = ev.getEventTime(); 106 mIsDoubleTap = !mPreviouslyDragging && 107 (mDownTouchTime - mLastDownTouchTime) < DOUBLE_TAP_TIMEOUT; 108 mIsWaitingForDoubleTap = false; 109 mLastDownTouchTime = mDownTouchTime; 110 if (mDoubleTapTimeoutCallback != null) { 111 mHandler.removeCallbacks(mDoubleTapTimeoutCallback); 112 } 113 break; 114 } 115 case MotionEvent.ACTION_MOVE: { 116 // Skip event if we did not start processing this touch gesture 117 if (!mIsUserInteracting) { 118 break; 119 } 120 121 // Update the velocity tracker 122 addMovement(ev); 123 int pointerIndex = ev.findPointerIndex(mActivePointerId); 124 if (pointerIndex == -1) { 125 Log.e(TAG, "Invalid active pointer id on MOVE: " + mActivePointerId); 126 break; 127 } 128 129 float x = ev.getRawX(pointerIndex); 130 float y = ev.getRawY(pointerIndex); 131 mLastDelta.set(x - mLastTouch.x, y - mLastTouch.y); 132 mDownDelta.set(x - mDownTouch.x, y - mDownTouch.y); 133 134 boolean hasMovedBeyondTap = mDownDelta.length() > mViewConfig.getScaledTouchSlop(); 135 if (!mIsDragging) { 136 if (hasMovedBeyondTap) { 137 mIsDragging = true; 138 mStartedDragging = true; 139 } 140 } else { 141 mStartedDragging = false; 142 } 143 mLastTouch.set(x, y); 144 break; 145 } 146 case MotionEvent.ACTION_POINTER_UP: { 147 // Skip event if we did not start processing this touch gesture 148 if (!mIsUserInteracting) { 149 break; 150 } 151 152 // Update the velocity tracker 153 addMovement(ev); 154 155 int pointerIndex = ev.getActionIndex(); 156 int pointerId = ev.getPointerId(pointerIndex); 157 if (pointerId == mActivePointerId) { 158 // Select a new active pointer id and reset the movement state 159 final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; 160 mActivePointerId = ev.getPointerId(newPointerIndex); 161 if (DEBUG) { 162 Log.e(TAG, "Relinquish active pointer id on POINTER_UP: " + 163 mActivePointerId); 164 } 165 mLastTouch.set(ev.getRawX(newPointerIndex), ev.getRawY(newPointerIndex)); 166 } 167 break; 168 } 169 case MotionEvent.ACTION_UP: { 170 // Skip event if we did not start processing this touch gesture 171 if (!mIsUserInteracting) { 172 break; 173 } 174 175 // Update the velocity tracker 176 addMovement(ev); 177 mVelocityTracker.computeCurrentVelocity(1000, 178 mViewConfig.getScaledMaximumFlingVelocity()); 179 mVelocity.set(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); 180 181 int pointerIndex = ev.findPointerIndex(mActivePointerId); 182 if (pointerIndex == -1) { 183 Log.e(TAG, "Invalid active pointer id on UP: " + mActivePointerId); 184 break; 185 } 186 187 mUpTouchTime = ev.getEventTime(); 188 mLastTouch.set(ev.getRawX(pointerIndex), ev.getRawY(pointerIndex)); 189 mPreviouslyDragging = mIsDragging; 190 mIsWaitingForDoubleTap = !mIsDoubleTap && !mIsDragging && 191 (mUpTouchTime - mDownTouchTime) < DOUBLE_TAP_TIMEOUT; 192 193 // Fall through to clean up 194 } 195 case MotionEvent.ACTION_CANCEL: { 196 recycleVelocityTracker(); 197 break; 198 } 199 } 200 } 201 202 /** 203 * @return the velocity of the active touch pointer at the point it is lifted off the screen. 204 */ 205 public PointF getVelocity() { 206 return mVelocity; 207 } 208 209 /** 210 * @return the last touch position of the active pointer. 211 */ 212 public PointF getLastTouchPosition() { 213 return mLastTouch; 214 } 215 216 /** 217 * @return the movement delta between the last handled touch event and the previous touch 218 * position. 219 */ 220 public PointF getLastTouchDelta() { 221 return mLastDelta; 222 } 223 224 /** 225 * @return the down touch position. 226 */ 227 public PointF getDownTouchPosition() { 228 return mDownTouch; 229 } 230 231 /** 232 * @return the movement delta between the last handled touch event and the down touch 233 * position. 234 */ 235 public PointF getDownTouchDelta() { 236 return mDownDelta; 237 } 238 239 /** 240 * @return whether the user has started dragging. 241 */ 242 public boolean isDragging() { 243 return mIsDragging; 244 } 245 246 /** 247 * @return whether the user is currently interacting with the PiP. 248 */ 249 public boolean isUserInteracting() { 250 return mIsUserInteracting; 251 } 252 253 /** 254 * @return whether the user has started dragging just in the last handled touch event. 255 */ 256 public boolean startedDragging() { 257 return mStartedDragging; 258 } 259 260 /** 261 * Sets whether touching is currently allowed. 262 */ 263 public void setAllowTouches(boolean allowTouches) { 264 mAllowTouches = allowTouches; 265 266 // If the user happens to touch down before this is sent from the system during a transition 267 // then block any additional handling by resetting the state now 268 if (mIsUserInteracting) { 269 reset(); 270 } 271 } 272 273 /** 274 * Disallows dragging offscreen for the duration of the current gesture. 275 */ 276 public void setDisallowDraggingOffscreen() { 277 mAllowDraggingOffscreen = false; 278 } 279 280 /** 281 * @return whether dragging offscreen is allowed during this gesture. 282 */ 283 public boolean allowDraggingOffscreen() { 284 return mAllowDraggingOffscreen; 285 } 286 287 /** 288 * @return whether this gesture is a double-tap. 289 */ 290 public boolean isDoubleTap() { 291 return mIsDoubleTap; 292 } 293 294 /** 295 * @return whether this gesture will potentially lead to a following double-tap. 296 */ 297 public boolean isWaitingForDoubleTap() { 298 return mIsWaitingForDoubleTap; 299 } 300 301 /** 302 * Schedules the callback to run if the next double tap does not occur. Only runs if 303 * isWaitingForDoubleTap() is true. 304 */ 305 public void scheduleDoubleTapTimeoutCallback() { 306 if (mIsWaitingForDoubleTap) { 307 long delay = getDoubleTapTimeoutCallbackDelay(); 308 mHandler.removeCallbacks(mDoubleTapTimeoutCallback); 309 mHandler.postDelayed(mDoubleTapTimeoutCallback, delay); 310 } 311 } 312 313 @VisibleForTesting long getDoubleTapTimeoutCallbackDelay() { 314 if (mIsWaitingForDoubleTap) { 315 return Math.max(0, DOUBLE_TAP_TIMEOUT - (mUpTouchTime - mDownTouchTime)); 316 } 317 return -1; 318 } 319 320 private void initOrResetVelocityTracker() { 321 if (mVelocityTracker == null) { 322 mVelocityTracker = VelocityTracker.obtain(); 323 } else { 324 mVelocityTracker.clear(); 325 } 326 } 327 328 private void recycleVelocityTracker() { 329 if (mVelocityTracker != null) { 330 mVelocityTracker.recycle(); 331 mVelocityTracker = null; 332 } 333 } 334 335 private void addMovement(MotionEvent event) { 336 // Add movement to velocity tracker using raw screen X and Y coordinates instead 337 // of window coordinates because the window frame may be moving at the same time. 338 float deltaX = event.getRawX() - event.getX(); 339 float deltaY = event.getRawY() - event.getY(); 340 event.offsetLocation(deltaX, deltaY); 341 mVelocityTracker.addMovement(event); 342 event.offsetLocation(-deltaX, -deltaY); 343 } 344 345 public void dump(PrintWriter pw, String prefix) { 346 final String innerPrefix = prefix + " "; 347 pw.println(prefix + TAG); 348 pw.println(innerPrefix + "mAllowTouches=" + mAllowTouches); 349 pw.println(innerPrefix + "mActivePointerId=" + mActivePointerId); 350 pw.println(innerPrefix + "mDownTouch=" + mDownTouch); 351 pw.println(innerPrefix + "mDownDelta=" + mDownDelta); 352 pw.println(innerPrefix + "mLastTouch=" + mLastTouch); 353 pw.println(innerPrefix + "mLastDelta=" + mLastDelta); 354 pw.println(innerPrefix + "mVelocity=" + mVelocity); 355 pw.println(innerPrefix + "mIsUserInteracting=" + mIsUserInteracting); 356 pw.println(innerPrefix + "mIsDragging=" + mIsDragging); 357 pw.println(innerPrefix + "mStartedDragging=" + mStartedDragging); 358 pw.println(innerPrefix + "mAllowDraggingOffscreen=" + mAllowDraggingOffscreen); 359 } 360 } 361