Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit eacd86b3 authored by Omar Abdelmonem's avatar Omar Abdelmonem Committed by Android (Google) Code Review
Browse files

Merge "Implement draggability feature for TouchpadDebugView" into main

parents 8dd660e1 5b20222b
Loading
Loading
Loading
Loading
+136 −3
Original line number Diff line number Diff line
@@ -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) {
@@ -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);
@@ -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;
    }
}
+6 −23
Original line number Diff line number Diff line
@@ -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;

@@ -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,
@@ -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 {
+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);
    }
}