• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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