1 2 /* 3 * Copyright (C) 2019 The Android Open Source Project 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.quickstep.inputconsumers; 19 20 import static android.view.MotionEvent.ACTION_CANCEL; 21 import static android.view.MotionEvent.ACTION_DOWN; 22 import static android.view.MotionEvent.ACTION_MOVE; 23 import static android.view.MotionEvent.ACTION_POINTER_DOWN; 24 import static android.view.MotionEvent.ACTION_POINTER_UP; 25 import static android.view.MotionEvent.ACTION_UP; 26 27 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_GESTURE; 28 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_KEY; 29 import static com.android.launcher3.Utilities.squaredHypot; 30 31 import android.animation.Animator; 32 import android.animation.AnimatorListenerAdapter; 33 import android.animation.ValueAnimator; 34 import android.content.Context; 35 import android.content.res.Resources; 36 import android.graphics.PointF; 37 import android.os.Bundle; 38 import android.os.SystemClock; 39 import android.view.GestureDetector; 40 import android.view.GestureDetector.SimpleOnGestureListener; 41 import android.view.HapticFeedbackConstants; 42 import android.view.MotionEvent; 43 import android.view.ViewConfiguration; 44 45 import com.android.app.animation.Interpolators; 46 import com.android.launcher3.BaseDraggingActivity; 47 import com.android.launcher3.R; 48 import com.android.quickstep.BaseActivityInterface; 49 import com.android.quickstep.GestureState; 50 import com.android.quickstep.InputConsumer; 51 import com.android.quickstep.RecentsAnimationDeviceState; 52 import com.android.quickstep.SystemUiProxy; 53 import com.android.systemui.shared.system.InputMonitorCompat; 54 55 import java.util.function.Consumer; 56 57 /** 58 * Touch consumer for handling events to launch assistant from launcher 59 */ 60 public class AssistantInputConsumer extends DelegateInputConsumer { 61 62 private static final String TAG = "AssistantInputConsumer"; 63 private static final long RETRACT_ANIMATION_DURATION_MS = 300; 64 65 // From //java/com/google/android/apps/gsa/search/shared/util/OpaContract.java. 66 private static final String OPA_BUNDLE_TRIGGER = "triggered_by"; 67 // From //java/com/google/android/apps/gsa/assistant/shared/proto/opa_trigger.proto. 68 private static final int OPA_BUNDLE_TRIGGER_DIAG_SWIPE_GESTURE = 83; 69 70 private final PointF mDownPos = new PointF(); 71 private final PointF mLastPos = new PointF(); 72 private final PointF mStartDragPos = new PointF(); 73 74 private int mActivePointerId = -1; 75 private boolean mPassedSlop; 76 private boolean mLaunchedAssistant; 77 private float mDistance; 78 private float mTimeFraction; 79 private long mDragTime; 80 private float mLastProgress; 81 private BaseActivityInterface mActivityInterface; 82 83 private final float mDragDistThreshold; 84 private final float mFlingDistThreshold; 85 private final long mTimeThreshold; 86 private final int mAngleThreshold; 87 private final float mSquaredSlop; 88 private final Context mContext; 89 private final Consumer<MotionEvent> mGestureDetector; 90 AssistantInputConsumer( Context context, GestureState gestureState, InputConsumer delegate, InputMonitorCompat inputMonitor, RecentsAnimationDeviceState deviceState, MotionEvent startEvent)91 public AssistantInputConsumer( 92 Context context, 93 GestureState gestureState, 94 InputConsumer delegate, 95 InputMonitorCompat inputMonitor, 96 RecentsAnimationDeviceState deviceState, 97 MotionEvent startEvent) { 98 super(delegate, inputMonitor); 99 final Resources res = context.getResources(); 100 mContext = context; 101 mDragDistThreshold = res.getDimension(R.dimen.gestures_assistant_drag_threshold); 102 mFlingDistThreshold = res.getDimension(R.dimen.gestures_assistant_fling_threshold); 103 mTimeThreshold = res.getInteger(R.integer.assistant_gesture_min_time_threshold); 104 mAngleThreshold = res.getInteger(R.integer.assistant_gesture_corner_deg_threshold); 105 106 float slop = ViewConfiguration.get(context).getScaledTouchSlop(); 107 108 mSquaredSlop = slop * slop; 109 mActivityInterface = gestureState.getActivityInterface(); 110 111 boolean flingDisabled = deviceState.isAssistantGestureIsConstrained() 112 || deviceState.isInDeferredGestureRegion(startEvent); 113 mGestureDetector = flingDisabled 114 ? ev -> { } 115 : new GestureDetector(context, new AssistantGestureListener())::onTouchEvent; 116 } 117 118 @Override getType()119 public int getType() { 120 return TYPE_ASSISTANT | mDelegate.getType(); 121 } 122 123 @Override onMotionEvent(MotionEvent ev)124 public void onMotionEvent(MotionEvent ev) { 125 // TODO add logging 126 switch (ev.getActionMasked()) { 127 case ACTION_DOWN: { 128 mActivePointerId = ev.getPointerId(0); 129 mDownPos.set(ev.getX(), ev.getY()); 130 mLastPos.set(mDownPos); 131 mTimeFraction = 0; 132 break; 133 } 134 case ACTION_POINTER_DOWN: { 135 if (mState != STATE_ACTIVE) { 136 mState = STATE_DELEGATE_ACTIVE; 137 } 138 break; 139 } 140 case ACTION_POINTER_UP: { 141 int ptrIdx = ev.getActionIndex(); 142 int ptrId = ev.getPointerId(ptrIdx); 143 if (ptrId == mActivePointerId) { 144 final int newPointerIdx = ptrIdx == 0 ? 1 : 0; 145 mDownPos.set( 146 ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x), 147 ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y)); 148 mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx)); 149 mActivePointerId = ev.getPointerId(newPointerIdx); 150 } 151 break; 152 } 153 case ACTION_MOVE: { 154 if (mState == STATE_DELEGATE_ACTIVE) { 155 break; 156 } 157 if (!mDelegate.allowInterceptByParent()) { 158 mState = STATE_DELEGATE_ACTIVE; 159 break; 160 } 161 int pointerIndex = ev.findPointerIndex(mActivePointerId); 162 if (pointerIndex == -1) { 163 break; 164 } 165 mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); 166 167 if (!mPassedSlop) { 168 // Normal gesture, ensure we pass the slop before we start tracking the gesture 169 if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y) 170 > mSquaredSlop) { 171 172 mPassedSlop = true; 173 mStartDragPos.set(mLastPos.x, mLastPos.y); 174 mDragTime = SystemClock.uptimeMillis(); 175 176 if (isValidAssistantGestureAngle( 177 mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y)) { 178 setActive(ev); 179 } else { 180 mState = STATE_DELEGATE_ACTIVE; 181 } 182 } 183 } else { 184 // Movement 185 mDistance = (float) Math.hypot(mLastPos.x - mStartDragPos.x, 186 mLastPos.y - mStartDragPos.y); 187 if (mDistance >= 0) { 188 final long diff = SystemClock.uptimeMillis() - mDragTime; 189 mTimeFraction = Math.min(diff * 1f / mTimeThreshold, 1); 190 updateAssistantProgress(); 191 } 192 } 193 break; 194 } 195 case ACTION_CANCEL: 196 case ACTION_UP: 197 if (mState != STATE_DELEGATE_ACTIVE && !mLaunchedAssistant) { 198 ValueAnimator animator = ValueAnimator.ofFloat(mLastProgress, 0) 199 .setDuration(RETRACT_ANIMATION_DURATION_MS); 200 animator.addUpdateListener(valueAnimator -> { 201 float progress = (float) valueAnimator.getAnimatedValue(); 202 SystemUiProxy.INSTANCE.get(mContext).onAssistantProgress(progress); 203 }); 204 // Ensure that we always send a zero at the end to clear the invocation state. 205 animator.addListener(new AnimatorListenerAdapter() { 206 @Override 207 public void onAnimationEnd(Animator animation) { 208 super.onAnimationEnd(animation); 209 SystemUiProxy.INSTANCE.get(mContext).onAssistantProgress(0f); 210 } 211 }); 212 animator.setInterpolator(Interpolators.DECELERATE_2); 213 animator.start(); 214 } 215 mPassedSlop = false; 216 mState = STATE_INACTIVE; 217 break; 218 } 219 220 mGestureDetector.accept(ev); 221 222 if (mState != STATE_ACTIVE) { 223 mDelegate.onMotionEvent(ev); 224 } 225 } 226 updateAssistantProgress()227 private void updateAssistantProgress() { 228 if (!mLaunchedAssistant) { 229 mLastProgress = Math.min(mDistance * 1f / mDragDistThreshold, 1) * mTimeFraction; 230 if (mDistance >= mDragDistThreshold && mTimeFraction >= 1) { 231 SystemUiProxy.INSTANCE.get(mContext).onAssistantGestureCompletion(0); 232 startAssistantInternal(); 233 } else { 234 SystemUiProxy.INSTANCE.get(mContext).onAssistantProgress(mLastProgress); 235 } 236 } 237 } 238 startAssistantInternal()239 private void startAssistantInternal() { 240 BaseDraggingActivity launcherActivity = mActivityInterface.getCreatedActivity(); 241 if (launcherActivity != null) { 242 launcherActivity.getRootView().performHapticFeedback( 243 13, // HapticFeedbackConstants.GESTURE_END 244 HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); 245 } 246 247 Bundle args = new Bundle(); 248 args.putInt(OPA_BUNDLE_TRIGGER, OPA_BUNDLE_TRIGGER_DIAG_SWIPE_GESTURE); 249 args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_GESTURE); 250 SystemUiProxy.INSTANCE.get(mContext).startAssistant(args); 251 mLaunchedAssistant = true; 252 } 253 254 /** 255 * Determine if angle is larger than threshold for assistant detection 256 */ isValidAssistantGestureAngle(float deltaX, float deltaY)257 private boolean isValidAssistantGestureAngle(float deltaX, float deltaY) { 258 float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX)); 259 260 // normalize so that angle is measured clockwise from horizontal in the bottom right corner 261 // and counterclockwise from horizontal in the bottom left corner 262 angle = angle > 90 ? 180 - angle : angle; 263 return (angle > mAngleThreshold && angle < 90); 264 } 265 266 private class AssistantGestureListener extends SimpleOnGestureListener { 267 @Override onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)268 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 269 if (isValidAssistantGestureAngle(velocityX, -velocityY) 270 && mDistance >= mFlingDistThreshold 271 && !mLaunchedAssistant 272 && mState != STATE_DELEGATE_ACTIVE) { 273 mLastProgress = 1; 274 SystemUiProxy.INSTANCE.get(mContext).onAssistantGestureCompletion( 275 (float) Math.sqrt(velocityX * velocityX + velocityY * velocityY)); 276 startAssistantInternal(); 277 } 278 return true; 279 } 280 } 281 282 @Override getDelegatorName()283 protected String getDelegatorName() { 284 return "AssistantInputConsumer"; 285 } 286 } 287