• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 
17 package com.android.car.carlauncher;
18 
19 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
20 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
21 import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY;
22 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
23 
24 import android.annotation.MainThread;
25 import android.app.Activity;
26 import android.app.Application;
27 import android.content.Context;
28 import android.graphics.PixelFormat;
29 import android.graphics.Rect;
30 import android.hardware.input.InputManager;
31 import android.os.Bundle;
32 import android.util.Log;
33 import android.view.GestureDetector;
34 import android.view.MotionEvent;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.view.WindowManager;
38 
39 import androidx.annotation.NonNull;
40 import androidx.annotation.Nullable;
41 
42 import com.android.wm.shell.TaskView;
43 
44 import java.util.List;
45 
46 /**
47  * This class is responsible to intercept the swipe gestures & long press over {@link
48  * ControlledCarTaskView}.
49  *
50  * <ul>
51  *   <li>The gesture interception will only occur when the corresponding {@link
52  *       ControlledCarTaskViewConfig#mCaptureGestures} is set.
53  *   <li>The long press interception will only occur when the corresponding {@link
54  *       ControlledCarTaskViewConfig#mCaptureLongPress} is set.
55  * </ul>
56  */
57 public final class TaskViewInputInterceptor {
58 
59     private static final String TAG = TaskViewInputInterceptor.class.getSimpleName();
60     private static final boolean DBG = Log.isLoggable(CarLauncher.TAG, Log.DEBUG);
61 
62     private static final Rect sTmpBounds = new Rect();
63 
64     private final Activity mHostActivity;
65     private final InputManager mInputManager;
66     private final TaskViewManager mTaskViewManager;
67     private final WindowManager mWm;
68     private final GestureDetector mGestureDetector =
69             new GestureDetector(new TaskViewGestureListener());
70     private final Application.ActivityLifecycleCallbacks mActivityLifecycleCallbacks =
71             new ActivityLifecycleHandler();
72 
73     private View mSpyWindow;
74     private boolean mInitialized = false;
75 
TaskViewInputInterceptor(Activity hostActivity, TaskViewManager taskViewManager)76     TaskViewInputInterceptor(Activity hostActivity, TaskViewManager taskViewManager) {
77         mHostActivity = hostActivity;
78         mInputManager = hostActivity.getSystemService(InputManager.class);
79         mTaskViewManager = taskViewManager;
80         mWm = mHostActivity.getSystemService(WindowManager.class);
81     }
82 
isIn(MotionEvent event, TaskView taskView)83     private static boolean isIn(MotionEvent event, TaskView taskView) {
84         taskView.getBoundsOnScreen(sTmpBounds);
85         return sTmpBounds.contains((int) event.getX(), (int) event.getY());
86     }
87 
88     /** Initializes & starts intercepting gestures. Does nothing if already initialized. */
89     @MainThread
init()90     void init() {
91         if (mInitialized) {
92             Log.e(TAG, "Already initialized");
93             return;
94         }
95         mInitialized = true;
96         mHostActivity.registerActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
97         startInterceptingGestures();
98     }
99 
100     /**
101      * Releases the held resources and stops intercepting gestures. Does nothing if already
102      * released.
103      */
104     @MainThread
release()105     void release() {
106         if (!mInitialized) {
107             Log.e(TAG, "Failed to release as it is not initialized");
108             return;
109         }
110         mInitialized = false;
111         mHostActivity.unregisterActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
112         stopInterceptingGestures();
113     }
114 
startInterceptingGestures()115     private void startInterceptingGestures() {
116         if (DBG) {
117             Log.d(TAG, "Start intercepting gestures");
118         }
119         if (mSpyWindow != null) {
120             Log.d(TAG, "Already intercepting gestures");
121             return;
122         }
123         createAndAddSpyWindow();
124     }
125 
stopInterceptingGestures()126     private void stopInterceptingGestures() {
127         if (DBG) {
128             Log.d(TAG, "Stop intercepting gestures");
129         }
130         if (mSpyWindow == null) {
131             Log.d(TAG, "Already not intercepting gestures");
132             return;
133         }
134         removeSpyWindow();
135     }
136 
createAndAddSpyWindow()137     private void createAndAddSpyWindow() {
138         mSpyWindow = new GestureSpyView(mHostActivity);
139         WindowManager.LayoutParams p =
140                 new WindowManager.LayoutParams(
141                         ViewGroup.LayoutParams.MATCH_PARENT,
142                         ViewGroup.LayoutParams.MATCH_PARENT,
143                         TYPE_APPLICATION_OVERLAY,
144                         WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
145                                 | FLAG_NOT_FOCUSABLE
146                                 | FLAG_LAYOUT_IN_SCREEN,
147                         // LAYOUT_IN_SCREEN required so that event coordinate system matches the
148                         // taskview.getBoundsOnScreen coordinate system
149                         PixelFormat.TRANSLUCENT);
150         p.inputFeatures = INPUT_FEATURE_SPY;
151         p.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
152         mWm.addView(mSpyWindow, p);
153     }
154 
removeSpyWindow()155     private void removeSpyWindow() {
156         if (mSpyWindow == null) {
157             Log.e(TAG, "Spy window is not present");
158             return;
159         }
160         mWm.removeView(mSpyWindow);
161         mSpyWindow = null;
162     }
163 
164     private final class GestureSpyView extends View {
165         private boolean mConsumingCurrentEventStream = false;
166         private boolean mActionDownInsideTaskView = false;
167         private float mTouchDownX;
168         private float mTouchDownY;
169 
GestureSpyView(Context context)170         GestureSpyView(Context context) {
171             super(context);
172         }
173 
174         @Override
dispatchTouchEvent(MotionEvent event)175         public boolean dispatchTouchEvent(MotionEvent event) {
176             boolean justToggled = false;
177             mGestureDetector.onTouchEvent(event);
178 
179             if (event.getAction() == MotionEvent.ACTION_DOWN) {
180                 mActionDownInsideTaskView = false;
181 
182                 List<ControlledCarTaskView> taskViewList =
183                         mTaskViewManager.getControlledTaskViews();
184                 for (ControlledCarTaskView tv : taskViewList) {
185                     if (tv.getConfig().mCaptureGestures && isIn(event, tv)) {
186                         mTouchDownX = event.getX();
187                         mTouchDownY = event.getY();
188                         mActionDownInsideTaskView = true;
189                         break;
190                     }
191                 }
192 
193                 // Stop consuming immediately on ACTION_DOWN
194                 mConsumingCurrentEventStream = false;
195             }
196 
197             if (event.getAction() == MotionEvent.ACTION_MOVE) {
198                 if (!mConsumingCurrentEventStream && mActionDownInsideTaskView
199                         && Float.compare(mTouchDownX, event.getX()) != 0
200                         && Float.compare(mTouchDownY, event.getY()) != 0) {
201                     // Start consuming on ACTION_MOVE when ACTION_DOWN happened inside TaskView
202                     mConsumingCurrentEventStream = true;
203                     justToggled = true;
204                 }
205 
206                 // Handling the events
207                 if (mConsumingCurrentEventStream) {
208                     // Disable the propagation when consuming events.
209                     mInputManager.pilferPointers(getViewRootImpl().getInputToken());
210 
211                     if (justToggled) {
212                         // When just toggled from DOWN to MOVE, dispatch a DOWN event as DOWN event
213                         // is meant to be the first event in an event stream.
214                         MotionEvent cloneEvent = MotionEvent.obtain(event);
215                         cloneEvent.setAction(MotionEvent.ACTION_DOWN);
216                         TaskViewInputInterceptor.this.mHostActivity.dispatchTouchEvent(cloneEvent);
217                         cloneEvent.recycle();
218                     }
219                     TaskViewInputInterceptor.this.mHostActivity.dispatchTouchEvent(event);
220                 }
221             }
222 
223             if (event.getAction() == MotionEvent.ACTION_UP) {
224                 // Handling the events
225                 if (mConsumingCurrentEventStream) {
226                     // Disable the propagation when handling manually.
227                     mInputManager.pilferPointers(getViewRootImpl().getInputToken());
228                     TaskViewInputInterceptor.this.mHostActivity.dispatchTouchEvent(event);
229                 }
230                 mConsumingCurrentEventStream = false;
231             }
232             return false;
233         }
234     }
235 
236     private final class TaskViewGestureListener extends GestureDetector.SimpleOnGestureListener {
237         @Override
onLongPress(@onNull MotionEvent e)238         public void onLongPress(@NonNull MotionEvent e) {
239             List<ControlledCarTaskView> taskViewList = mTaskViewManager.getControlledTaskViews();
240             for (ControlledCarTaskView tv : taskViewList) {
241                 if (tv.getConfig().mCaptureLongPress && isIn(e, tv)) {
242                     if (DBG) {
243                         Log.d(TAG, "Long press captured for taskView: " + tv);
244                     }
245                     mInputManager.pilferPointers(mSpyWindow.getViewRootImpl().getInputToken());
246                     if (tv.getOnLongClickListener() != null) {
247                         tv.getOnLongClickListener().onLongClick(tv);
248                     }
249                     return;
250                 }
251             }
252             if (DBG) {
253                 Log.d(TAG, "Long press not captured");
254             }
255         }
256     }
257 
258     private final class ActivityLifecycleHandler implements Application.ActivityLifecycleCallbacks {
259         @Override
onActivityCreated( @onNull Activity activity, @Nullable Bundle savedInstanceState)260         public void onActivityCreated(
261                 @NonNull Activity activity, @Nullable Bundle savedInstanceState) {}
262 
263         @Override
onActivityStarted(@onNull Activity activity)264         public void onActivityStarted(@NonNull Activity activity) {
265             if (!mInitialized) {
266                 return;
267             }
268             startInterceptingGestures();
269         }
270 
271         @Override
onActivityResumed(@onNull Activity activity)272         public void onActivityResumed(@NonNull Activity activity) {}
273 
274         @Override
onActivityPaused(@onNull Activity activity)275         public void onActivityPaused(@NonNull Activity activity) {}
276 
277         @Override
onActivityStopped(@onNull Activity activity)278         public void onActivityStopped(@NonNull Activity activity) {
279             if (!mInitialized) {
280                 return;
281             }
282             stopInterceptingGestures();
283         }
284 
285         @Override
onActivitySaveInstanceState( @onNull Activity activity, @NonNull Bundle outState)286         public void onActivitySaveInstanceState(
287                 @NonNull Activity activity, @NonNull Bundle outState) {}
288 
289         @Override
onActivityDestroyed(@onNull Activity activity)290         public void onActivityDestroyed(@NonNull Activity activity) {}
291     }
292 }
293