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