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