diff options
8 files changed, 204 insertions, 2 deletions
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 538283e10738..89f66c010976 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -9345,6 +9345,18 @@ public final class Settings { "accessibility_autoclick_ignore_minor_cursor_movement"; /** + * String setting that stores the position of the autoclick panel when + * {@link #ACCESSIBILITY_AUTOCLICK_ENABLED} is set. The position is stored as a + * comma-separated string containing gravity, x-coordinate, y-coordinate, and corner index. + * For example, "8388659,15,30,0", where 8388659 means gravity Gravity.START | Gravity.TOP. + * + * @see #ACCESSIBILITY_AUTOCLICK_ENABLED + * @hide + */ + public static final String ACCESSIBILITY_AUTOCLICK_PANEL_POSITION = + "accessibility_autoclick_panel_position"; + + /** * Whether or not larger size icons are used for the pointer of mouse/trackpad for * accessibility. * (0 = false, 1 = true) diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index 69c812c6fb41..34ec1481f697 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -110,6 +110,7 @@ message SecureSettingsProto { // Settings for accessibility autoclick optional SettingProto autoclick_cursor_area_size = 62 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto autoclick_ignore_minor_cursor_movement = 63 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto autoclick_panel_position = 64 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Accessibility accessibility = 2; diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 60fe15662980..f692601edcb7 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -95,6 +95,7 @@ public class SecureSettings { Settings.Secure.ACCESSIBILITY_AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT, Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED, Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY, + Settings.Secure.ACCESSIBILITY_AUTOCLICK_PANEL_POSITION, Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON, Settings.Secure.PREFERRED_TTY_MODE, Settings.Secure.ENHANCED_VOICE_PRIVACY_ENABLED, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 3d941e82727f..e42d3fb9da41 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -146,6 +146,7 @@ public class SecureSettingsValidators { Secure.ACCESSIBILITY_AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_AUTOCLICK_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_AUTOCLICK_DELAY, NON_NEGATIVE_INTEGER_VALIDATOR); + VALIDATORS.put(Secure.ACCESSIBILITY_AUTOCLICK_PANEL_POSITION, ANY_STRING_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_LARGE_POINTER_ICON, BOOLEAN_VALIDATOR); VALIDATORS.put( Secure.PREFERRED_TTY_MODE, diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index f03a5fb454fa..c29a5a20a96f 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -1727,6 +1727,8 @@ class SettingsProtoDumpUtil { dumpSetting(s, p, Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY, SecureSettingsProto.Accessibility.AUTOCLICK_DELAY); + dumpSetting(s, p, Settings.Secure.ACCESSIBILITY_AUTOCLICK_PANEL_POSITION, + SecureSettingsProto.Accessibility.AUTOCLICK_PANEL_POSITION); dumpSetting(s, p, Settings.Secure.ACCESSIBILITY_BUTTON_TARGET_COMPONENT, SecureSettingsProto.Accessibility.BUTTON_TARGET_COMPONENT); diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java index aa82df493f84..ca9017408501 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -156,7 +156,7 @@ public class AutoclickController extends BaseEventStreamTransformation { mWindowManager = mContext.getSystemService(WindowManager.class); mAutoclickTypePanel = - new AutoclickTypePanel(mContext, mWindowManager, clickPanelController); + new AutoclickTypePanel(mContext, mWindowManager, mUserId, clickPanelController); mAutoclickTypePanel.show(); mWindowManager.addView(mAutoclickIndicatorView, mAutoclickIndicatorView.getLayoutParams()); diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java index 342675a65360..ab4b3b13eece 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java @@ -16,6 +16,7 @@ package com.android.server.accessibility.autoclick; +import static android.provider.Settings.Secure.ACCESSIBILITY_AUTOCLICK_PANEL_POSITION; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import android.annotation.IntDef; @@ -23,6 +24,8 @@ import android.content.Context; import android.graphics.PixelFormat; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; +import android.provider.Settings; +import android.text.TextUtils; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; @@ -52,6 +55,9 @@ public class AutoclickTypePanel { public static final int CORNER_TOP_LEFT = 2; public static final int CORNER_TOP_RIGHT = 3; + // Used to remember and restore panel's position. + protected static final String POSITION_DELIMITER = ","; + // Distance between panel and screen edge. // TODO(b/396402941): Finalize edge margin. private static final int PANEL_EDGE_MARGIN = 15; @@ -112,6 +118,8 @@ public class AutoclickTypePanel { private final WindowManager mWindowManager; + private final int mUserId; + private WindowManager.LayoutParams mParams; private final ClickPanelControllerInterface mClickPanelController; @@ -142,9 +150,11 @@ public class AutoclickTypePanel { public AutoclickTypePanel( Context context, WindowManager windowManager, + int userId, ClickPanelControllerInterface clickPanelController) { mContext = context; mWindowManager = windowManager; + mUserId = userId; mClickPanelController = clickPanelController; mParams = getDefaultLayoutParams(); @@ -308,6 +318,9 @@ public class AutoclickTypePanel { } public void show() { + // Restores the panel position from saved settings. If no valid position is saved, + // defaults to bottom-right corner. + restorePanelPosition(); mWindowManager.addView(mContentView, mParams); } @@ -316,6 +329,9 @@ public class AutoclickTypePanel { // button background styling is correct when the panel shows up next time. toggleSelectedButtonStyle(mSelectedButton, /* isSelected= */ false); + // Save the panel's position when user turns off the autoclick. + savePanelPosition(); + mWindowManager.removeView(mContentView); } @@ -424,6 +440,79 @@ public class AutoclickTypePanel { } } + private void savePanelPosition() { + String positionString = TextUtils.join(POSITION_DELIMITER, new String[]{ + String.valueOf(mParams.gravity), + String.valueOf(mParams.x), + String.valueOf(mParams.y), + String.valueOf(mCurrentCornerIndex) + }); + Settings.Secure.putStringForUser(mContext.getContentResolver(), + ACCESSIBILITY_AUTOCLICK_PANEL_POSITION, positionString, mUserId); + } + + /** + * Restores the panel position from saved settings. If no valid position is saved, + * defaults to bottom-right corner. + */ + private void restorePanelPosition() { + // Try to get saved position from settings. + String savedPosition = Settings.Secure.getStringForUser(mContext.getContentResolver(), + ACCESSIBILITY_AUTOCLICK_PANEL_POSITION, mUserId); + if (savedPosition == null) { + setPanelPositionForCorner(mParams, CORNER_BOTTOM_RIGHT); + mCurrentCornerIndex = 0; + return; + } + + // Parse saved position string in "gravity,x,y,corner" format. + String[] parts = TextUtils.split(savedPosition, POSITION_DELIMITER); + if (!isValidPositionParts(parts)) { + setPanelPositionForCorner(mParams, CORNER_BOTTOM_RIGHT); + mCurrentCornerIndex = 0; + return; + } + + // Restore the saved position values. + mParams.gravity = Integer.parseInt(parts[0]); + mParams.x = Integer.parseInt(parts[1]); + mParams.y = Integer.parseInt(parts[2]); + mCurrentCornerIndex = Integer.parseInt(parts[3]); + } + + private boolean isValidPositionParts(String[] parts) { + // Check basic array validity. + if (parts == null || parts.length != 4) { + return false; + } + + // Parse values after validating they are numbers. + int gravity = Integer.parseInt(parts[0]); + int x = Integer.parseInt(parts[1]); + int y = Integer.parseInt(parts[2]); + int cornerIndex = Integer.parseInt(parts[3]); + + // Check gravity is valid (START/END | TOP/BOTTOM). + if (gravity != (Gravity.START | Gravity.TOP) && gravity != (Gravity.END | Gravity.TOP) + && gravity != (Gravity.START | Gravity.BOTTOM) && gravity != (Gravity.END + | Gravity.BOTTOM)) { + return false; + } + + // Check coordinates are positive and within screen bounds. + int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels; + int screenHeight = mContext.getResources().getDisplayMetrics().heightPixels; + if (x < 0 || x > screenWidth || y < 0 || y > screenHeight) { + return false; + } + + // Check corner index is valid. + if (cornerIndex < 0 || cornerIndex >= 4) { + return false; + } + return true; + } + @VisibleForTesting boolean getExpansionStateForTesting() { return mExpanded; diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java index c60c4b60d081..9e123406dff5 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java @@ -16,6 +16,8 @@ package com.android.server.accessibility.autoclick; +import static android.provider.Settings.Secure.ACCESSIBILITY_AUTOCLICK_PANEL_POSITION; + import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTOCLICK_TYPE_LEFT_CLICK; @@ -23,13 +25,16 @@ import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AUTO import static com.android.server.accessibility.autoclick.AutoclickTypePanel.AutoclickType; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.CORNER_BOTTOM_LEFT; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.CORNER_BOTTOM_RIGHT; +import static com.android.server.accessibility.autoclick.AutoclickTypePanel.CORNER_TOP_LEFT; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.CORNER_TOP_RIGHT; import static com.android.server.accessibility.autoclick.AutoclickTypePanel.ClickPanelControllerInterface; +import static com.android.server.accessibility.autoclick.AutoclickTypePanel.POSITION_DELIMITER; import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.graphics.drawable.GradientDrawable; +import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper; @@ -92,7 +97,8 @@ public class AutoclickTypePanelTest { mTestableContext.addMockSystemService(Context.WINDOW_SERVICE, mMockWindowManager); mAutoclickTypePanel = - new AutoclickTypePanel(mTestableContext, mMockWindowManager, clickPanelController); + new AutoclickTypePanel(mTestableContext, mMockWindowManager, + mTestableContext.getUserId(), clickPanelController); View contentView = mAutoclickTypePanel.getContentViewForTesting(); mLeftClickButton = contentView.findViewById(R.id.accessibility_autoclick_left_click_layout); mRightClickButton = @@ -294,6 +300,96 @@ public class AutoclickTypePanelTest { .isEqualTo(CORNER_BOTTOM_LEFT); } + @Test + public void restorePanelPosition_noSavedPosition_useDefault() { + // Given no saved position in Settings. + Settings.Secure.putString(mTestableContext.getContentResolver(), + ACCESSIBILITY_AUTOCLICK_PANEL_POSITION, null); + + // Create panel which triggers position restoration internally. + AutoclickTypePanel panel = new AutoclickTypePanel(mTestableContext, mMockWindowManager, + mTestableContext.getUserId(), + clickPanelController); + + // Verify panel is positioned at default bottom-right corner. + WindowManager.LayoutParams params = panel.getLayoutParamsForTesting(); + assertThat(panel.getCurrentCornerIndexForTesting()).isEqualTo(CORNER_BOTTOM_RIGHT); + assertThat(params.gravity).isEqualTo(Gravity.END | Gravity.BOTTOM); + assertThat(params.x).isEqualTo(15); // Default edge margin. + assertThat(params.y).isEqualTo(90); // Default bottom offset. + } + + @Test + public void restorePanelPosition_position_button() { + // Move panel to top-left by clicking position button twice. + mPositionButton.callOnClick(); + mPositionButton.callOnClick(); + + // Hide panel to trigger position saving. + mAutoclickTypePanel.hide(); + + // Verify position is correctly saved in Settings. + String savedPosition = Settings.Secure.getStringForUser( + mTestableContext.getContentResolver(), + ACCESSIBILITY_AUTOCLICK_PANEL_POSITION, mTestableContext.getUserId()); + String[] parts = savedPosition.split(POSITION_DELIMITER); + assertThat(parts).hasLength(4); + assertThat(Integer.parseInt(parts[0])).isEqualTo(Gravity.START | Gravity.TOP); + assertThat(Integer.parseInt(parts[1])).isEqualTo(15); + assertThat(Integer.parseInt(parts[2])).isEqualTo(30); + assertThat(Integer.parseInt(parts[3])).isEqualTo(CORNER_TOP_LEFT); + + // Show panel to trigger position restoration. + mAutoclickTypePanel.show(); + + // Then verify position is restored correctly. + WindowManager.LayoutParams params = mAutoclickTypePanel.getLayoutParamsForTesting(); + assertThat(params.gravity).isEqualTo(Gravity.START | Gravity.TOP); + assertThat(params.x).isEqualTo(15); + assertThat(params.y).isEqualTo(30); + assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting()).isEqualTo( + CORNER_TOP_LEFT); + } + + @Test + public void restorePanelPosition_dragToLeft() { + // Get initial panel position. + View contentView = mAutoclickTypePanel.getContentViewForTesting(); + int[] panelLocation = new int[2]; + contentView.getLocationOnScreen(panelLocation); + + // Simulate drag from initial position to left side of screen. + int screenWidth = mTestableContext.getResources().getDisplayMetrics().widthPixels; + dispatchDragSequence(contentView, + /* startX =*/ panelLocation[0], /* startY =*/ panelLocation[1], + /* endX =*/ (float) screenWidth / 4, /* endY =*/ panelLocation[1] + 10); + + // Hide panel to trigger position saving. + mAutoclickTypePanel.hide(); + + // Verify position is saved correctly. + String savedPosition = Settings.Secure.getStringForUser( + mTestableContext.getContentResolver(), + ACCESSIBILITY_AUTOCLICK_PANEL_POSITION, mTestableContext.getUserId()); + String[] parts = savedPosition.split(POSITION_DELIMITER); + assertThat(parts).hasLength(4); + assertThat(Integer.parseInt(parts[0])).isEqualTo(Gravity.START | Gravity.TOP); + assertThat(Integer.parseInt(parts[1])).isEqualTo(15); + assertThat(Integer.parseInt(parts[2])).isEqualTo(panelLocation[1] + 10); + assertThat(Integer.parseInt(parts[3])).isEqualTo(CORNER_BOTTOM_LEFT); + + // Show panel to trigger position restoration. + mAutoclickTypePanel.show(); + + // Then verify dragged position is restored. + WindowManager.LayoutParams params = mAutoclickTypePanel.getLayoutParamsForTesting(); + assertThat(params.gravity).isEqualTo(Gravity.START | Gravity.TOP); + assertThat(params.x).isEqualTo(15); // PANEL_EDGE_MARGIN + assertThat(params.y).isEqualTo(panelLocation[1] + 10); + assertThat(mAutoclickTypePanel.getCurrentCornerIndexForTesting()).isEqualTo( + CORNER_BOTTOM_LEFT); + } + // Helper method to handle drag event sequences private void dispatchDragSequence(View view, float startX, float startY, float endX, float endY) { |