1 /* 2 * Copyright 2024 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.server.input.debug; 18 19 import static android.util.TypedValue.COMPLEX_UNIT_DIP; 20 21 import android.annotation.NonNull; 22 import android.content.Context; 23 import android.content.res.Configuration; 24 import android.graphics.Color; 25 import android.graphics.PixelFormat; 26 import android.graphics.Rect; 27 import android.util.Slog; 28 import android.util.TypedValue; 29 import android.view.Gravity; 30 import android.view.MotionEvent; 31 import android.view.SurfaceControl; 32 import android.view.ViewConfiguration; 33 import android.view.ViewRootImpl; 34 import android.view.WindowManager; 35 import android.widget.LinearLayout; 36 import android.widget.TextView; 37 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.server.input.TouchpadFingerState; 40 import com.android.server.input.TouchpadHardwareProperties; 41 import com.android.server.input.TouchpadHardwareState; 42 43 import java.util.Objects; 44 import java.util.function.Consumer; 45 46 public class TouchpadDebugView extends LinearLayout { 47 private static final float MAX_SCREEN_WIDTH_PROPORTION = 0.4f; 48 private static final float MAX_SCREEN_HEIGHT_PROPORTION = 0.4f; 49 private static final float MIN_SCALE_FACTOR = 10f; 50 private static final float TEXT_SIZE_SP = 16.0f; 51 private static final float DEFAULT_RES_X = 47f; 52 private static final float DEFAULT_RES_Y = 45f; 53 private static final int TEXT_PADDING_DP = 12; 54 private static final int ROUNDED_CORNER_RADIUS_DP = 24; 55 private static final int BUTTON_PRESSED_BACKGROUND_COLOR = Color.rgb(118, 151, 99); 56 private static final int BUTTON_RELEASED_BACKGROUND_COLOR = Color.rgb(84, 85, 169); 57 /** 58 * Input device ID for the touchpad that this debug view is displaying. 59 */ 60 private final int mTouchpadId; 61 private static final String TAG = "TouchpadDebugView"; 62 63 @NonNull 64 private final WindowManager mWindowManager; 65 66 @NonNull 67 private final WindowManager.LayoutParams mWindowLayoutParams; 68 69 private final int mTouchSlop; 70 71 private float mTouchDownX; 72 private float mTouchDownY; 73 private int mScreenWidth; 74 private int mScreenHeight; 75 private int mWindowLocationBeforeDragX; 76 private int mWindowLocationBeforeDragY; 77 private int mLatestGestureType = 0; 78 private TouchpadSelectionView mTouchpadSelectionView; 79 private TouchpadVisualizationView mTouchpadVisualizationView; 80 private TextView mGestureInfoView; 81 @NonNull 82 private TouchpadHardwareState mLastTouchpadState = 83 new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0, 84 new TouchpadFingerState[0]); 85 private final TouchpadHardwareProperties mTouchpadHardwareProperties; 86 TouchpadDebugView(Context context, int touchpadId, TouchpadHardwareProperties touchpadHardwareProperties, Consumer<Integer> touchpadSwitchHandler)87 public TouchpadDebugView(Context context, int touchpadId, 88 TouchpadHardwareProperties touchpadHardwareProperties, 89 Consumer<Integer> touchpadSwitchHandler) { 90 super(context); 91 mTouchpadId = touchpadId; 92 mWindowManager = 93 Objects.requireNonNull(getContext().getSystemService(WindowManager.class)); 94 mTouchpadHardwareProperties = touchpadHardwareProperties; 95 init(context, touchpadId, touchpadSwitchHandler); 96 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 97 98 mWindowLayoutParams = new WindowManager.LayoutParams(); 99 mWindowLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 100 mWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 101 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; 102 mWindowLayoutParams.privateFlags |= 103 WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; 104 mWindowLayoutParams.setFitInsetsTypes(0); 105 mWindowLayoutParams.layoutInDisplayCutoutMode = 106 WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 107 mWindowLayoutParams.format = PixelFormat.TRANSLUCENT; 108 mWindowLayoutParams.setTitle("TouchpadDebugView - display " + mContext.getDisplayId()); 109 110 mWindowLayoutParams.x = 40; 111 mWindowLayoutParams.y = 100; 112 mWindowLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT; 113 mWindowLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; 114 mWindowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT; 115 } 116 init(Context context, int touchpadId, Consumer<Integer> touchpadSwitchHandler)117 private void init(Context context, int touchpadId, 118 Consumer<Integer> touchpadSwitchHandler) { 119 updateScreenDimensions(); 120 setOrientation(VERTICAL); 121 setLayoutParams(new LayoutParams( 122 LayoutParams.WRAP_CONTENT, 123 LayoutParams.WRAP_CONTENT)); 124 setBackgroundColor(Color.TRANSPARENT); 125 126 mTouchpadSelectionView = new TouchpadSelectionView(context, 127 touchpadId, touchpadSwitchHandler); 128 mTouchpadSelectionView.setBackgroundColor(BUTTON_RELEASED_BACKGROUND_COLOR); 129 mTouchpadSelectionView.setGravity(Gravity.CENTER); 130 int paddingInDP = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, TEXT_PADDING_DP, 131 getResources().getDisplayMetrics()); 132 mTouchpadSelectionView.setPadding(paddingInDP, paddingInDP, paddingInDP, paddingInDP); 133 mTouchpadSelectionView.setLayoutParams( 134 new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); 135 136 mTouchpadVisualizationView = new TouchpadVisualizationView(context, 137 mTouchpadHardwareProperties); 138 139 mGestureInfoView = new TextView(context); 140 mGestureInfoView.setTextSize(TEXT_SIZE_SP); 141 mGestureInfoView.setText("Latest Gesture: "); 142 mGestureInfoView.setGravity(Gravity.CENTER); 143 mGestureInfoView.setPadding(paddingInDP, paddingInDP, paddingInDP, paddingInDP); 144 mGestureInfoView.setLayoutParams( 145 new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); 146 //TODO(b/369061237): Handle longer text 147 148 updateTheme(getResources().getConfiguration().uiMode); 149 150 addView(mTouchpadSelectionView); 151 addView(mTouchpadVisualizationView); 152 addView(mGestureInfoView); 153 154 updateViewsDimensions(); 155 } 156 157 @Override onAttachedToWindow()158 public void onAttachedToWindow() { 159 super.onAttachedToWindow(); 160 postDelayed(() -> { 161 final ViewRootImpl viewRootImpl = getRootView().getViewRootImpl(); 162 if (viewRootImpl == null) { 163 Slog.d("TouchpadDebugView", "ViewRootImpl is null."); 164 return; 165 } 166 167 SurfaceControl surfaceControl = viewRootImpl.getSurfaceControl(); 168 if (surfaceControl != null && surfaceControl.isValid()) { 169 try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) { 170 transaction.setCornerRadius(surfaceControl, 171 TypedValue.applyDimension(COMPLEX_UNIT_DIP, 172 ROUNDED_CORNER_RADIUS_DP, 173 getResources().getDisplayMetrics())).apply(); 174 } 175 } else { 176 Slog.d("TouchpadDebugView", "SurfaceControl is invalid or has been released."); 177 } 178 }, 100); 179 } 180 181 @Override onTouchEvent(MotionEvent event)182 public boolean onTouchEvent(MotionEvent event) { 183 if (event.getClassification() == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE 184 || event.getClassification() == MotionEvent.CLASSIFICATION_PINCH) { 185 return false; 186 } 187 188 float deltaX; 189 float deltaY; 190 switch (event.getAction()) { 191 case MotionEvent.ACTION_DOWN: 192 mWindowLocationBeforeDragX = mWindowLayoutParams.x; 193 mWindowLocationBeforeDragY = mWindowLayoutParams.y; 194 mTouchDownX = event.getRawX() - mWindowLocationBeforeDragX; 195 mTouchDownY = event.getRawY() - mWindowLocationBeforeDragY; 196 return true; 197 198 case MotionEvent.ACTION_MOVE: 199 deltaX = event.getRawX() - mWindowLayoutParams.x - mTouchDownX; 200 deltaY = event.getRawY() - mWindowLayoutParams.y - mTouchDownY; 201 if (isSlopExceeded(deltaX, deltaY)) { 202 mWindowLayoutParams.x = 203 Math.max(0, Math.min((int) (event.getRawX() - mTouchDownX), 204 mScreenWidth - this.getWidth())); 205 mWindowLayoutParams.y = 206 Math.max(0, Math.min((int) (event.getRawY() - mTouchDownY), 207 mScreenHeight - this.getHeight())); 208 209 mWindowManager.updateViewLayout(this, mWindowLayoutParams); 210 } 211 return true; 212 213 case MotionEvent.ACTION_UP: 214 deltaX = event.getRawX() - mWindowLayoutParams.x - mTouchDownX; 215 deltaY = event.getRawY() - mWindowLayoutParams.y - mTouchDownY; 216 if (!isSlopExceeded(deltaX, deltaY)) { 217 performClick(); 218 } 219 return true; 220 221 case MotionEvent.ACTION_CANCEL: 222 // Move the window back to the original position 223 mWindowLayoutParams.x = mWindowLocationBeforeDragX; 224 mWindowLayoutParams.y = mWindowLocationBeforeDragY; 225 mWindowManager.updateViewLayout(this, mWindowLayoutParams); 226 return true; 227 228 default: 229 return super.onTouchEvent(event); 230 } 231 } 232 233 @Override performClick()234 public boolean performClick() { 235 super.performClick(); 236 Slog.d(TAG, "You tapped the window!"); 237 return true; 238 } 239 240 @Override onConfigurationChanged(Configuration newConfig)241 protected void onConfigurationChanged(Configuration newConfig) { 242 super.onConfigurationChanged(newConfig); 243 244 updateTheme(newConfig.uiMode); 245 updateScreenDimensions(); 246 updateViewsDimensions(); 247 248 // Adjust view position to stay within screen bounds after rotation 249 mWindowLayoutParams.x = 250 Math.max(0, Math.min(mWindowLayoutParams.x, mScreenWidth - getWidth())); 251 mWindowLayoutParams.y = 252 Math.max(0, Math.min(mWindowLayoutParams.y, mScreenHeight - getHeight())); 253 mWindowManager.updateViewLayout(this, mWindowLayoutParams); 254 } 255 updateTheme(int uiMode)256 private void updateTheme(int uiMode) { 257 int currentNightMode = uiMode & Configuration.UI_MODE_NIGHT_MASK; 258 if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) { 259 setNightModeTheme(); 260 } else { 261 setLightModeTheme(); 262 } 263 } 264 setLightModeTheme()265 private void setLightModeTheme() { 266 mTouchpadVisualizationView.setLightModeTheme(); 267 mGestureInfoView.setBackgroundColor(Color.WHITE); 268 mGestureInfoView.setTextColor(Color.BLACK); 269 } 270 setNightModeTheme()271 private void setNightModeTheme() { 272 mTouchpadVisualizationView.setNightModeTheme(); 273 mGestureInfoView.setBackgroundColor(Color.BLACK); 274 mGestureInfoView.setTextColor(Color.WHITE); 275 } 276 isSlopExceeded(float deltaX, float deltaY)277 private boolean isSlopExceeded(float deltaX, float deltaY) { 278 return deltaX * deltaX + deltaY * deltaY >= mTouchSlop * mTouchSlop; 279 } 280 updateViewsDimensions()281 private void updateViewsDimensions() { 282 float resX = mTouchpadHardwareProperties.getResX() == 0f ? DEFAULT_RES_X 283 : mTouchpadHardwareProperties.getResX(); 284 float resY = mTouchpadHardwareProperties.getResY() == 0f ? DEFAULT_RES_Y 285 : mTouchpadHardwareProperties.getResY(); 286 287 float touchpadHeightMm = Math.abs( 288 mTouchpadHardwareProperties.getBottom() - mTouchpadHardwareProperties.getTop()) 289 / resY; 290 float touchpadWidthMm = Math.abs( 291 mTouchpadHardwareProperties.getLeft() - mTouchpadHardwareProperties.getRight()) 292 / resX; 293 294 float maxViewWidthPx = mScreenWidth * MAX_SCREEN_WIDTH_PROPORTION; 295 float maxViewHeightPx = mScreenHeight * MAX_SCREEN_HEIGHT_PROPORTION; 296 297 float minScaleFactorPx = TypedValue.applyDimension(COMPLEX_UNIT_DIP, MIN_SCALE_FACTOR, 298 getResources().getDisplayMetrics()); 299 300 float scaleFactorBasedOnWidth = 301 touchpadWidthMm * minScaleFactorPx > maxViewWidthPx ? maxViewWidthPx 302 / touchpadWidthMm : minScaleFactorPx; 303 float scaleFactorBasedOnHeight = 304 touchpadHeightMm * minScaleFactorPx > maxViewHeightPx ? maxViewHeightPx 305 / touchpadHeightMm : minScaleFactorPx; 306 float scaleFactorUsed = Math.min(scaleFactorBasedOnHeight, scaleFactorBasedOnWidth); 307 308 mTouchpadVisualizationView.setLayoutParams( 309 new LayoutParams((int) (touchpadWidthMm * scaleFactorUsed), 310 (int) (touchpadHeightMm * scaleFactorUsed))); 311 312 mTouchpadVisualizationView.updateScaleFactor(scaleFactorUsed); 313 mTouchpadVisualizationView.invalidate(); 314 } 315 updateScreenDimensions()316 private void updateScreenDimensions() { 317 Rect windowBounds = 318 mWindowManager.getCurrentWindowMetrics().getBounds(); 319 mScreenWidth = windowBounds.width(); 320 mScreenHeight = windowBounds.height(); 321 } 322 getTouchpadId()323 public int getTouchpadId() { 324 return mTouchpadId; 325 } 326 getWindowLayoutParams()327 public WindowManager.LayoutParams getWindowLayoutParams() { 328 return mWindowLayoutParams; 329 } 330 331 @VisibleForTesting getGestureInfoView()332 TextView getGestureInfoView() { 333 return mGestureInfoView; 334 } 335 336 /** 337 * Notify the view of a change in TouchpadHardwareState and changing the 338 * color of the view based on the status of the button click. 339 */ updateHardwareState(TouchpadHardwareState touchpadHardwareState, int deviceId)340 public void updateHardwareState(TouchpadHardwareState touchpadHardwareState, int deviceId) { 341 if (deviceId != mTouchpadId) { 342 return; 343 } 344 345 mTouchpadVisualizationView.onTouchpadHardwareStateNotified(touchpadHardwareState); 346 if (mLastTouchpadState.getButtonsDown() == 0) { 347 if (touchpadHardwareState.getButtonsDown() > 0) { 348 onTouchpadButtonPress(); 349 } 350 } else { 351 if (touchpadHardwareState.getButtonsDown() == 0) { 352 onTouchpadButtonRelease(); 353 } 354 } 355 mLastTouchpadState = touchpadHardwareState; 356 } 357 onTouchpadButtonPress()358 private void onTouchpadButtonPress() { 359 Slog.d(TAG, "You clicked me!"); 360 mTouchpadSelectionView.setBackgroundColor(BUTTON_PRESSED_BACKGROUND_COLOR); 361 } 362 onTouchpadButtonRelease()363 private void onTouchpadButtonRelease() { 364 Slog.d(TAG, "You released the click"); 365 mTouchpadSelectionView.setBackgroundColor(BUTTON_RELEASED_BACKGROUND_COLOR); 366 } 367 368 /** 369 * Notify the view of any new gesture on the touchpad and displaying its name 370 */ updateGestureInfo(int newGestureType, int deviceId)371 public void updateGestureInfo(int newGestureType, int deviceId) { 372 if (deviceId == mTouchpadId && mLatestGestureType != newGestureType) { 373 mGestureInfoView.setText(getGestureText(newGestureType)); 374 mLatestGestureType = newGestureType; 375 } 376 } 377 378 @NonNull getGestureText(int gestureType)379 static String getGestureText(int gestureType) { 380 // These values are a representation of the GestureType enum in the 381 // external/libchrome-gestures/include/gestures.h library in the C++ code 382 String mGestureName = switch (gestureType) { 383 case 1 -> "Move, 1 Finger"; 384 case 2 -> "Scroll, 2 Fingers"; 385 case 3 -> "Buttons Change, 1 Fingers"; 386 case 4 -> "Fling"; 387 case 5 -> "Swipe, 3 Fingers"; 388 case 6 -> "Pinch, 2 Fingers"; 389 case 7 -> "Swipe Lift, 3 Fingers"; 390 case 8 -> "Metrics"; 391 case 9 -> "Four Finger Swipe, 4 Fingers"; 392 case 10 -> "Four Finger Swipe Lift, 4 Fingers"; 393 case 11 -> "Mouse Wheel"; 394 default -> "Unknown Gesture"; 395 }; 396 return "Latest Gesture: " + mGestureName; 397 } 398 } 399