diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java index 2b4e09d668517..83def0cc1fd6a 100644 --- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java +++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java @@ -80,6 +80,8 @@ public class AccessibilityShortcutController { "com.android.server.accessibility.MagnificationController"; public static final ComponentName MAGNIFICATION_COMPONENT_NAME = new ComponentName("com.android.server.accessibility", "Magnification"); + public static final ComponentName ONE_HANDED_COMPONENT_NAME = + new ComponentName("com.android.server.accessibility", "OneHandedMode"); public static final ComponentName REDUCE_BRIGHT_COLORS_COMPONENT_NAME = new ComponentName("com.android.server.accessibility", "ReduceBrightColors"); @@ -117,7 +119,7 @@ public class AccessibilityShortcutController { public static Map getFrameworkShortcutFeaturesMap() { if (sFrameworkShortcutFeaturesMap == null) { - Map featuresMap = new ArrayMap<>(2); + Map featuresMap = new ArrayMap<>(4); featuresMap.put(COLOR_INVERSION_COMPONENT_NAME, new ToggleableFrameworkFeatureInfo( Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, @@ -128,6 +130,11 @@ public class AccessibilityShortcutController { Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, "1" /* Value to enable */, "0" /* Value to disable */, R.string.color_correction_feature_name)); + featuresMap.put(ONE_HANDED_COMPONENT_NAME, + new ToggleableFrameworkFeatureInfo( + Settings.Secure.ONE_HANDED_MODE_ACTIVATED, + "1" /* Value to enable */, "0" /* Value to disable */, + R.string.one_handed_mode_feature_name)); featuresMap.put(REDUCE_BRIGHT_COLORS_COMPONENT_NAME, new ToggleableFrameworkFeatureInfo( Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED, diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java index f2d91ba308426..4a20ad0f33f5e 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java @@ -21,6 +21,7 @@ import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTT import static com.android.internal.accessibility.AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME; import static com.android.internal.accessibility.AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; +import static com.android.internal.accessibility.AccessibilityShortcutController.ONE_HANDED_COMPONENT_NAME; import static com.android.internal.accessibility.AccessibilityShortcutController.REDUCE_BRIGHT_COLORS_COMPONENT_NAME; import static com.android.internal.accessibility.util.AccessibilityUtils.getAccessibilityServiceFragmentType; import static com.android.internal.accessibility.util.ShortcutUtils.isShortcutContained; @@ -230,6 +231,16 @@ public final class AccessibilityTargetHelper { context.getDrawable(R.drawable.ic_accessibility_color_inversion), Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED); + final ToggleAllowListingFeatureTarget oneHandedMode = + new ToggleAllowListingFeatureTarget(context, + shortcutType, + isShortcutContained(context, shortcutType, + ONE_HANDED_COMPONENT_NAME.flattenToString()), + ONE_HANDED_COMPONENT_NAME.flattenToString(), + context.getString(R.string.one_handed_mode_feature_name), + context.getDrawable(R.drawable.ic_accessibility_one_handed), + Settings.Secure.ONE_HANDED_MODE_ACTIVATED); + final ToggleAllowListingFeatureTarget reduceBrightColors = new ToggleAllowListingFeatureTarget(context, shortcutType, @@ -243,6 +254,7 @@ public final class AccessibilityTargetHelper { targets.add(magnification); targets.add(daltonizer); targets.add(colorInversion); + targets.add(oneHandedMode); targets.add(reduceBrightColors); return targets; diff --git a/core/res/res/drawable/ic_accessibility_one_handed.xml b/core/res/res/drawable/ic_accessibility_one_handed.xml new file mode 100644 index 0000000000000..0a469ba731eec --- /dev/null +++ b/core/res/res/drawable/ic_accessibility_one_handed.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/core/res/res/drawable/ic_accessibility_one_handed_foreground.xml b/core/res/res/drawable/ic_accessibility_one_handed_foreground.xml new file mode 100644 index 0000000000000..18e56187efb33 --- /dev/null +++ b/core/res/res/drawable/ic_accessibility_one_handed_foreground.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index a0c6f451f14e9..4cbfc69527efa 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -4636,6 +4636,9 @@ shown in the warning dialog about the accessibility shortcut. --> Color Correction + + One-Handed mode + Extra dim diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 5019ccadd353a..3a22efce70fc1 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3410,9 +3410,12 @@ + + + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java index ae7ab528ed0af..ce57638f16f43 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java @@ -135,6 +135,7 @@ public class OneHandedController implements RemoteCallable } }; + private final ContentObserver mActivatedObserver; private final ContentObserver mEnabledObserver; private final ContentObserver mTimeoutObserver; private final ContentObserver mTaskChangeExitObserver; @@ -170,11 +171,13 @@ public class OneHandedController implements RemoteCallable @Override public void onStartFinished(Rect bounds) { mState.setState(STATE_ACTIVE); + notifyShortcutState(STATE_ACTIVE); } @Override public void onStopFinished(Rect bounds) { mState.setState(STATE_NONE); + notifyShortcutState(STATE_NONE); } }; @@ -287,6 +290,7 @@ public class OneHandedController implements RemoteCallable context.getContentResolver(), mUserId); mTimeoutHandler = timeoutHandler; + mActivatedObserver = getObserver(this::onActivatedActionChanged); mEnabledObserver = getObserver(this::onEnabledSettingChanged); mTimeoutObserver = getObserver(this::onTimeoutSettingChanged); mTaskChangeExitObserver = getObserver(this::onTaskChangeExitSettingChanged); @@ -347,6 +351,12 @@ public class OneHandedController implements RemoteCallable updateOneHandedEnabled(); } + @VisibleForTesting + void notifyShortcutState(@OneHandedState.State int state) { + mOneHandedSettingsUtil.setOneHandedModeActivated( + mContext.getContentResolver(), state == STATE_ACTIVE ? 1 : 0, mUserId); + } + @VisibleForTesting void startOneHanded() { if (isLockedDisabled()) { @@ -416,6 +426,9 @@ public class OneHandedController implements RemoteCallable } private void registerSettingObservers(int newUserId) { + mOneHandedSettingsUtil.registerSettingsKeyObserver( + Settings.Secure.ONE_HANDED_MODE_ACTIVATED, + mContext.getContentResolver(), mActivatedObserver, newUserId); mOneHandedSettingsUtil.registerSettingsKeyObserver(Settings.Secure.ONE_HANDED_MODE_ENABLED, mContext.getContentResolver(), mEnabledObserver, newUserId); mOneHandedSettingsUtil.registerSettingsKeyObserver(Settings.Secure.ONE_HANDED_MODE_TIMEOUT, @@ -465,6 +478,26 @@ public class OneHandedController implements RemoteCallable }; } + @VisibleForTesting + void onActivatedActionChanged() { + if (mState.isTransitioning() || !isOneHandedEnabled()) { + return; + } + + final boolean isActivated = mState.getState() == STATE_ACTIVE; + final boolean requestActivated = mOneHandedSettingsUtil.getOneHandedModeActivated( + mContext.getContentResolver(), mUserId); + // When gesture trigger action, we will update settings and introduce observer callback + // again, then the following logic will just ignore the second redundant callback. + if (isActivated ^ requestActivated) { + if (requestActivated) { + startOneHanded(); + } else { + stopOneHanded(); + } + } + } + @VisibleForTesting void onEnabledSettingChanged() { final boolean enabled = mOneHandedSettingsUtil.getSettingsOneHandedModeEnabled( @@ -544,6 +577,11 @@ public class OneHandedController implements RemoteCallable return mLockedDisabled; } + @VisibleForTesting + boolean isOneHandedEnabled() { + return mIsOneHandedEnabled; + } + private void updateOneHandedEnabled() { if (mState.getState() == STATE_ENTERING || mState.getState() == STATE_ACTIVE) { mMainExecutor.execute(() -> stopOneHanded()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java index 2ab51f3f1313e..bb68224b0a872 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java @@ -131,8 +131,28 @@ public final class OneHandedSettingsUtil { * Returns whether swipe bottom to notification gesture enabled or not. */ public boolean getSettingsSwipeToNotificationEnabled(ContentResolver resolver, int userId) { - return Settings.Secure.getInt(resolver, - Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, 0 /* Default OFF */) == 1; + return Settings.Secure.getIntForUser(resolver, + Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, 0, userId) == 1; + } + + /** + * Sets one handed activated or not to notify state for shortcut + * + * @return activated or not + */ + public boolean getOneHandedModeActivated(ContentResolver resolver, int userId) { + return Settings.Secure.getIntForUser(resolver, + Settings.Secure.ONE_HANDED_MODE_ACTIVATED, 0, userId) == 1; + } + + /** + * Sets one handed activated or not to notify state for shortcut + * + * @return activated or not + */ + public boolean setOneHandedModeActivated(ContentResolver resolver, int state, int userId) { + return Settings.Secure.putIntForUser(resolver, + Settings.Secure.ONE_HANDED_MODE_ACTIVATED, state, userId); } void dump(PrintWriter pw, String prefix, ContentResolver resolver, @@ -145,6 +165,8 @@ public final class OneHandedSettingsUtil { pw.println(getSettingsOneHandedModeTimeout(resolver, userId)); pw.print(innerPrefix + "tapsAppToExit="); pw.println(getSettingsTapsAppToExit(resolver, userId)); + pw.print(innerPrefix + "shortcutActivated="); + pw.println(getOneHandedModeActivated(resolver, userId)); } public OneHandedSettingsUtil() { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java index c1282c9d7b125..99342f02463af 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java @@ -16,7 +16,9 @@ package com.android.wm.shell.onehanded; +import static com.android.wm.shell.onehanded.OneHandedState.STATE_ACTIVE; import static com.android.wm.shell.onehanded.OneHandedState.STATE_ENTERING; +import static com.android.wm.shell.onehanded.OneHandedState.STATE_EXITING; import static com.android.wm.shell.onehanded.OneHandedState.STATE_NONE; import static com.google.common.truth.Truth.assertThat; @@ -360,4 +362,71 @@ public class OneHandedControllerTest extends OneHandedTestCase { verify(mMockGestureHandler).onGestureEnabled(isOneHandedEnabled); } + + @Test + public void testStateActive_shortcutRequestActivate_skipActions() { + when(mSpiedTransitionState.getState()).thenReturn(STATE_ACTIVE); + when(mSpiedTransitionState.isTransitioning()).thenReturn(false); + when(mMockSettingsUitl.getOneHandedModeActivated(any(), anyInt())).thenReturn(true); + mSpiedOneHandedController.onActivatedActionChanged(); + + verify(mSpiedOneHandedController, never()).startOneHanded(); + verify(mSpiedOneHandedController, never()).stopOneHanded(); + } + + @Test + public void testStateNotActive_shortcutRequestInActivate_skipAction() { + when(mSpiedTransitionState.getState()).thenReturn(STATE_NONE); + when(mSpiedTransitionState.isTransitioning()).thenReturn(false); + when(mMockSettingsUitl.getOneHandedModeActivated(any(), anyInt())).thenReturn(false); + mSpiedOneHandedController.onActivatedActionChanged(); + + verify(mSpiedOneHandedController, never()).startOneHanded(); + verify(mSpiedOneHandedController, never()).stopOneHanded(); + } + + @Test + public void testStateNotActive_shortcutRequestActivate_doAction() { + when(mSpiedTransitionState.getState()).thenReturn(STATE_NONE); + when(mSpiedTransitionState.isTransitioning()).thenReturn(false); + when(mMockSettingsUitl.getOneHandedModeActivated(any(), anyInt())).thenReturn(true); + mSpiedOneHandedController.onActivatedActionChanged(); + + verify(mSpiedOneHandedController).startOneHanded(); + verify(mSpiedOneHandedController, never()).stopOneHanded(); + } + + @Test + public void testEnteringTransition_shortcutRequestActivate_skipActions() { + when(mSpiedTransitionState.getState()).thenReturn(STATE_ENTERING); + when(mSpiedTransitionState.isTransitioning()).thenReturn(true); + when(mMockSettingsUitl.getOneHandedModeActivated(any(), anyInt())).thenReturn(true); + mSpiedOneHandedController.onActivatedActionChanged(); + + verify(mSpiedOneHandedController, never()).startOneHanded(); + verify(mSpiedOneHandedController, never()).stopOneHanded(); + } + + @Test + public void testExitingTransition_shortcutRequestActivate_skipActions() { + when(mSpiedTransitionState.getState()).thenReturn(STATE_EXITING); + when(mSpiedTransitionState.isTransitioning()).thenReturn(true); + when(mMockSettingsUitl.getOneHandedModeActivated(any(), anyInt())).thenReturn(true); + mSpiedOneHandedController.onActivatedActionChanged(); + + verify(mSpiedOneHandedController, never()).startOneHanded(); + verify(mSpiedOneHandedController, never()).stopOneHanded(); + } + + @Test + public void testOneHandedDisabled_shortcutEnabled_skipActions() { + when(mSpiedOneHandedController.isOneHandedEnabled()).thenReturn(false); + when(mSpiedTransitionState.getState()).thenReturn(STATE_NONE); + when(mSpiedTransitionState.isTransitioning()).thenReturn(false); + when(mMockSettingsUitl.getOneHandedModeActivated(any(), anyInt())).thenReturn(true); + mSpiedOneHandedController.onActivatedActionChanged(); + + verify(mSpiedOneHandedController, never()).startOneHanded(); + verify(mSpiedOneHandedController, never()).stopOneHanded(); + } }