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