Loading services/core/java/com/android/server/input/debug/TouchpadDebugView.java +136 −3 Original line number Diff line number Diff line Loading @@ -16,23 +16,69 @@ package com.android.server.input.debug; import android.annotation.NonNull; import android.content.Context; import android.content.res.Configuration; import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.Rect; import android.util.Slog; import android.view.Gravity; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.view.WindowManager; import android.widget.LinearLayout; import android.widget.TextView; public class TouchpadDebugView extends LinearLayout { import java.util.Objects; public class TouchpadDebugView extends LinearLayout { /** * Input device ID for the touchpad that this debug view is displaying. */ private final int mTouchpadId; @NonNull private final WindowManager mWindowManager; @NonNull private final WindowManager.LayoutParams mWindowLayoutParams; private final int mTouchSlop; private float mTouchDownX; private float mTouchDownY; private int mScreenWidth; private int mScreenHeight; private int mWindowLocationBeforeDragX; private int mWindowLocationBeforeDragY; public TouchpadDebugView(Context context, int touchpadId) { super(context); mTouchpadId = touchpadId; mWindowManager = Objects.requireNonNull(getContext().getSystemService(WindowManager.class)); init(context); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); // TODO(b/360137366): Use the hardware properties to initialise layout parameters. mWindowLayoutParams = new WindowManager.LayoutParams(); mWindowLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; mWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; mWindowLayoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; mWindowLayoutParams.setFitInsetsTypes(0); mWindowLayoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; mWindowLayoutParams.format = PixelFormat.TRANSLUCENT; mWindowLayoutParams.setTitle("TouchpadDebugView - display " + mContext.getDisplayId()); mWindowLayoutParams.x = 40; mWindowLayoutParams.y = 100; mWindowLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT; mWindowLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; mWindowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT; } private void init(Context context) { Loading @@ -43,14 +89,12 @@ public class TouchpadDebugView extends LinearLayout { setBackgroundColor(Color.TRANSPARENT); // TODO(b/286551975): Replace this content with the touchpad debug view. TextView textView1 = new TextView(context); textView1.setBackgroundColor(Color.parseColor("#FFFF0000")); textView1.setTextSize(20); textView1.setText("Touchpad Debug View 1"); textView1.setGravity(Gravity.CENTER); textView1.setTextColor(Color.WHITE); textView1.setLayoutParams(new LayoutParams(1000, 200)); TextView textView2 = new TextView(context); Loading @@ -63,9 +107,98 @@ public class TouchpadDebugView extends LinearLayout { addView(textView1); addView(textView2); updateScreenDimensions(); } @Override public boolean onTouchEvent(MotionEvent event) { float deltaX; float deltaY; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mWindowLocationBeforeDragX = mWindowLayoutParams.x; mWindowLocationBeforeDragY = mWindowLayoutParams.y; mTouchDownX = event.getRawX() - mWindowLocationBeforeDragX; mTouchDownY = event.getRawY() - mWindowLocationBeforeDragY; return true; case MotionEvent.ACTION_MOVE: deltaX = event.getRawX() - mWindowLayoutParams.x - mTouchDownX; deltaY = event.getRawY() - mWindowLayoutParams.y - mTouchDownY; Slog.d("TouchpadDebugView", "Slop = " + mTouchSlop); if (isSlopExceeded(deltaX, deltaY)) { Slog.d("TouchpadDebugView", "Slop exceeded"); mWindowLayoutParams.x = Math.max(0, Math.min((int) (event.getRawX() - mTouchDownX), mScreenWidth - this.getWidth())); mWindowLayoutParams.y = Math.max(0, Math.min((int) (event.getRawY() - mTouchDownY), mScreenHeight - this.getHeight())); Slog.d("TouchpadDebugView", "New position X: " + mWindowLayoutParams.x + ", Y: " + mWindowLayoutParams.y); mWindowManager.updateViewLayout(this, mWindowLayoutParams); } return true; case MotionEvent.ACTION_UP: deltaX = event.getRawX() - mWindowLayoutParams.x - mTouchDownX; deltaY = event.getRawY() - mWindowLayoutParams.y - mTouchDownY; if (!isSlopExceeded(deltaX, deltaY)) { performClick(); } return true; case MotionEvent.ACTION_CANCEL: // Move the window back to the original position mWindowLayoutParams.x = mWindowLocationBeforeDragX; mWindowLayoutParams.y = mWindowLocationBeforeDragY; mWindowManager.updateViewLayout(this, mWindowLayoutParams); return true; default: return super.onTouchEvent(event); } } @Override public boolean performClick() { super.performClick(); Slog.d("TouchpadDebugView", "You clicked me!"); return true; } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); updateScreenDimensions(); // Adjust view position to stay within screen bounds after rotation mWindowLayoutParams.x = Math.max(0, Math.min(mWindowLayoutParams.x, mScreenWidth - getWidth())); mWindowLayoutParams.y = Math.max(0, Math.min(mWindowLayoutParams.y, mScreenHeight - getHeight())); mWindowManager.updateViewLayout(this, mWindowLayoutParams); } private boolean isSlopExceeded(float deltaX, float deltaY) { return deltaX * deltaX + deltaY * deltaY >= mTouchSlop * mTouchSlop; } private void updateScreenDimensions() { Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds(); mScreenWidth = windowBounds.width(); mScreenHeight = windowBounds.height(); } public int getTouchpadId() { return mTouchpadId; } public WindowManager.LayoutParams getWindowLayoutParams() { return mWindowLayoutParams; } } services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java +6 −23 Original line number Diff line number Diff line Loading @@ -18,14 +18,12 @@ package com.android.server.input.debug; import android.annotation.Nullable; import android.content.Context; import android.graphics.PixelFormat; import android.hardware.display.DisplayManager; import android.hardware.input.InputManager; import android.os.Handler; import android.os.Looper; import android.util.Slog; import android.view.Display; import android.view.Gravity; import android.view.InputDevice; import android.view.WindowManager; Loading @@ -36,12 +34,14 @@ import java.util.Objects; public class TouchpadDebugViewController { private static final String TAG = "TouchpadDebugViewController"; private static final String TAG = "TouchpadDebugView"; private final Context mContext; private final Handler mHandler; @Nullable private TouchpadDebugView mTouchpadDebugView; private final InputManagerService mInputManagerService; public TouchpadDebugViewController(Context context, Looper looper, Loading Loading @@ -95,32 +95,15 @@ public class TouchpadDebugViewController { mContext.getSystemService(WindowManager.class)); mTouchpadDebugView = new TouchpadDebugView(mContext, touchpadId); final WindowManager.LayoutParams mWindowLayoutParams = mTouchpadDebugView.getWindowLayoutParams(); final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; lp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; lp.setFitInsetsTypes(0); lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; lp.format = PixelFormat.TRANSLUCENT; lp.setTitle("TouchpadDebugView - display " + mContext.getDisplayId()); lp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; lp.x = 40; lp.y = 100; lp.width = WindowManager.LayoutParams.WRAP_CONTENT; lp.height = WindowManager.LayoutParams.WRAP_CONTENT; lp.gravity = Gravity.TOP | Gravity.LEFT; wm.addView(mTouchpadDebugView, lp); wm.addView(mTouchpadDebugView, mWindowLayoutParams); Slog.d(TAG, "Touchpad debug view created."); TouchpadHardwareProperties mTouchpadHardwareProperties = mInputManagerService.getTouchpadHardwareProperties( touchpadId); // TODO(b/360137366): Use the hardware properties to initialise layout parameters. if (mTouchpadHardwareProperties != null) { Slog.d(TAG, mTouchpadHardwareProperties.toString()); } else { Loading tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java 0 → 100644 +292 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.input.debug; import static android.view.InputDevice.SOURCE_TOUCHSCREEN; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; import android.graphics.Rect; import android.testing.TestableContext; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowMetrics; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import com.android.cts.input.MotionEventBuilder; import com.android.cts.input.PointerBuilder; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; /** * Build/Install/Run: * atest TouchpadDebugViewTest */ @RunWith(AndroidJUnit4.class) public class TouchpadDebugViewTest { private static final int TOUCHPAD_DEVICE_ID = 6; private TouchpadDebugView mTouchpadDebugView; private WindowManager.LayoutParams mWindowLayoutParams; @Mock WindowManager mWindowManager; Rect mWindowBounds; WindowMetrics mWindowMetrics; TestableContext mTestableContext; @Before public void setUp() { MockitoAnnotations.initMocks(this); Context context = InstrumentationRegistry.getInstrumentation().getContext(); mTestableContext = new TestableContext(context); mTestableContext.addMockSystemService(WindowManager.class, mWindowManager); mWindowBounds = new Rect(0, 0, 2560, 1600); mWindowMetrics = new WindowMetrics(mWindowBounds, new WindowInsets(mWindowBounds), 1.0f); when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics); mTouchpadDebugView = new TouchpadDebugView(mTestableContext, TOUCHPAD_DEVICE_ID); mTouchpadDebugView.measure( View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) ); doAnswer(invocation -> { mTouchpadDebugView.layout(0, 0, mTouchpadDebugView.getMeasuredWidth(), mTouchpadDebugView.getMeasuredHeight()); return null; }).when(mWindowManager).addView(any(), any()); doAnswer(invocation -> { mTouchpadDebugView.layout(0, 0, mTouchpadDebugView.getMeasuredWidth(), mTouchpadDebugView.getMeasuredHeight()); return null; }).when(mWindowManager).updateViewLayout(any(), any()); mWindowLayoutParams = mTouchpadDebugView.getWindowLayoutParams(); mWindowLayoutParams.x = 20; mWindowLayoutParams.y = 20; mTouchpadDebugView.layout(0, 0, mTouchpadDebugView.getMeasuredWidth(), mTouchpadDebugView.getMeasuredHeight()); } @Test public void testDragView() { // Initial view position relative to screen. int initialX = mWindowLayoutParams.x; int initialY = mWindowLayoutParams.y; float offsetX = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10; float offsetY = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10; // Simulate ACTION_DOWN event (initial touch). MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(40f) .y(40f) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionDown); verify(mWindowManager, times(0)).updateViewLayout(any(), any()); // Simulate ACTION_MOVE event (dragging to the right). MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(40f + offsetX) .y(40f + offsetY) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionMove); ArgumentCaptor<WindowManager.LayoutParams> mWindowLayoutParamsCaptor = ArgumentCaptor.forClass(WindowManager.LayoutParams.class); verify(mWindowManager).updateViewLayout(any(), mWindowLayoutParamsCaptor.capture()); // Verify position after ACTION_MOVE assertEquals(initialX + (long) offsetX, mWindowLayoutParamsCaptor.getValue().x); assertEquals(initialY + (long) offsetY, mWindowLayoutParamsCaptor.getValue().y); // Simulate ACTION_UP event (release touch). MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(40f + offsetX) .y(40f + offsetY) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionUp); assertEquals(initialX + (long) offsetX, mWindowLayoutParamsCaptor.getValue().x); assertEquals(initialY + (long) offsetY, mWindowLayoutParamsCaptor.getValue().y); } @Test public void testDragViewOutOfBounds() { int initialX = mWindowLayoutParams.x; int initialY = mWindowLayoutParams.y; MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(initialX + 10f) .y(initialY + 10f) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionDown); verify(mWindowManager, times(0)).updateViewLayout(any(), any()); // Simulate ACTION_MOVE event (dragging far to the right and bottom, beyond screen bounds) MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(mWindowBounds.width() + mTouchpadDebugView.getWidth()) .y(mWindowBounds.height() + mTouchpadDebugView.getHeight()) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionMove); ArgumentCaptor<WindowManager.LayoutParams> mWindowLayoutParamsCaptor = ArgumentCaptor.forClass(WindowManager.LayoutParams.class); verify(mWindowManager).updateViewLayout(any(), mWindowLayoutParamsCaptor.capture()); // Verify the view has been clamped to the right and bottom edges of the screen assertEquals(mWindowBounds.width() - mTouchpadDebugView.getWidth(), mWindowLayoutParamsCaptor.getValue().x); assertEquals(mWindowBounds.height() - mTouchpadDebugView.getHeight(), mWindowLayoutParamsCaptor.getValue().y); MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(mWindowBounds.width() + mTouchpadDebugView.getWidth()) .y(mWindowBounds.height() + mTouchpadDebugView.getHeight()) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionUp); // Verify the view has been clamped to the right and bottom edges of the screen assertEquals(mWindowBounds.width() - mTouchpadDebugView.getWidth(), mWindowLayoutParamsCaptor.getValue().x); assertEquals(mWindowBounds.height() - mTouchpadDebugView.getHeight(), mWindowLayoutParamsCaptor.getValue().y); } @Test public void testSlopOffset() { int initialX = mWindowLayoutParams.x; int initialY = mWindowLayoutParams.y; float offsetX = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() / 2.0f; float offsetY = -(ViewConfiguration.get(mTestableContext).getScaledTouchSlop() / 2.0f); MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(initialX) .y(initialY) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionDown); MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(initialX + offsetX) .y(initialY + offsetY) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionMove); MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(initialX) .y(initialY) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionUp); // In this case the updateViewLayout() method wouldn't be called because the drag // distance hasn't exceeded the slop verify(mWindowManager, times(0)).updateViewLayout(any(), any()); } @Test public void testViewReturnsToInitialPositionOnCancel() { int initialX = mWindowLayoutParams.x; int initialY = mWindowLayoutParams.y; float offsetX = 50; float offsetY = 50; MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(initialX) .y(initialY) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionDown); MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(initialX + offsetX) .y(initialY + offsetY) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionMove); ArgumentCaptor<WindowManager.LayoutParams> mWindowLayoutParamsCaptor = ArgumentCaptor.forClass(WindowManager.LayoutParams.class); verify(mWindowManager).updateViewLayout(any(), mWindowLayoutParamsCaptor.capture()); assertEquals(initialX + (long) offsetX, mWindowLayoutParamsCaptor.getValue().x); assertEquals(initialY + (long) offsetY, mWindowLayoutParamsCaptor.getValue().y); // Simulate ACTION_CANCEL event (canceling the touch event stream) MotionEvent actionCancel = new MotionEventBuilder(MotionEvent.ACTION_CANCEL, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(initialX + offsetX) .y(initialY + offsetY) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionCancel); // Verify the view returns to its initial position verify(mWindowManager, times(2)).updateViewLayout(any(), mWindowLayoutParamsCaptor.capture()); assertEquals(initialX, mWindowLayoutParamsCaptor.getValue().x); assertEquals(initialY, mWindowLayoutParamsCaptor.getValue().y); } } Loading
services/core/java/com/android/server/input/debug/TouchpadDebugView.java +136 −3 Original line number Diff line number Diff line Loading @@ -16,23 +16,69 @@ package com.android.server.input.debug; import android.annotation.NonNull; import android.content.Context; import android.content.res.Configuration; import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.Rect; import android.util.Slog; import android.view.Gravity; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.view.WindowManager; import android.widget.LinearLayout; import android.widget.TextView; public class TouchpadDebugView extends LinearLayout { import java.util.Objects; public class TouchpadDebugView extends LinearLayout { /** * Input device ID for the touchpad that this debug view is displaying. */ private final int mTouchpadId; @NonNull private final WindowManager mWindowManager; @NonNull private final WindowManager.LayoutParams mWindowLayoutParams; private final int mTouchSlop; private float mTouchDownX; private float mTouchDownY; private int mScreenWidth; private int mScreenHeight; private int mWindowLocationBeforeDragX; private int mWindowLocationBeforeDragY; public TouchpadDebugView(Context context, int touchpadId) { super(context); mTouchpadId = touchpadId; mWindowManager = Objects.requireNonNull(getContext().getSystemService(WindowManager.class)); init(context); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); // TODO(b/360137366): Use the hardware properties to initialise layout parameters. mWindowLayoutParams = new WindowManager.LayoutParams(); mWindowLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; mWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; mWindowLayoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; mWindowLayoutParams.setFitInsetsTypes(0); mWindowLayoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; mWindowLayoutParams.format = PixelFormat.TRANSLUCENT; mWindowLayoutParams.setTitle("TouchpadDebugView - display " + mContext.getDisplayId()); mWindowLayoutParams.x = 40; mWindowLayoutParams.y = 100; mWindowLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT; mWindowLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; mWindowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT; } private void init(Context context) { Loading @@ -43,14 +89,12 @@ public class TouchpadDebugView extends LinearLayout { setBackgroundColor(Color.TRANSPARENT); // TODO(b/286551975): Replace this content with the touchpad debug view. TextView textView1 = new TextView(context); textView1.setBackgroundColor(Color.parseColor("#FFFF0000")); textView1.setTextSize(20); textView1.setText("Touchpad Debug View 1"); textView1.setGravity(Gravity.CENTER); textView1.setTextColor(Color.WHITE); textView1.setLayoutParams(new LayoutParams(1000, 200)); TextView textView2 = new TextView(context); Loading @@ -63,9 +107,98 @@ public class TouchpadDebugView extends LinearLayout { addView(textView1); addView(textView2); updateScreenDimensions(); } @Override public boolean onTouchEvent(MotionEvent event) { float deltaX; float deltaY; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mWindowLocationBeforeDragX = mWindowLayoutParams.x; mWindowLocationBeforeDragY = mWindowLayoutParams.y; mTouchDownX = event.getRawX() - mWindowLocationBeforeDragX; mTouchDownY = event.getRawY() - mWindowLocationBeforeDragY; return true; case MotionEvent.ACTION_MOVE: deltaX = event.getRawX() - mWindowLayoutParams.x - mTouchDownX; deltaY = event.getRawY() - mWindowLayoutParams.y - mTouchDownY; Slog.d("TouchpadDebugView", "Slop = " + mTouchSlop); if (isSlopExceeded(deltaX, deltaY)) { Slog.d("TouchpadDebugView", "Slop exceeded"); mWindowLayoutParams.x = Math.max(0, Math.min((int) (event.getRawX() - mTouchDownX), mScreenWidth - this.getWidth())); mWindowLayoutParams.y = Math.max(0, Math.min((int) (event.getRawY() - mTouchDownY), mScreenHeight - this.getHeight())); Slog.d("TouchpadDebugView", "New position X: " + mWindowLayoutParams.x + ", Y: " + mWindowLayoutParams.y); mWindowManager.updateViewLayout(this, mWindowLayoutParams); } return true; case MotionEvent.ACTION_UP: deltaX = event.getRawX() - mWindowLayoutParams.x - mTouchDownX; deltaY = event.getRawY() - mWindowLayoutParams.y - mTouchDownY; if (!isSlopExceeded(deltaX, deltaY)) { performClick(); } return true; case MotionEvent.ACTION_CANCEL: // Move the window back to the original position mWindowLayoutParams.x = mWindowLocationBeforeDragX; mWindowLayoutParams.y = mWindowLocationBeforeDragY; mWindowManager.updateViewLayout(this, mWindowLayoutParams); return true; default: return super.onTouchEvent(event); } } @Override public boolean performClick() { super.performClick(); Slog.d("TouchpadDebugView", "You clicked me!"); return true; } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); updateScreenDimensions(); // Adjust view position to stay within screen bounds after rotation mWindowLayoutParams.x = Math.max(0, Math.min(mWindowLayoutParams.x, mScreenWidth - getWidth())); mWindowLayoutParams.y = Math.max(0, Math.min(mWindowLayoutParams.y, mScreenHeight - getHeight())); mWindowManager.updateViewLayout(this, mWindowLayoutParams); } private boolean isSlopExceeded(float deltaX, float deltaY) { return deltaX * deltaX + deltaY * deltaY >= mTouchSlop * mTouchSlop; } private void updateScreenDimensions() { Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds(); mScreenWidth = windowBounds.width(); mScreenHeight = windowBounds.height(); } public int getTouchpadId() { return mTouchpadId; } public WindowManager.LayoutParams getWindowLayoutParams() { return mWindowLayoutParams; } }
services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java +6 −23 Original line number Diff line number Diff line Loading @@ -18,14 +18,12 @@ package com.android.server.input.debug; import android.annotation.Nullable; import android.content.Context; import android.graphics.PixelFormat; import android.hardware.display.DisplayManager; import android.hardware.input.InputManager; import android.os.Handler; import android.os.Looper; import android.util.Slog; import android.view.Display; import android.view.Gravity; import android.view.InputDevice; import android.view.WindowManager; Loading @@ -36,12 +34,14 @@ import java.util.Objects; public class TouchpadDebugViewController { private static final String TAG = "TouchpadDebugViewController"; private static final String TAG = "TouchpadDebugView"; private final Context mContext; private final Handler mHandler; @Nullable private TouchpadDebugView mTouchpadDebugView; private final InputManagerService mInputManagerService; public TouchpadDebugViewController(Context context, Looper looper, Loading Loading @@ -95,32 +95,15 @@ public class TouchpadDebugViewController { mContext.getSystemService(WindowManager.class)); mTouchpadDebugView = new TouchpadDebugView(mContext, touchpadId); final WindowManager.LayoutParams mWindowLayoutParams = mTouchpadDebugView.getWindowLayoutParams(); final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; lp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; lp.setFitInsetsTypes(0); lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; lp.format = PixelFormat.TRANSLUCENT; lp.setTitle("TouchpadDebugView - display " + mContext.getDisplayId()); lp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; lp.x = 40; lp.y = 100; lp.width = WindowManager.LayoutParams.WRAP_CONTENT; lp.height = WindowManager.LayoutParams.WRAP_CONTENT; lp.gravity = Gravity.TOP | Gravity.LEFT; wm.addView(mTouchpadDebugView, lp); wm.addView(mTouchpadDebugView, mWindowLayoutParams); Slog.d(TAG, "Touchpad debug view created."); TouchpadHardwareProperties mTouchpadHardwareProperties = mInputManagerService.getTouchpadHardwareProperties( touchpadId); // TODO(b/360137366): Use the hardware properties to initialise layout parameters. if (mTouchpadHardwareProperties != null) { Slog.d(TAG, mTouchpadHardwareProperties.toString()); } else { Loading
tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java 0 → 100644 +292 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.input.debug; import static android.view.InputDevice.SOURCE_TOUCHSCREEN; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; import android.graphics.Rect; import android.testing.TestableContext; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowMetrics; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import com.android.cts.input.MotionEventBuilder; import com.android.cts.input.PointerBuilder; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; /** * Build/Install/Run: * atest TouchpadDebugViewTest */ @RunWith(AndroidJUnit4.class) public class TouchpadDebugViewTest { private static final int TOUCHPAD_DEVICE_ID = 6; private TouchpadDebugView mTouchpadDebugView; private WindowManager.LayoutParams mWindowLayoutParams; @Mock WindowManager mWindowManager; Rect mWindowBounds; WindowMetrics mWindowMetrics; TestableContext mTestableContext; @Before public void setUp() { MockitoAnnotations.initMocks(this); Context context = InstrumentationRegistry.getInstrumentation().getContext(); mTestableContext = new TestableContext(context); mTestableContext.addMockSystemService(WindowManager.class, mWindowManager); mWindowBounds = new Rect(0, 0, 2560, 1600); mWindowMetrics = new WindowMetrics(mWindowBounds, new WindowInsets(mWindowBounds), 1.0f); when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics); mTouchpadDebugView = new TouchpadDebugView(mTestableContext, TOUCHPAD_DEVICE_ID); mTouchpadDebugView.measure( View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) ); doAnswer(invocation -> { mTouchpadDebugView.layout(0, 0, mTouchpadDebugView.getMeasuredWidth(), mTouchpadDebugView.getMeasuredHeight()); return null; }).when(mWindowManager).addView(any(), any()); doAnswer(invocation -> { mTouchpadDebugView.layout(0, 0, mTouchpadDebugView.getMeasuredWidth(), mTouchpadDebugView.getMeasuredHeight()); return null; }).when(mWindowManager).updateViewLayout(any(), any()); mWindowLayoutParams = mTouchpadDebugView.getWindowLayoutParams(); mWindowLayoutParams.x = 20; mWindowLayoutParams.y = 20; mTouchpadDebugView.layout(0, 0, mTouchpadDebugView.getMeasuredWidth(), mTouchpadDebugView.getMeasuredHeight()); } @Test public void testDragView() { // Initial view position relative to screen. int initialX = mWindowLayoutParams.x; int initialY = mWindowLayoutParams.y; float offsetX = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10; float offsetY = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10; // Simulate ACTION_DOWN event (initial touch). MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(40f) .y(40f) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionDown); verify(mWindowManager, times(0)).updateViewLayout(any(), any()); // Simulate ACTION_MOVE event (dragging to the right). MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(40f + offsetX) .y(40f + offsetY) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionMove); ArgumentCaptor<WindowManager.LayoutParams> mWindowLayoutParamsCaptor = ArgumentCaptor.forClass(WindowManager.LayoutParams.class); verify(mWindowManager).updateViewLayout(any(), mWindowLayoutParamsCaptor.capture()); // Verify position after ACTION_MOVE assertEquals(initialX + (long) offsetX, mWindowLayoutParamsCaptor.getValue().x); assertEquals(initialY + (long) offsetY, mWindowLayoutParamsCaptor.getValue().y); // Simulate ACTION_UP event (release touch). MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(40f + offsetX) .y(40f + offsetY) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionUp); assertEquals(initialX + (long) offsetX, mWindowLayoutParamsCaptor.getValue().x); assertEquals(initialY + (long) offsetY, mWindowLayoutParamsCaptor.getValue().y); } @Test public void testDragViewOutOfBounds() { int initialX = mWindowLayoutParams.x; int initialY = mWindowLayoutParams.y; MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(initialX + 10f) .y(initialY + 10f) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionDown); verify(mWindowManager, times(0)).updateViewLayout(any(), any()); // Simulate ACTION_MOVE event (dragging far to the right and bottom, beyond screen bounds) MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(mWindowBounds.width() + mTouchpadDebugView.getWidth()) .y(mWindowBounds.height() + mTouchpadDebugView.getHeight()) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionMove); ArgumentCaptor<WindowManager.LayoutParams> mWindowLayoutParamsCaptor = ArgumentCaptor.forClass(WindowManager.LayoutParams.class); verify(mWindowManager).updateViewLayout(any(), mWindowLayoutParamsCaptor.capture()); // Verify the view has been clamped to the right and bottom edges of the screen assertEquals(mWindowBounds.width() - mTouchpadDebugView.getWidth(), mWindowLayoutParamsCaptor.getValue().x); assertEquals(mWindowBounds.height() - mTouchpadDebugView.getHeight(), mWindowLayoutParamsCaptor.getValue().y); MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(mWindowBounds.width() + mTouchpadDebugView.getWidth()) .y(mWindowBounds.height() + mTouchpadDebugView.getHeight()) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionUp); // Verify the view has been clamped to the right and bottom edges of the screen assertEquals(mWindowBounds.width() - mTouchpadDebugView.getWidth(), mWindowLayoutParamsCaptor.getValue().x); assertEquals(mWindowBounds.height() - mTouchpadDebugView.getHeight(), mWindowLayoutParamsCaptor.getValue().y); } @Test public void testSlopOffset() { int initialX = mWindowLayoutParams.x; int initialY = mWindowLayoutParams.y; float offsetX = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() / 2.0f; float offsetY = -(ViewConfiguration.get(mTestableContext).getScaledTouchSlop() / 2.0f); MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(initialX) .y(initialY) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionDown); MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(initialX + offsetX) .y(initialY + offsetY) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionMove); MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(initialX) .y(initialY) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionUp); // In this case the updateViewLayout() method wouldn't be called because the drag // distance hasn't exceeded the slop verify(mWindowManager, times(0)).updateViewLayout(any(), any()); } @Test public void testViewReturnsToInitialPositionOnCancel() { int initialX = mWindowLayoutParams.x; int initialY = mWindowLayoutParams.y; float offsetX = 50; float offsetY = 50; MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(initialX) .y(initialY) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionDown); MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(initialX + offsetX) .y(initialY + offsetY) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionMove); ArgumentCaptor<WindowManager.LayoutParams> mWindowLayoutParamsCaptor = ArgumentCaptor.forClass(WindowManager.LayoutParams.class); verify(mWindowManager).updateViewLayout(any(), mWindowLayoutParamsCaptor.capture()); assertEquals(initialX + (long) offsetX, mWindowLayoutParamsCaptor.getValue().x); assertEquals(initialY + (long) offsetY, mWindowLayoutParamsCaptor.getValue().y); // Simulate ACTION_CANCEL event (canceling the touch event stream) MotionEvent actionCancel = new MotionEventBuilder(MotionEvent.ACTION_CANCEL, SOURCE_TOUCHSCREEN) .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) .x(initialX + offsetX) .y(initialY + offsetY) ) .build(); mTouchpadDebugView.dispatchTouchEvent(actionCancel); // Verify the view returns to its initial position verify(mWindowManager, times(2)).updateViewLayout(any(), mWindowLayoutParamsCaptor.capture()); assertEquals(initialX, mWindowLayoutParamsCaptor.getValue().x); assertEquals(initialY, mWindowLayoutParamsCaptor.getValue().y); } }