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