1 /* 2 * Copyright (C) 2021 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 package com.android.quickstep.inputconsumers; 17 18 import static android.view.MotionEvent.ACTION_CANCEL; 19 import static android.view.MotionEvent.ACTION_MOVE; 20 import static android.view.MotionEvent.ACTION_UP; 21 import static android.view.MotionEvent.INVALID_POINTER_ID; 22 23 import static com.android.launcher3.Flags.enableCursorHoverStates; 24 import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; 25 import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent; 26 import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TOUCHING; 27 28 import android.content.Context; 29 import android.content.res.Resources; 30 import android.graphics.PointF; 31 import android.graphics.Rect; 32 import android.os.Handler; 33 import android.os.Looper; 34 import android.view.InputDevice; 35 import android.view.MotionEvent; 36 import android.view.VelocityTracker; 37 import android.view.ViewConfiguration; 38 39 import androidx.annotation.Nullable; 40 41 import com.android.launcher3.DeviceProfile; 42 import com.android.launcher3.R; 43 import com.android.launcher3.taskbar.TaskbarActivityContext; 44 import com.android.launcher3.taskbar.TaskbarThresholdUtils; 45 import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback; 46 import com.android.launcher3.taskbar.bubbles.BubbleControllers; 47 import com.android.launcher3.touch.OverScroll; 48 import com.android.launcher3.util.DisplayController; 49 import com.android.quickstep.GestureState; 50 import com.android.quickstep.InputConsumer; 51 import com.android.quickstep.OverviewCommandHelper; 52 import com.android.systemui.shared.system.InputMonitorCompat; 53 54 /** 55 * Listens for touch (swipe) and hover events to unstash the Taskbar. 56 */ 57 public class TaskbarUnstashInputConsumer extends DelegateInputConsumer { 58 59 private static final int HOVER_TASKBAR_UNSTASH_TIMEOUT = 500; 60 61 private static final int NUM_MOTION_MOVE_THRESHOLD = 3; 62 63 private static final Handler sUnstashHandler = new Handler(Looper.getMainLooper()); 64 65 private final TaskbarActivityContext mTaskbarActivityContext; 66 private final OverviewCommandHelper mOverviewCommandHelper; 67 private final float mUnstashArea; 68 private final int mTaskbarNavThreshold; 69 private final int mTaskbarNavThresholdY; 70 private final boolean mIsTaskbarAllAppsOpen; 71 private boolean mHasPassedTaskbarNavThreshold; 72 private boolean mIsInBubbleBarArea; 73 private boolean mIsVerticalGestureOverBubbleBar; 74 private boolean mIsPassedBubbleBarSlop; 75 private final int mTouchSlop; 76 77 private final PointF mDownPos = new PointF(); 78 private final PointF mLastPos = new PointF(); 79 private int mActivePointerId = INVALID_POINTER_ID; 80 81 private final boolean mIsTransientTaskbar; 82 83 private boolean mIsStashedTaskbarHovered = false; 84 private final Rect mStashedTaskbarHandleBounds = new Rect(); 85 private final Rect mBottomEdgeBounds = new Rect(); 86 private final int mBottomScreenEdge; 87 private final int mStashedTaskbarBottomEdge; 88 89 private final @Nullable TransitionCallback mTransitionCallback; 90 private final GestureState mGestureState; 91 private VelocityTracker mVelocityTracker; 92 private boolean mCanPlayTaskbarBgAlphaAnimation = true; 93 private int mMotionMoveCount = 0; 94 // Velocity defined as dp per s 95 private float mTaskbarSlowVelocityYThreshold; 96 TaskbarUnstashInputConsumer(Context context, InputConsumer delegate, InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext, OverviewCommandHelper overviewCommandHelper, GestureState gestureState)97 public TaskbarUnstashInputConsumer(Context context, InputConsumer delegate, 98 InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext, 99 OverviewCommandHelper overviewCommandHelper, GestureState gestureState) { 100 super(delegate, inputMonitor); 101 mTaskbarActivityContext = taskbarActivityContext; 102 mOverviewCommandHelper = overviewCommandHelper; 103 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 104 105 Resources res = context.getResources(); 106 mUnstashArea = res.getDimensionPixelSize(R.dimen.taskbar_unstash_input_area); 107 mTaskbarNavThreshold = TaskbarThresholdUtils.getFromNavThreshold(res, 108 taskbarActivityContext.getDeviceProfile()); 109 mTaskbarNavThresholdY = taskbarActivityContext.getDeviceProfile().heightPx 110 - mTaskbarNavThreshold; 111 mIsTaskbarAllAppsOpen = mTaskbarActivityContext.isTaskbarAllAppsOpen(); 112 113 mIsTransientTaskbar = DisplayController.isTransientTaskbar(context); 114 mTaskbarSlowVelocityYThreshold = 115 res.getDimensionPixelSize(R.dimen.taskbar_slow_velocity_y_threshold); 116 117 mBottomScreenEdge = res.getDimensionPixelSize( 118 R.dimen.taskbar_stashed_screen_edge_hover_deadzone_height); 119 mStashedTaskbarBottomEdge = 120 res.getDimensionPixelSize(R.dimen.taskbar_stashed_below_hover_deadzone_height); 121 122 mTransitionCallback = mIsTransientTaskbar 123 ? taskbarActivityContext.getTranslationCallbacks() 124 : null; 125 mGestureState = gestureState; 126 } 127 128 @Override getType()129 public int getType() { 130 return TYPE_TASKBAR_STASH | TYPE_CURSOR_HOVER | mDelegate.getType(); 131 } 132 133 @Override allowInterceptByParent()134 public boolean allowInterceptByParent() { 135 return super.allowInterceptByParent() && !mHasPassedTaskbarNavThreshold; 136 } 137 138 @Override onMotionEvent(MotionEvent ev)139 public void onMotionEvent(MotionEvent ev) { 140 if (enableScalingRevealHomeAnimation() && mIsTransientTaskbar) { 141 checkVelocityForTaskbarBackground(ev); 142 } 143 if (mState != STATE_ACTIVE) { 144 boolean isStashedTaskbarHovered = isMouseEvent(ev) 145 && isStashedTaskbarHovered((int) ev.getX(), (int) ev.getY()); 146 // Only show the transient task bar if the touch events are on the screen. 147 if (!isTrackpadMotionEvent(ev)) { 148 final float x = ev.getRawX(); 149 final float y = ev.getRawY(); 150 switch (ev.getAction()) { 151 case MotionEvent.ACTION_DOWN: 152 mActivePointerId = ev.getPointerId(0); 153 mDownPos.set(ev.getX(), ev.getY()); 154 mLastPos.set(mDownPos); 155 156 mHasPassedTaskbarNavThreshold = false; 157 mTaskbarActivityContext.setAutohideSuspendFlag( 158 FLAG_AUTOHIDE_SUSPEND_TOUCHING, true); 159 if (mTransitionCallback != null && !mIsTaskbarAllAppsOpen) { 160 mTransitionCallback.onActionDown(); 161 } 162 if (mIsTransientTaskbar && isInBubbleBarArea(x)) { 163 mIsInBubbleBarArea = true; 164 } 165 break; 166 case MotionEvent.ACTION_POINTER_UP: 167 int ptrIdx = ev.getActionIndex(); 168 int ptrId = ev.getPointerId(ptrIdx); 169 if (ptrId == mActivePointerId) { 170 final int newPointerIdx = ptrIdx == 0 ? 1 : 0; 171 mDownPos.set( 172 ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x), 173 ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y)); 174 mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx)); 175 mActivePointerId = ev.getPointerId(newPointerIdx); 176 } 177 break; 178 case MotionEvent.ACTION_MOVE: 179 int pointerIndex = ev.findPointerIndex(mActivePointerId); 180 if (pointerIndex == INVALID_POINTER_ID) { 181 break; 182 } 183 mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); 184 185 float dX = mLastPos.x - mDownPos.x; 186 float dY = mLastPos.y - mDownPos.y; 187 188 if (!mIsPassedBubbleBarSlop && mIsInBubbleBarArea) { 189 boolean passedSlop = 190 Math.abs(dY) > mTouchSlop || Math.abs(dX) > mTouchSlop; 191 if (passedSlop) { 192 mIsPassedBubbleBarSlop = true; 193 mIsVerticalGestureOverBubbleBar = Math.abs(dY) > Math.abs(dX); 194 if (mIsVerticalGestureOverBubbleBar) { 195 setActive(ev); 196 } 197 } 198 } 199 200 if (mIsTransientTaskbar) { 201 boolean passedTaskbarNavThreshold = dY < 0 202 && Math.abs(dY) >= mTaskbarNavThreshold; 203 204 if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold 205 && !mGestureState.isInExtendedSlopRegion()) { 206 mHasPassedTaskbarNavThreshold = true; 207 if (mIsInBubbleBarArea && mIsVerticalGestureOverBubbleBar) { 208 mTaskbarActivityContext.onSwipeToOpenBubblebar(); 209 } else { 210 mTaskbarActivityContext.onSwipeToUnstashTaskbar(); 211 } 212 } 213 214 if (dY < 0) { 215 dY = -OverScroll.dampedScroll(-dY, mTaskbarNavThresholdY); 216 if (mTransitionCallback != null && !mIsTaskbarAllAppsOpen) { 217 mTransitionCallback.onActionMove(dY); 218 } 219 } 220 } 221 break; 222 case MotionEvent.ACTION_UP: 223 case MotionEvent.ACTION_CANCEL: 224 cleanupAfterMotionEvent(); 225 break; 226 case MotionEvent.ACTION_BUTTON_RELEASE: 227 if (isStashedTaskbarHovered) { 228 mOverviewCommandHelper.addCommand(OverviewCommandHelper.TYPE_HOME); 229 } 230 break; 231 } 232 } 233 boolean isMovingInBubbleBarArea = mIsInBubbleBarArea && ev.getAction() == ACTION_MOVE; 234 if (!isStashedTaskbarHovered) { 235 // if we're moving in the bubble bar area but we haven't passed the slop yet, don't 236 // propagate to the delegate, until we can determine the direction of the gesture. 237 if (!isMovingInBubbleBarArea || mIsPassedBubbleBarSlop) { 238 mDelegate.onMotionEvent(ev); 239 } 240 } 241 } else if (mIsVerticalGestureOverBubbleBar) { 242 // if we get here then this gesture is a vertical swipe over the bubble bar. 243 // we're also active and there's no need to delegate any additional motion events. the 244 // rest of the gesture will be handled here. 245 switch (ev.getAction()) { 246 case ACTION_MOVE: 247 int pointerIndex = ev.findPointerIndex(mActivePointerId); 248 if (pointerIndex == INVALID_POINTER_ID) { 249 break; 250 } 251 mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); 252 253 float dY = mLastPos.y - mDownPos.y; 254 255 // bubble bar swipe gesture uses the same threshold as the taskbar. 256 boolean passedTaskbarNavThreshold = dY < 0 257 && Math.abs(dY) >= mTaskbarNavThreshold; 258 259 if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold) { 260 mHasPassedTaskbarNavThreshold = true; 261 mTaskbarActivityContext.onSwipeToOpenBubblebar(); 262 } 263 break; 264 case ACTION_UP: 265 case ACTION_CANCEL: 266 cleanupAfterMotionEvent(); 267 break; 268 } 269 } 270 } 271 checkVelocityForTaskbarBackground(MotionEvent ev)272 private void checkVelocityForTaskbarBackground(MotionEvent ev) { 273 int actionMasked = ev.getActionMasked(); 274 if (actionMasked == MotionEvent.ACTION_DOWN && mVelocityTracker != null) { 275 mVelocityTracker.clear(); 276 } 277 if (mVelocityTracker == null) { 278 mVelocityTracker = VelocityTracker.obtain(); 279 } 280 mVelocityTracker.addMovement(ev); 281 282 mVelocityTracker.computeCurrentVelocity(1000); 283 if (ev.getAction() == ACTION_MOVE) { 284 mMotionMoveCount++; 285 } 286 287 float velocityYPxPerS = mVelocityTracker.getYVelocity(); 288 if (mCanPlayTaskbarBgAlphaAnimation 289 && mMotionMoveCount >= NUM_MOTION_MOVE_THRESHOLD // Arbitrary value 290 && velocityYPxPerS != 0 // Ignore these 291 && velocityYPxPerS >= mTaskbarSlowVelocityYThreshold) { 292 mTaskbarActivityContext.playTaskbarBackgroundAlphaAnimation(); 293 mCanPlayTaskbarBgAlphaAnimation = false; 294 } 295 } 296 cleanupAfterMotionEvent()297 private void cleanupAfterMotionEvent() { 298 mTaskbarActivityContext.setAutohideSuspendFlag( 299 FLAG_AUTOHIDE_SUSPEND_TOUCHING, false); 300 if (mTransitionCallback != null) { 301 mTransitionCallback.onActionEnd(); 302 } 303 mHasPassedTaskbarNavThreshold = false; 304 mIsInBubbleBarArea = false; 305 mIsVerticalGestureOverBubbleBar = false; 306 mIsPassedBubbleBarSlop = false; 307 308 if (mVelocityTracker != null) { 309 mVelocityTracker.recycle(); 310 } 311 mVelocityTracker = null; 312 mCanPlayTaskbarBgAlphaAnimation = true; 313 mMotionMoveCount = 0; 314 } 315 isInBubbleBarArea(float x)316 private boolean isInBubbleBarArea(float x) { 317 if (mTaskbarActivityContext == null || !mIsTransientTaskbar) { 318 return false; 319 } 320 BubbleControllers controllers = mTaskbarActivityContext.getBubbleControllers(); 321 if (controllers == null) { 322 return false; 323 } 324 if (controllers.bubbleStashController.isStashed()) { 325 return controllers.bubbleStashedHandleViewController.containsX((int) x); 326 } else { 327 Rect bubbleBarBounds = controllers.bubbleBarViewController.getBubbleBarBounds(); 328 return x >= bubbleBarBounds.left && x <= bubbleBarBounds.right; 329 } 330 } 331 332 /** 333 * Listen for hover events for the stashed taskbar. 334 * 335 * <p>When hovered over the stashed taskbar handle, show the unstash hint. 336 * <p>When the cursor is touching the bottom edge below the stashed taskbar, unstash it. 337 * <p>When the cursor is within a defined threshold of the screen's bottom edge outside of 338 * the stashed taskbar, unstash it. 339 */ 340 @Override onHoverEvent(MotionEvent ev)341 public void onHoverEvent(MotionEvent ev) { 342 if (!enableCursorHoverStates() || mTaskbarActivityContext == null 343 || !mTaskbarActivityContext.isTaskbarStashed()) { 344 return; 345 } 346 347 if (mIsStashedTaskbarHovered) { 348 updateHoveredTaskbarState((int) ev.getX(), (int) ev.getY()); 349 } else { 350 updateUnhoveredTaskbarState((int) ev.getX(), (int) ev.getY()); 351 } 352 } 353 updateHoveredTaskbarState(int x, int y)354 private void updateHoveredTaskbarState(int x, int y) { 355 DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile(); 356 mBottomEdgeBounds.set( 357 (dp.widthPx - (int) mUnstashArea) / 2, 358 dp.heightPx - mStashedTaskbarBottomEdge, 359 (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea), 360 dp.heightPx); 361 362 if (mBottomEdgeBounds.contains(x, y)) { 363 // start a single unstash timeout if hovering bottom edge under the hinted taskbar. 364 if (!sUnstashHandler.hasMessagesOrCallbacks()) { 365 sUnstashHandler.postDelayed(() -> { 366 mTaskbarActivityContext.onSwipeToUnstashTaskbar(); 367 mIsStashedTaskbarHovered = false; 368 }, HOVER_TASKBAR_UNSTASH_TIMEOUT); 369 } 370 } else if (!isStashedTaskbarHovered(x, y)) { 371 // If exit hovering stashed taskbar, remove hint and clear pending unstash calls. 372 sUnstashHandler.removeCallbacksAndMessages(null); 373 startStashedTaskbarHover(/* isHovered = */ false); 374 } else { 375 sUnstashHandler.removeCallbacksAndMessages(null); 376 } 377 } 378 updateUnhoveredTaskbarState(int x, int y)379 private void updateUnhoveredTaskbarState(int x, int y) { 380 sUnstashHandler.removeCallbacksAndMessages(null); 381 382 DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile(); 383 mBottomEdgeBounds.set( 384 0, 385 dp.heightPx - mBottomScreenEdge, 386 dp.widthPx, 387 dp.heightPx); 388 389 if (isStashedTaskbarHovered(x, y)) { 390 // If enter hovering stashed taskbar, start hint. 391 startStashedTaskbarHover(/* isHovered = */ true); 392 } else if (mBottomEdgeBounds.contains(x, y)) { 393 // If hover screen's bottom edge not below the stashed taskbar, unstash it. 394 mTaskbarActivityContext.onSwipeToUnstashTaskbar(); 395 } 396 } 397 startStashedTaskbarHover(boolean isHovered)398 private void startStashedTaskbarHover(boolean isHovered) { 399 mTaskbarActivityContext.startTaskbarUnstashHint(isHovered); 400 mIsStashedTaskbarHovered = isHovered; 401 } 402 isStashedTaskbarHovered(int x, int y)403 private boolean isStashedTaskbarHovered(int x, int y) { 404 if (!mTaskbarActivityContext.isTaskbarStashed() 405 || mTaskbarActivityContext.isTaskbarAllAppsOpen() 406 || !enableCursorHoverStates()) { 407 return false; 408 } 409 DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile(); 410 mStashedTaskbarHandleBounds.set( 411 (dp.widthPx - (int) mUnstashArea) / 2, 412 dp.heightPx - dp.stashedTaskbarHeight, 413 (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea), 414 dp.heightPx); 415 return mStashedTaskbarHandleBounds.contains(x, y); 416 } 417 isMouseEvent(MotionEvent event)418 private boolean isMouseEvent(MotionEvent event) { 419 return event.getSource() == InputDevice.SOURCE_MOUSE; 420 } 421 422 @Override getDelegatorName()423 protected String getDelegatorName() { 424 return "TaskbarUnstashInputConsumer"; 425 } 426 } 427