Loading services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +14 −1 Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ import android.net.Uri; import android.os.Handler; import android.os.SystemClock; import android.provider.Settings; import android.util.Slog; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; Loading Loading @@ -891,9 +892,21 @@ public class AutoclickController extends BaseEventStreamTransformation { final int pointerIndex = mClickScheduler.mLastMotionEvent.getActionIndex(); mLastCursorX = mClickScheduler.mLastMotionEvent.getX(pointerIndex); mLastCursorY = mClickScheduler.mLastMotionEvent.getY(pointerIndex); // Remove previous scroll panel if exists. if (mAutoclickScrollPanel.isVisible()) { mAutoclickScrollPanel.hide(); } // Show scroll panel at the cursor position. mAutoclickScrollPanel.show(mLastCursorX, mLastCursorY); } else { // Fallback: Show scroll panel at its default position (center of screen). Slog.w(LOG_TAG, "Showing scroll panel at default position - no cursor position " + "available"); mAutoclickScrollPanel.show(); } } return; } Loading services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java +71 −4 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_M import android.annotation.IntDef; import android.content.Context; import android.graphics.PixelFormat; import android.util.DisplayMetrics; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; Loading @@ -45,6 +46,10 @@ public class AutoclickScrollPanel { public static final int DIRECTION_EXIT = 4; public static final int DIRECTION_NONE = 5; // Distance between panel and screen edge. // TODO(b/388845721): Finalize edge margin. private static final int PANEL_EDGE_MARGIN = 15; @IntDef({ DIRECTION_UP, DIRECTION_DOWN, Loading @@ -59,6 +64,7 @@ public class AutoclickScrollPanel { private final Context mContext; private final View mContentView; private final WindowManager mWindowManager; private final WindowManager.LayoutParams mParams; private ScrollPanelControllerInterface mScrollPanelController; // Scroll panel buttons. Loading @@ -70,6 +76,10 @@ public class AutoclickScrollPanel { private boolean mInScrollMode = false; // Panel size determined after measuring. private int mPanelWidth; private int mPanelHeight; /** * Interface for handling scroll operations. */ Loading @@ -90,6 +100,7 @@ public class AutoclickScrollPanel { mScrollPanelController = controller; mContentView = LayoutInflater.from(context).inflate( R.layout.accessibility_autoclick_scroll_panel, null); mParams = getDefaultLayoutParams(); // Initialize buttons. mUpButton = mContentView.findViewById(R.id.scroll_up); Loading @@ -99,6 +110,13 @@ public class AutoclickScrollPanel { mExitButton = mContentView.findViewById(R.id.scroll_exit); initializeButtonState(); // Measure the panel to get its dimensions. mContentView.measure( View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); mPanelWidth = mContentView.getMeasuredWidth(); mPanelHeight = mContentView.getMeasuredHeight(); } /** Loading @@ -120,10 +138,60 @@ public class AutoclickScrollPanel { if (mInScrollMode) { return; } mWindowManager.addView(mContentView, getLayoutParams()); mWindowManager.addView(mContentView, mParams); mInScrollMode = true; } /** * Shows the autoclick scroll panel positioned at the bottom right of the cursor. * * @param cursorX The x-coordinate of the cursor. * @param cursorY The y-coordinate of the cursor. */ public void show(float cursorX, float cursorY) { if (mInScrollMode) { return; } // Position the panel at the cursor location positionPanelAtCursor(cursorX, cursorY); mWindowManager.addView(mContentView, mParams); mInScrollMode = true; } /** * Positions the panel at the bottom right of the cursor coordinates, * ensuring it stays within the screen boundaries. */ protected void positionPanelAtCursor(float cursorX, float cursorY) { // Set gravity to TOP|LEFT for absolute positioning. mParams.gravity = Gravity.LEFT | Gravity.TOP; // Get screen dimensions. // TODO(b/388845721): Make sure this works on multiple screens. DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); int screenWidth = displayMetrics.widthPixels; int screenHeight = displayMetrics.heightPixels; // Calculate initial position. int panelX = (int) cursorX; int panelY = (int) cursorY; // Check if panel would go off right edge of screen. if (panelX + mPanelWidth > screenWidth - PANEL_EDGE_MARGIN) { // Place to the left of cursor instead if no space left for right edge. panelX = (int) cursorX - mPanelWidth; } // Check if panel would go off bottom edge of screen. if (panelY + mPanelHeight > screenHeight - PANEL_EDGE_MARGIN) { // Place above cursor instead if no space left for bottom edge. panelY = (int) cursorY - mPanelHeight; } mParams.x = panelX; mParams.y = panelY; } /** * Hides the autoclick scroll panel. */ Loading Loading @@ -176,7 +244,7 @@ public class AutoclickScrollPanel { * Manager. */ @NonNull private WindowManager.LayoutParams getLayoutParams() { private WindowManager.LayoutParams getDefaultLayoutParams() { final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; Loading @@ -189,7 +257,6 @@ public class AutoclickScrollPanel { mContext.getString(R.string.accessibility_autoclick_scroll_panel_title); layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT; layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; layoutParams.gravity = Gravity.CENTER; return layoutParams; } Loading @@ -205,6 +272,6 @@ public class AutoclickScrollPanel { @VisibleForTesting public WindowManager.LayoutParams getLayoutParamsForTesting() { return getLayoutParams(); return mParams; } } services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java +3 −2 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import static com.android.server.testutils.MockitoUtilsKt.eq; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; Loading Loading @@ -718,7 +719,7 @@ public class AutoclickControllerTest { mTestableLooper.processAllMessages(); // Verify scroll panel is shown once. verify(mockScrollPanel, times(1)).show(); verify(mockScrollPanel, times(1)).show(anyFloat(), anyFloat()); assertThat(motionEventCaptor.downEvent).isNull(); // Second significant hover move event to trigger another autoclick. Loading @@ -726,7 +727,7 @@ public class AutoclickControllerTest { mTestableLooper.processAllMessages(); // Verify scroll panel is still only shown once (not called again). verify(mockScrollPanel, times(1)).show(); verify(mockScrollPanel, times(1)).show(anyFloat(), anyFloat()); assertThat(motionEventCaptor.downEvent).isNull(); } Loading services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java +111 −0 Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ import android.content.Context; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; Loading Loading @@ -69,6 +70,9 @@ public class AutoclickScrollPanelTest { private ImageButton mRightButton; private ImageButton mExitButton; private int mScreenWidth; private int mScreenHeight; @Before public void setUp() { mTestableContext.addMockSystemService(Context.WINDOW_SERVICE, mMockWindowManager); Loading @@ -83,6 +87,10 @@ public class AutoclickScrollPanelTest { mLeftButton = contentView.findViewById(R.id.scroll_left); mRightButton = contentView.findViewById(R.id.scroll_right); mExitButton = contentView.findViewById(R.id.scroll_exit); DisplayMetrics displayMetrics = mTestableContext.getResources().getDisplayMetrics(); mScreenWidth = displayMetrics.widthPixels; mScreenHeight = displayMetrics.heightPixels; } @Test Loading Loading @@ -224,6 +232,109 @@ public class AutoclickScrollPanelTest { eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ true)); } @Test public void show_withCursorPosition_addsView() { float cursorX = 300; float cursorY = 300; // Call the new method with cursor coordinates. mScrollPanel.show(cursorX, cursorY); // Verify view is added to window manager. verify(mMockWindowManager).addView(any(), any(WindowManager.LayoutParams.class)); // Verify panel is visible. assertThat(mScrollPanel.isVisible()).isTrue(); } @Test public void hideAndReshow_updatesPosition() { // First show at one position. float firstX = 300; float firstY = 300; mScrollPanel.show(firstX, firstY); assertThat(mScrollPanel.isVisible()).isTrue(); // Hide panel. mScrollPanel.hide(); assertThat(mScrollPanel.isVisible()).isFalse(); // Show at different position. float secondX = 500; float secondY = 500; mScrollPanel.show(secondX, secondY); // Verify panel is visible. assertThat(mScrollPanel.isVisible()).isTrue(); // Verify view was added twice to window manager. verify(mMockWindowManager, times(2)).addView(any(), any(WindowManager.LayoutParams.class)); } @Test public void showPanel_normalCase() { // Normal case - in the middle of the screen. int cursorX = mScreenWidth / 2; int cursorY = mScreenHeight / 2; // Capture the current layout params before positioning. WindowManager.LayoutParams params = mScrollPanel.getLayoutParamsForTesting(); mScrollPanel.positionPanelAtCursor(cursorX, cursorY); // Panel should be at cursor position (gravity is LEFT|TOP). assertThat(params.x).isEqualTo(cursorX); assertThat(params.y).isEqualTo(cursorY); } @Test public void showPanel_nearRightEdge_positionsLeftOfCursor() { // Near right edge case. // 100px from right edge. int cursorX = mScreenWidth - 10; // Center of screen vertically. int cursorY = mScreenHeight / 2; // Capture the current layout params before positioning. WindowManager.LayoutParams params = mScrollPanel.getLayoutParamsForTesting(); mScrollPanel.positionPanelAtCursor(cursorX, cursorY); // Panel should be left of cursor. assertThat(params.x).isLessThan(cursorX); } @Test public void showPanel_nearBottomEdge_positionsAboveCursor() { // Near bottom edge case. // Center of screen horizontally. int cursorX = mScreenWidth / 2; // 10px from bottom edge. int cursorY = mScreenHeight - 10; // Capture the current layout params before positioning. WindowManager.LayoutParams params = mScrollPanel.getLayoutParamsForTesting(); mScrollPanel.positionPanelAtCursor(cursorX, cursorY); // Panel should be above cursor. assertThat(params.y).isLessThan(cursorY); } @Test public void showPanel_nearBottomRightCorner_positionsLeftAndAboveCursor() { // Near bottom-right corner case. // 10px from right edge. int cursorX = mScreenWidth - 10; // 10px from bottom edge. int cursorY = mScreenHeight - 10; // Capture the current layout params before positioning. WindowManager.LayoutParams params = mScrollPanel.getLayoutParamsForTesting(); mScrollPanel.positionPanelAtCursor(cursorX, cursorY); // Panel should be left of and above cursor. assertThat(params.x).isLessThan(cursorX); assertThat(params.y).isLessThan(cursorY); } // Helper method to simulate a hover event on a view. private void triggerHoverEvent(View view, int action) { MotionEvent event = MotionEvent.obtain( Loading Loading
services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +14 −1 Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ import android.net.Uri; import android.os.Handler; import android.os.SystemClock; import android.provider.Settings; import android.util.Slog; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; Loading Loading @@ -891,9 +892,21 @@ public class AutoclickController extends BaseEventStreamTransformation { final int pointerIndex = mClickScheduler.mLastMotionEvent.getActionIndex(); mLastCursorX = mClickScheduler.mLastMotionEvent.getX(pointerIndex); mLastCursorY = mClickScheduler.mLastMotionEvent.getY(pointerIndex); // Remove previous scroll panel if exists. if (mAutoclickScrollPanel.isVisible()) { mAutoclickScrollPanel.hide(); } // Show scroll panel at the cursor position. mAutoclickScrollPanel.show(mLastCursorX, mLastCursorY); } else { // Fallback: Show scroll panel at its default position (center of screen). Slog.w(LOG_TAG, "Showing scroll panel at default position - no cursor position " + "available"); mAutoclickScrollPanel.show(); } } return; } Loading
services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java +71 −4 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_M import android.annotation.IntDef; import android.content.Context; import android.graphics.PixelFormat; import android.util.DisplayMetrics; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; Loading @@ -45,6 +46,10 @@ public class AutoclickScrollPanel { public static final int DIRECTION_EXIT = 4; public static final int DIRECTION_NONE = 5; // Distance between panel and screen edge. // TODO(b/388845721): Finalize edge margin. private static final int PANEL_EDGE_MARGIN = 15; @IntDef({ DIRECTION_UP, DIRECTION_DOWN, Loading @@ -59,6 +64,7 @@ public class AutoclickScrollPanel { private final Context mContext; private final View mContentView; private final WindowManager mWindowManager; private final WindowManager.LayoutParams mParams; private ScrollPanelControllerInterface mScrollPanelController; // Scroll panel buttons. Loading @@ -70,6 +76,10 @@ public class AutoclickScrollPanel { private boolean mInScrollMode = false; // Panel size determined after measuring. private int mPanelWidth; private int mPanelHeight; /** * Interface for handling scroll operations. */ Loading @@ -90,6 +100,7 @@ public class AutoclickScrollPanel { mScrollPanelController = controller; mContentView = LayoutInflater.from(context).inflate( R.layout.accessibility_autoclick_scroll_panel, null); mParams = getDefaultLayoutParams(); // Initialize buttons. mUpButton = mContentView.findViewById(R.id.scroll_up); Loading @@ -99,6 +110,13 @@ public class AutoclickScrollPanel { mExitButton = mContentView.findViewById(R.id.scroll_exit); initializeButtonState(); // Measure the panel to get its dimensions. mContentView.measure( View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); mPanelWidth = mContentView.getMeasuredWidth(); mPanelHeight = mContentView.getMeasuredHeight(); } /** Loading @@ -120,10 +138,60 @@ public class AutoclickScrollPanel { if (mInScrollMode) { return; } mWindowManager.addView(mContentView, getLayoutParams()); mWindowManager.addView(mContentView, mParams); mInScrollMode = true; } /** * Shows the autoclick scroll panel positioned at the bottom right of the cursor. * * @param cursorX The x-coordinate of the cursor. * @param cursorY The y-coordinate of the cursor. */ public void show(float cursorX, float cursorY) { if (mInScrollMode) { return; } // Position the panel at the cursor location positionPanelAtCursor(cursorX, cursorY); mWindowManager.addView(mContentView, mParams); mInScrollMode = true; } /** * Positions the panel at the bottom right of the cursor coordinates, * ensuring it stays within the screen boundaries. */ protected void positionPanelAtCursor(float cursorX, float cursorY) { // Set gravity to TOP|LEFT for absolute positioning. mParams.gravity = Gravity.LEFT | Gravity.TOP; // Get screen dimensions. // TODO(b/388845721): Make sure this works on multiple screens. DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); int screenWidth = displayMetrics.widthPixels; int screenHeight = displayMetrics.heightPixels; // Calculate initial position. int panelX = (int) cursorX; int panelY = (int) cursorY; // Check if panel would go off right edge of screen. if (panelX + mPanelWidth > screenWidth - PANEL_EDGE_MARGIN) { // Place to the left of cursor instead if no space left for right edge. panelX = (int) cursorX - mPanelWidth; } // Check if panel would go off bottom edge of screen. if (panelY + mPanelHeight > screenHeight - PANEL_EDGE_MARGIN) { // Place above cursor instead if no space left for bottom edge. panelY = (int) cursorY - mPanelHeight; } mParams.x = panelX; mParams.y = panelY; } /** * Hides the autoclick scroll panel. */ Loading Loading @@ -176,7 +244,7 @@ public class AutoclickScrollPanel { * Manager. */ @NonNull private WindowManager.LayoutParams getLayoutParams() { private WindowManager.LayoutParams getDefaultLayoutParams() { final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; Loading @@ -189,7 +257,6 @@ public class AutoclickScrollPanel { mContext.getString(R.string.accessibility_autoclick_scroll_panel_title); layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT; layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; layoutParams.gravity = Gravity.CENTER; return layoutParams; } Loading @@ -205,6 +272,6 @@ public class AutoclickScrollPanel { @VisibleForTesting public WindowManager.LayoutParams getLayoutParamsForTesting() { return getLayoutParams(); return mParams; } }
services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java +3 −2 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import static com.android.server.testutils.MockitoUtilsKt.eq; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; Loading Loading @@ -718,7 +719,7 @@ public class AutoclickControllerTest { mTestableLooper.processAllMessages(); // Verify scroll panel is shown once. verify(mockScrollPanel, times(1)).show(); verify(mockScrollPanel, times(1)).show(anyFloat(), anyFloat()); assertThat(motionEventCaptor.downEvent).isNull(); // Second significant hover move event to trigger another autoclick. Loading @@ -726,7 +727,7 @@ public class AutoclickControllerTest { mTestableLooper.processAllMessages(); // Verify scroll panel is still only shown once (not called again). verify(mockScrollPanel, times(1)).show(); verify(mockScrollPanel, times(1)).show(anyFloat(), anyFloat()); assertThat(motionEventCaptor.downEvent).isNull(); } Loading
services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java +111 −0 Original line number Diff line number Diff line Loading @@ -32,6 +32,7 @@ import android.content.Context; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; Loading Loading @@ -69,6 +70,9 @@ public class AutoclickScrollPanelTest { private ImageButton mRightButton; private ImageButton mExitButton; private int mScreenWidth; private int mScreenHeight; @Before public void setUp() { mTestableContext.addMockSystemService(Context.WINDOW_SERVICE, mMockWindowManager); Loading @@ -83,6 +87,10 @@ public class AutoclickScrollPanelTest { mLeftButton = contentView.findViewById(R.id.scroll_left); mRightButton = contentView.findViewById(R.id.scroll_right); mExitButton = contentView.findViewById(R.id.scroll_exit); DisplayMetrics displayMetrics = mTestableContext.getResources().getDisplayMetrics(); mScreenWidth = displayMetrics.widthPixels; mScreenHeight = displayMetrics.heightPixels; } @Test Loading Loading @@ -224,6 +232,109 @@ public class AutoclickScrollPanelTest { eq(AutoclickScrollPanel.DIRECTION_EXIT), eq(/* hovered= */ true)); } @Test public void show_withCursorPosition_addsView() { float cursorX = 300; float cursorY = 300; // Call the new method with cursor coordinates. mScrollPanel.show(cursorX, cursorY); // Verify view is added to window manager. verify(mMockWindowManager).addView(any(), any(WindowManager.LayoutParams.class)); // Verify panel is visible. assertThat(mScrollPanel.isVisible()).isTrue(); } @Test public void hideAndReshow_updatesPosition() { // First show at one position. float firstX = 300; float firstY = 300; mScrollPanel.show(firstX, firstY); assertThat(mScrollPanel.isVisible()).isTrue(); // Hide panel. mScrollPanel.hide(); assertThat(mScrollPanel.isVisible()).isFalse(); // Show at different position. float secondX = 500; float secondY = 500; mScrollPanel.show(secondX, secondY); // Verify panel is visible. assertThat(mScrollPanel.isVisible()).isTrue(); // Verify view was added twice to window manager. verify(mMockWindowManager, times(2)).addView(any(), any(WindowManager.LayoutParams.class)); } @Test public void showPanel_normalCase() { // Normal case - in the middle of the screen. int cursorX = mScreenWidth / 2; int cursorY = mScreenHeight / 2; // Capture the current layout params before positioning. WindowManager.LayoutParams params = mScrollPanel.getLayoutParamsForTesting(); mScrollPanel.positionPanelAtCursor(cursorX, cursorY); // Panel should be at cursor position (gravity is LEFT|TOP). assertThat(params.x).isEqualTo(cursorX); assertThat(params.y).isEqualTo(cursorY); } @Test public void showPanel_nearRightEdge_positionsLeftOfCursor() { // Near right edge case. // 100px from right edge. int cursorX = mScreenWidth - 10; // Center of screen vertically. int cursorY = mScreenHeight / 2; // Capture the current layout params before positioning. WindowManager.LayoutParams params = mScrollPanel.getLayoutParamsForTesting(); mScrollPanel.positionPanelAtCursor(cursorX, cursorY); // Panel should be left of cursor. assertThat(params.x).isLessThan(cursorX); } @Test public void showPanel_nearBottomEdge_positionsAboveCursor() { // Near bottom edge case. // Center of screen horizontally. int cursorX = mScreenWidth / 2; // 10px from bottom edge. int cursorY = mScreenHeight - 10; // Capture the current layout params before positioning. WindowManager.LayoutParams params = mScrollPanel.getLayoutParamsForTesting(); mScrollPanel.positionPanelAtCursor(cursorX, cursorY); // Panel should be above cursor. assertThat(params.y).isLessThan(cursorY); } @Test public void showPanel_nearBottomRightCorner_positionsLeftAndAboveCursor() { // Near bottom-right corner case. // 10px from right edge. int cursorX = mScreenWidth - 10; // 10px from bottom edge. int cursorY = mScreenHeight - 10; // Capture the current layout params before positioning. WindowManager.LayoutParams params = mScrollPanel.getLayoutParamsForTesting(); mScrollPanel.positionPanelAtCursor(cursorX, cursorY); // Panel should be left of and above cursor. assertThat(params.x).isLessThan(cursorX); assertThat(params.y).isLessThan(cursorY); } // Helper method to simulate a hover event on a view. private void triggerHoverEvent(View view, int action) { MotionEvent event = MotionEvent.obtain( Loading