• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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