diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java index 109a4c5f79..54940529e0 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AssistantTouchConsumer.java @@ -22,59 +22,70 @@ import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_POINTER_UP; import static android.view.MotionEvent.ACTION_UP; +import android.animation.ValueAnimator; import android.content.Context; +import android.content.res.Resources; import android.graphics.PointF; -import android.graphics.Rect; import android.os.Bundle; import android.os.RemoteException; import android.os.SystemClock; import android.util.Log; -import android.view.Display; import android.view.MotionEvent; -import android.view.Surface; -import android.view.ViewConfiguration; -import android.view.WindowManager; +import com.android.launcher3.anim.Interpolators; +import com.android.quickstep.util.MotionPauseDetector; import com.android.systemui.shared.recents.ISystemUiProxy; -import com.android.systemui.shared.system.NavigationBarCompat; -import com.android.systemui.shared.system.WindowManagerWrapper; import com.android.launcher3.R; +import com.android.systemui.shared.system.NavigationBarCompat; /** * Touch consumer for handling events to launch assistant from launcher */ public class AssistantTouchConsumer implements InputConsumer { private static final String TAG = "AssistantTouchConsumer"; + private static final long RETRACT_ANIMATION_DURATION_MS = 300; + + /* The assistant touch consume competes with quick switch InputConsumer gesture. The delegate + * can be chosen to run if the angle passing the slop is lower than the threshold angle. When + * this occurs, the state changes to {@link #STATE_DELEGATE_ACTIVE} where the next incoming + * motion events are handled by the delegate instead of the assistant touch consumer. If the + * angle is higher than the threshold, the state will change to {@link #STATE_ASSISTANT_ACTIVE}. + */ + private static final int STATE_INACTIVE = 0; + private static final int STATE_ASSISTANT_ACTIVE = 1; + private static final int STATE_DELEGATE_ACTIVE = 2; private final PointF mDownPos = new PointF(); private final PointF mLastPos = new PointF(); + private final PointF mStartDragPos = new PointF(); + private int mActivePointerId = -1; - - private final int mDisplayRotation; - private final Rect mStableInsets = new Rect(); - - private final float mDragSlop; - private final float mTouchSlop; - private final float mThreshold; - - private float mStartDisplacement; - private boolean mPassedDragSlop; - private boolean mPassedTouchSlop; - private long mPassedTouchSlopTime; + private boolean mPassedSlop; private boolean mLaunchedAssistant; + private float mDistance; + private float mTimeFraction; + private long mDragTime; private float mLastProgress; + private int mState; + private final float mDistThreshold; + private final long mTimeThreshold; + private final int mAngleThreshold; + private final float mSlop; + private final MotionPauseDetector mMotionPauseDetector; private final ISystemUiProxy mSysUiProxy; + private final InputConsumer mConsumerDelegate; - public AssistantTouchConsumer(Context context, ISystemUiProxy systemUiProxy) { + public AssistantTouchConsumer(Context context, ISystemUiProxy systemUiProxy, + InputConsumer delegate) { + final Resources res = context.getResources(); mSysUiProxy = systemUiProxy; - - mDragSlop = NavigationBarCompat.getQuickStepDragSlopPx(); - mTouchSlop = NavigationBarCompat.getQuickStepTouchSlopPx(); - mThreshold = context.getResources().getDimension(R.dimen.gestures_assistant_threshold); - - Display display = context.getSystemService(WindowManager.class).getDefaultDisplay(); - mDisplayRotation = display.getRotation(); - WindowManagerWrapper.getInstance().getStableInsets(mStableInsets); + mConsumerDelegate = delegate; + mMotionPauseDetector = new MotionPauseDetector(context); + mDistThreshold = res.getDimension(R.dimen.gestures_assistant_drag_threshold); + mTimeThreshold = res.getInteger(R.integer.assistant_gesture_min_time_threshold); + mAngleThreshold = res.getInteger(R.integer.assistant_gesture_corner_deg_threshold); + mSlop = NavigationBarCompat.getQuickScrubTouchSlopPx(); + mState = STATE_INACTIVE; } @Override @@ -82,15 +93,29 @@ public class AssistantTouchConsumer implements InputConsumer { return TYPE_ASSISTANT; } + @Override + public boolean isActive() { + return mState != STATE_INACTIVE; + } + @Override public void onMotionEvent(MotionEvent ev) { // TODO add logging + switch (ev.getActionMasked()) { case ACTION_DOWN: { mActivePointerId = ev.getPointerId(0); mDownPos.set(ev.getX(), ev.getY()); mLastPos.set(mDownPos); - mLastProgress = -1; + mTimeFraction = 0; + + // Detect when the gesture decelerates to start the assistant + mMotionPauseDetector.setOnMotionPauseListener(isPaused -> { + if (isPaused && mState == STATE_ASSISTANT_ACTIVE) { + mTimeFraction = 1; + updateAssistantProgress(); + } + }); break; } case ACTION_POINTER_UP: { @@ -107,94 +132,100 @@ public class AssistantTouchConsumer implements InputConsumer { break; } case ACTION_MOVE: { + if (mState == STATE_DELEGATE_ACTIVE) { + break; + } int pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex == -1) { break; } mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); - float displacement = getDisplacement(ev); - if (!mPassedDragSlop) { - // Normal gesture, ensure we pass the drag slop before we start tracking - // the gesture - if (Math.abs(displacement) > mDragSlop) { - mPassedDragSlop = true; - mStartDisplacement = displacement; - mPassedTouchSlopTime = SystemClock.uptimeMillis(); - } - } + if (!mPassedSlop) { + // Normal gesture, ensure we pass the slop before we start tracking the gesture + if (Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) > mSlop) { + mPassedSlop = true; + mStartDragPos.set(mLastPos.x, mLastPos.y); + mDragTime = SystemClock.uptimeMillis(); - if (!mPassedTouchSlop) { - if (Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) >= - mTouchSlop) { - mPassedTouchSlop = true; - if (!mPassedDragSlop) { - mPassedDragSlop = true; - mStartDisplacement = displacement; - mPassedTouchSlopTime = SystemClock.uptimeMillis(); + // Determine if angle is larger than threshold for assistant detection + float angle = (float) Math.toDegrees( + Math.atan2(mDownPos.y - mLastPos.y, mDownPos.x - mLastPos.x)); + angle = angle > 90 ? 180 - angle : angle; + if (angle > mAngleThreshold) { + mState = STATE_ASSISTANT_ACTIVE; + + if (mConsumerDelegate != null) { + // Send cancel event + MotionEvent event = MotionEvent.obtain(ev); + event.setAction(MotionEvent.ACTION_CANCEL); + mConsumerDelegate.onMotionEvent(event); + } + } else { + mState = STATE_DELEGATE_ACTIVE; } } - } - - if (mPassedDragSlop) { - // Move - float distance = mStartDisplacement - displacement; - if (distance >= 0) { - onAssistantProgress(distance / mThreshold); + } else { + // Movement + mDistance = (float) Math.hypot(mLastPos.x - mStartDragPos.x, + mLastPos.y - mStartDragPos.y); + mMotionPauseDetector.addPosition(mDistance, 0); + if (mDistance >= 0) { + final long diff = SystemClock.uptimeMillis() - mDragTime; + mTimeFraction = Math.min(diff * 1f / mTimeThreshold, 1); + updateAssistantProgress(); } } break; } case ACTION_CANCEL: - break; - case ACTION_UP: { - if (ev.getEventTime() - mPassedTouchSlopTime < ViewConfiguration.getTapTimeout()) { - onAssistantProgress(1); + case ACTION_UP: + if (mState != STATE_DELEGATE_ACTIVE && !mLaunchedAssistant) { + ValueAnimator animator = ValueAnimator.ofFloat(mLastProgress, 0) + .setDuration(RETRACT_ANIMATION_DURATION_MS); + animator.addUpdateListener(valueAnimator -> { + float progress = (float) valueAnimator.getAnimatedValue(); + try { + mSysUiProxy.onAssistantProgress(progress); + } catch (RemoteException e) { + Log.w(TAG, "Failed to send SysUI start/send assistant progress: " + + progress, e); + } + }); + animator.setInterpolator(Interpolators.DEACCEL_2); + animator.start(); } - + mMotionPauseDetector.clear(); break; - } + } + + if (mState != STATE_ASSISTANT_ACTIVE && mConsumerDelegate != null) { + mConsumerDelegate.onMotionEvent(ev); } } - private void onAssistantProgress(float progress) { - if (mLastProgress == progress) { - return; - } - try { - mSysUiProxy.onAssistantProgress(Math.max(0, Math.min(1, progress))); - if (progress >= 1 && !mLaunchedAssistant) { - mSysUiProxy.startAssistant(new Bundle()); - mLaunchedAssistant = true; - } + private void updateAssistantProgress() { + if (!mLaunchedAssistant) { + float progress = Math.min(mDistance * 1f / mDistThreshold, 1) * mTimeFraction; mLastProgress = progress; - } catch (RemoteException e) { - Log.w(TAG, "Failed to notify SysUI to start/send assistant progress: " + progress, e); + try { + mSysUiProxy.onAssistantProgress(progress); + + if (mDistance >= mDistThreshold && mTimeFraction >= 1) { + mSysUiProxy.startAssistant(new Bundle()); + mLaunchedAssistant = true; + } + } catch (RemoteException e) { + Log.w(TAG, "Failed to send SysUI start/send assistant progress: " + progress, e); + } } } - private boolean isNavBarOnRight() { - return mDisplayRotation == Surface.ROTATION_90 && mStableInsets.right > 0; - } - - private boolean isNavBarOnLeft() { - return mDisplayRotation == Surface.ROTATION_270 && mStableInsets.left > 0; - } - - private float getDisplacement(MotionEvent ev) { - float eventX = ev.getX(); - float eventY = ev.getY(); - float displacement = eventY - mDownPos.y; - if (isNavBarOnRight()) { - displacement = eventX - mDownPos.x; - } else if (isNavBarOnLeft()) { - displacement = mDownPos.x - eventX; - } - return displacement; - } - - static boolean withinTouchRegion(Context context, float x) { - return x > context.getResources().getDisplayMetrics().widthPixels - - context.getResources().getDimension(R.dimen.gestures_assistant_width); + static boolean withinTouchRegion(Context context, MotionEvent ev) { + final Resources res = context.getResources(); + final int width = res.getDisplayMetrics().widthPixels; + final int height = res.getDisplayMetrics().heightPixels; + final int size = res.getDimensionPixelSize(R.dimen.gestures_assistant_size); + return (ev.getX() > width - size || ev.getX() < size) && ev.getY() > height - size; } } diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java index c281d2c1c4..8fe04617e6 100644 --- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java @@ -311,31 +311,36 @@ public class TouchInteractionService extends Service { mSwipeSharedState.clearAllState(); } + final ActivityControlHelper activityControl = + mOverviewComponentObserver.getActivityControlHelper(); if (runningTaskInfo == null && !mSwipeSharedState.goingToLauncher) { return InputConsumer.NO_OP; } else if (mAssistantAvailable && mOverviewInteractionState.isSwipeUpGestureEnabled() && FeatureFlags.ENABLE_ASSISTANT_GESTURE.get() - && AssistantTouchConsumer.withinTouchRegion(this, event.getX())) { - return new AssistantTouchConsumer(this, mRecentsModel.getSystemUiProxy()); - } else if (mSwipeSharedState.goingToLauncher || - mOverviewComponentObserver.getActivityControlHelper().isResumed()) { - return OverviewInputConsumer.newInstance( - mOverviewComponentObserver.getActivityControlHelper(), false); + && AssistantTouchConsumer.withinTouchRegion(this, event)) { + return new AssistantTouchConsumer(this, mISystemUiProxy, !activityControl.isResumed() + ? createOtherActivityInputConsumer(event, runningTaskInfo) : null); + } else if (mSwipeSharedState.goingToLauncher || activityControl.isResumed()) { + return OverviewInputConsumer.newInstance(activityControl, false); } else if (ENABLE_QUICKSTEP_LIVE_TILE.get() && - mOverviewComponentObserver.getActivityControlHelper().isInLiveTileMode()) { - return OverviewInputConsumer.newInstance( - mOverviewComponentObserver.getActivityControlHelper(), false); + activityControl.isInLiveTileMode()) { + return OverviewInputConsumer.newInstance(activityControl, false); } else { - ActivityControlHelper activityControl = - mOverviewComponentObserver.getActivityControlHelper(); - boolean shouldDefer = activityControl.deferStartingActivity(mActiveNavBarRegion, event); - return new OtherActivityInputConsumer(this, runningTaskInfo, mRecentsModel, - mOverviewComponentObserver.getOverviewIntent(), activityControl, - shouldDefer, mOverviewCallbacks, mTaskOverlayFactory, mInputConsumer, - this::onConsumerInactive, mSwipeSharedState); + return createOtherActivityInputConsumer(event, runningTaskInfo); } } + private OtherActivityInputConsumer createOtherActivityInputConsumer(MotionEvent event, + RunningTaskInfo runningTaskInfo) { + final ActivityControlHelper activityControl = + mOverviewComponentObserver.getActivityControlHelper(); + boolean shouldDefer = activityControl.deferStartingActivity(mActiveNavBarRegion, event); + return new OtherActivityInputConsumer(this, runningTaskInfo, mRecentsModel, + mOverviewComponentObserver.getOverviewIntent(), activityControl, + shouldDefer, mOverviewCallbacks, mTaskOverlayFactory, mInputConsumer, + this::onConsumerInactive, mSwipeSharedState); + } + /** * To be called by the consumer when it's no longer active. */ diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml index 3fbfcdd6c4..a96669832c 100644 --- a/quickstep/res/values/config.xml +++ b/quickstep/res/values/config.xml @@ -26,4 +26,8 @@ determines how many thumbnails will be fetched in the background. --> 3 12 + + + 200 + 30 diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index f5e5dd32cb..9c97c8c905 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -66,6 +66,6 @@ 24dp - 70dp - 200dp + 28dp + 70dp