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