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

Commit e648b8a9 authored by susalin's avatar susalin Committed by Jean Chen
Browse files

feat(magnify_ime): Add Option to Magnify Navigation Bar and IME [2/2]

Users who use screen magnifier now have the option to magnify
their navigation bar and IME (e.g. keyboard). We implement this through
a new Secure Setting monitored through the AccessibilityManagerService,
which can be toggled by a user in the Settings.

NO_IFTTT=already sync it

Bug: 342509709
Flag: com.android.server.accessibility.enable_magnification_magnify_nav_bar_and_ime
Test: manually
Test: atest MagnifyNavAndImePreferenceControllerTest
Test: atest ToggleScreenMagnificationPreferenceFragmentTest
Change-Id: I880da997a7f23b746bc0ef3b4b5f072b7648d311
parent 2b72e413
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -5340,6 +5340,12 @@
    <string name="accessibility_screen_magnification_follow_typing_title">Magnify typing</string>
    <!-- Summary for accessibility follow typing preference for magnification. [CHAR LIMIT=none] -->
    <string name="accessibility_screen_magnification_follow_typing_summary">Magnifier follows text as you type</string>
    <!-- Title for accessibility magnifier preference allowing the magnification of navigation bar and IME. [CHAR LIMIT=35] -->
    <string name="accessibility_screen_magnification_nav_ime_title">Magnify keyboard</string>
    <!-- Summary for accessibility magnifier preference allowing the magnification of navigation bar and IME. [CHAR LIMIT=35] -->
    <string name="accessibility_screen_magnification_nav_ime_summary">Keyboard is magnified with your content</string>
    <!-- Summary for accessibility magnifier preference allowing the magnification of navigation bar and IME. [CHAR LIMIT=35] -->
    <string name="accessibility_screen_magnification_nav_ime_unavailable_summary">Unavailable while only magnifying part of the screen</string>
    <!-- Title for accessibility magnifier preference where the magnifier never turns off while switching apps. [CHAR LIMIT=60] -->
    <string name="accessibility_screen_magnification_always_on_title">Keep on while switching apps</string>
    <!-- Summary for accessibility magnifier preference where the magnifier never turns off while switching apps. [CHAR LIMIT=none] -->
+123 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.settings.accessibility;

import static com.android.settings.accessibility.AccessibilityUtil.State.OFF;
import static com.android.settings.accessibility.AccessibilityUtil.State.ON;

import android.content.Context;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;

import com.android.server.accessibility.Flags;
import com.android.settings.R;
import com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnPause;
import com.android.settingslib.core.lifecycle.events.OnResume;

/**
 * Controller that accesses and switches the preference status of
 * magnifying the nav bar and IME feature.
 */
public class MagnifyNavAndImePreferenceController extends MagnificationTogglePreferenceController
        implements LifecycleObserver, OnResume, OnPause {
    static final String PREF_KEY = "accessibility_magnify_nav_and_ime";

    private @Nullable Preference mPreference;

    @VisibleForTesting
    final ContentObserver mContentObserver = new ContentObserver(
            new Handler(Looper.getMainLooper())) {
        @Override
        public void onChange(boolean selfChange, @Nullable Uri uri) {
            if (mPreference != null) {
                updateState(mPreference);
            }
        }
    };

    public MagnifyNavAndImePreferenceController(@NonNull Context context,
            @NonNull String preferenceKey) {
        super(context, preferenceKey);
    }

    @Override
    public void onResume() {
        MagnificationCapabilities.registerObserver(mContext, mContentObserver);
    }

    @Override
    public void onPause() {
        MagnificationCapabilities.unregisterObserver(mContext, mContentObserver);
    }

    @Override
    public void displayPreference(@NonNull PreferenceScreen screen) {
        super.displayPreference(screen);
        mPreference = screen.findPreference(getPreferenceKey());
        updateState(mPreference);
    }

    @Override
    public int getAvailabilityStatus() {
        return Flags.enableMagnificationMagnifyNavBarAndIme()
                ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
    }

    @Override
    public boolean isChecked() {
        return Settings.Secure.getInt(mContext.getContentResolver(),
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MAGNIFY_NAV_AND_IME, OFF) == ON;
    }

    @Override
    public boolean setChecked(boolean isChecked) {
        return Settings.Secure.putInt(mContext.getContentResolver(),
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MAGNIFY_NAV_AND_IME,
                isChecked ? ON : OFF);
    }

    @Override
    public int getSliceHighlightMenuRes() {
        return R.string.menu_key_accessibility;
    }

    @Override
    public void updateState(@NonNull Preference preference) {
        super.updateState(preference);

        @MagnificationMode int mode = MagnificationCapabilities.getCapabilities(mContext);
        preference.setEnabled(
                mode == MagnificationMode.FULLSCREEN || mode == MagnificationMode.ALL);

        @StringRes int resId = preference.isEnabled()
                ? R.string.accessibility_screen_magnification_nav_ime_summary
                : R.string.accessibility_screen_magnification_nav_ime_unavailable_summary;
        preference.setSummary(mContext.getString(resId));
    }
}
+35 −0
Original line number Diff line number Diff line
@@ -213,12 +213,17 @@ public class ToggleScreenMagnificationPreferenceFragment extends
                && hasMouse();
    }

    private static boolean isMagnificationMagnifyNavAndImeSupported() {
        return com.android.server.accessibility.Flags.enableMagnificationMagnifyNavBarAndIme();
    }

    @Override
    protected void initSettingsPreference() {
        final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY);
        if (isWindowMagnificationSupported(getContext())) {
            // LINT.IfChange(preference_list)
            addMagnificationModeSetting(generalCategory);
            addMagnifyNavAndImeSetting(generalCategory);
            addFollowTypingSetting(generalCategory);
            addOneFingerPanningSetting(generalCategory);
            addAlwaysOnSetting(generalCategory);
@@ -276,6 +281,29 @@ public class ToggleScreenMagnificationPreferenceFragment extends
        super.onProcessArguments(arguments);
    }

    private static Preference createMagnifyNavAndImePreference(Context context) {
        var pref = new SwitchPreferenceCompat(context);
        pref.setTitle(R.string.accessibility_screen_magnification_nav_ime_title);
        pref.setSummary(R.string.accessibility_screen_magnification_nav_ime_summary);
        pref.setKey(MagnifyNavAndImePreferenceController.PREF_KEY);
        return pref;
    }

    private void addMagnifyNavAndImeSetting(PreferenceCategory generalCategory) {
        if (!Flags.enableMagnificationMagnifyNavBarAndIme()) {
            return;
        }

        generalCategory.addPreference(createMagnifyNavAndImePreference(getPrefContext()));

        var magnifyNavAndImePreferenceController =
                new MagnifyNavAndImePreferenceController(getContext(),
                        MagnifyNavAndImePreferenceController.PREF_KEY);
        getSettingsLifecycle().addObserver(magnifyNavAndImePreferenceController);
        magnifyNavAndImePreferenceController.displayPreference(getPreferenceScreen());
        addPreferenceController(magnifyNavAndImePreferenceController);
    }

    private static Preference createMagnificationModePreference(Context context) {
        final Preference pref = new Preference(context);
        pref.setTitle(R.string.accessibility_magnification_mode_title);
@@ -450,6 +478,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends

        var keysToObserve = List.of(
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_FOLLOW_TYPING_ENABLED,
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MAGNIFY_NAV_AND_IME,
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_ALWAYS_ON_ENABLED,
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_JOYSTICK_ENABLED
        );
@@ -708,6 +737,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends
                                    createOneFingerPanningPreference(context),
                                    createAlwaysOnPreference(context),
                                    createJoystickPreference(context),
                                    createMagnifyNavAndImePreference(context),
                                    createCursorFollowingPreference(context)
                            )
                            .forEach(pref ->
@@ -726,11 +756,16 @@ public class ToggleScreenMagnificationPreferenceFragment extends

                    if (!isWindowMagnificationSupported(context)) {
                        niks.add(MagnificationModePreferenceController.PREF_KEY);
                        niks.add(MagnifyNavAndImePreferenceController.PREF_KEY);
                        niks.add(MagnificationFollowTypingPreferenceController.PREF_KEY);
                        niks.add(MagnificationOneFingerPanningPreferenceController.PREF_KEY);
                        niks.add(MagnificationAlwaysOnPreferenceController.PREF_KEY);
                        niks.add(MagnificationJoystickPreferenceController.PREF_KEY);
                    } else {
                        if (!isMagnificationMagnifyNavAndImeSupported()) {
                            niks.add(MagnifyNavAndImePreferenceController.PREF_KEY);
                        }

                        if (!isAlwaysOnSupported(context)
                                // This preference's title "Keep on while switching apps" does not
                                // mention magnification so it may confuse users who search a term
+184 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.settings.accessibility;

import static com.android.settings.accessibility.AccessibilityUtil.State.OFF;
import static com.android.settings.accessibility.AccessibilityUtil.State.ON;
import static com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode;
import static com.android.settings.core.BasePreferenceController.AVAILABLE;
import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;

import android.content.Context;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;

import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;
import androidx.test.core.app.ApplicationProvider;

import com.android.server.accessibility.Flags;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowContentResolver;

@RunWith(RobolectricTestRunner.class)
public class MagnifyNavAndImePreferenceControllerTest {
    private static final String KEY_MAGNIFY_NAV_AND_IME =
            Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MAGNIFY_NAV_AND_IME;

    private Context mContext;
    private ShadowContentResolver mShadowContentResolver;
    private SwitchPreference mSwitchPreference;
    private MagnifyNavAndImePreferenceController mController;

    @Rule
    public final SetFlagsRule mSetFlagsRule =
            new SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT);

    @Before
    public void setUp() {
        mContext = ApplicationProvider.getApplicationContext();
        mShadowContentResolver = Shadow.extract(mContext.getContentResolver());

        final PreferenceManager preferenceManager = new PreferenceManager(mContext);
        final PreferenceScreen screen = preferenceManager.createPreferenceScreen(mContext);
        mSwitchPreference = spy(new SwitchPreference(mContext));
        mSwitchPreference.setKey(MagnifyNavAndImePreferenceController.PREF_KEY);
        screen.addPreference(mSwitchPreference);

        mController = new MagnifyNavAndImePreferenceController(mContext,
                MagnifyNavAndImePreferenceController.PREF_KEY);
        mController.displayPreference(screen);
        mController.updateState(mSwitchPreference);

        reset(mSwitchPreference);
    }

    @Test
    @DisableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_MAGNIFY_NAV_BAR_AND_IME)
    public void getAvailabilityStatus_defaultState_disabled() {
        int status = mController.getAvailabilityStatus();

        assertThat(status).isEqualTo(CONDITIONALLY_UNAVAILABLE);
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_MAGNIFY_NAV_BAR_AND_IME)
    public void getAvailabilityStatus_featureFlagEnabled_enabled() {
        int status = mController.getAvailabilityStatus();

        assertThat(status).isEqualTo(AVAILABLE);
    }

    @Test
    public void isChecked_settingsEnabled_returnTrue() {
        Settings.Secure.putInt(mContext.getContentResolver(), KEY_MAGNIFY_NAV_AND_IME, ON);
        mController.updateState(mSwitchPreference);

        verify(mSwitchPreference).setChecked(true);
        assertThat(mController.isChecked()).isTrue();
        assertThat(mSwitchPreference.isChecked()).isTrue();
    }

    @Test
    public void isChecked_settingsDisabled_returnTrue() {
        Settings.Secure.putInt(mContext.getContentResolver(), KEY_MAGNIFY_NAV_AND_IME, OFF);
        mController.updateState(mSwitchPreference);

        verify(mSwitchPreference).setChecked(false);
        assertThat(mController.isChecked()).isFalse();
        assertThat(mSwitchPreference.isChecked()).isFalse();
    }

    @Test
    public void performClick_switchDisabled_shouldReturnEnable() {
        Settings.Secure.putInt(mContext.getContentResolver(), KEY_MAGNIFY_NAV_AND_IME, OFF);
        mController.updateState(mSwitchPreference);

        mSwitchPreference.performClick();

        verify(mSwitchPreference).setChecked(true);
        //assertThat(mController.isChecked()).isTrue();
        assertThat(mSwitchPreference.isChecked()).isTrue();
    }

    @Test
    public void performClick_switchEnabled_shouldReturnDisable() {
        Settings.Secure.putInt(mContext.getContentResolver(), KEY_MAGNIFY_NAV_AND_IME, ON);
        mController.updateState(mSwitchPreference);

        mSwitchPreference.performClick();

        verify(mSwitchPreference).setChecked(false);
        //assertThat(mController.isChecked()).isFalse();
        assertThat(mSwitchPreference.isChecked()).isFalse();
    }

    @Test
    public void onResume_verifyRegisterCapabilityObserver() {
        mController.onResume();
        assertThat(mShadowContentResolver.getContentObservers(
                Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY)))
                .hasSize(1);
    }

    @Test
    public void onPause_verifyUnregisterCapabilityObserver() {
        mController.onResume();
        mController.onPause();
        assertThat(mShadowContentResolver.getContentObservers(
                Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY)))
                .isEmpty();
    }
    @Test
    public void updateState_windowModeOnly_preferenceBecomesUnavailable() {
        MagnificationCapabilities.setCapabilities(mContext, MagnificationMode.WINDOW);

        mController.updateState(mSwitchPreference);
        assertThat(mSwitchPreference.isEnabled()).isFalse();
    }

    @Test
    public void updateState_fullscreenModeOnly_preferenceIsAvailable() {
        MagnificationCapabilities.setCapabilities(mContext, MagnificationMode.FULLSCREEN);

        mController.updateState(mSwitchPreference);
        assertThat(mSwitchPreference.isEnabled()).isTrue();
    }

    @Test
    public void updateState_switchMode_preferenceIsAvailable() {
        MagnificationCapabilities.setCapabilities(mContext, MagnificationMode.ALL);

        mController.updateState(mSwitchPreference);
        assertThat(mSwitchPreference.isEnabled()).isTrue();
    }
}
+41 −1
Original line number Diff line number Diff line
@@ -137,6 +137,8 @@ public class ToggleScreenMagnificationPreferenceFragmentTest {
            Settings.Secure.ACCESSIBILITY_MAGNIFICATION_ALWAYS_ON_ENABLED;
    private static final String KEY_JOYSTICK =
            Settings.Secure.ACCESSIBILITY_MAGNIFICATION_JOYSTICK_ENABLED;
    private static final String KEY_MAGNIFY_NAV_AND_IME =
            Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MAGNIFY_NAV_AND_IME;

    private FragmentController<ToggleScreenMagnificationPreferenceFragment> mFragController;
    private Context mContext;
@@ -227,6 +229,35 @@ public class ToggleScreenMagnificationPreferenceFragmentTest {
        assertThat(switchPreference.isChecked()).isFalse();
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_MAGNIFY_NAV_BAR_AND_IME)
    public void onResume_disableMagnifyNavAndIme_preferenceNotChecked() {
        setKeyMagnifyNavAndImeEnabled(false);

        mFragController.create(R.id.main_content, /* bundle= */ null).start().resume();

        final TwoStatePreference switchPreference =
                mFragController.get().findPreference(
                        MagnifyNavAndImePreferenceController.PREF_KEY);

        assertThat(switchPreference).isNotNull();
        assertThat(switchPreference.isChecked()).isFalse();
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_MAGNIFY_NAV_BAR_AND_IME)
    public void onResume_enableMagnifyNavAndIme_preferenceIsChecked() {
        setKeyMagnifyNavAndImeEnabled(true);

        mFragController.create(R.id.main_content, /* bundle= */ null).start().resume();

        final TwoStatePreference switchPreference =
                mFragController.get().findPreference(
                        MagnifyNavAndImePreferenceController.PREF_KEY);
        assertThat(switchPreference).isNotNull();
        assertThat(switchPreference.isChecked()).isTrue();
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_ONE_FINGER_PANNING_GESTURE)
    public void onResume_defaultStateForOneFingerPan_switchPreferenceShouldReturnFalse() {
@@ -352,6 +383,8 @@ public class ToggleScreenMagnificationPreferenceFragmentTest {
                        Settings.Secure.ACCESSIBILITY_QS_TARGETS),
                Settings.Secure.getUriFor(
                        Settings.Secure.ACCESSIBILITY_MAGNIFICATION_FOLLOW_TYPING_ENABLED),
                Settings.Secure.getUriFor(
                        Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MAGNIFY_NAV_AND_IME),
                Settings.Secure.getUriFor(
                        Settings.Secure.ACCESSIBILITY_MAGNIFICATION_ALWAYS_ON_ENABLED)
        };
@@ -863,7 +896,8 @@ public class ToggleScreenMagnificationPreferenceFragmentTest {
                MagnificationOneFingerPanningPreferenceController.PREF_KEY,
                MagnificationAlwaysOnPreferenceController.PREF_KEY,
                MagnificationJoystickPreferenceController.PREF_KEY,
                MagnificationCursorFollowingModePreferenceController.PREF_KEY);
                MagnificationCursorFollowingModePreferenceController.PREF_KEY,
                MagnifyNavAndImePreferenceController.PREF_KEY);

        final List<SearchIndexableRaw> rawData = ToggleScreenMagnificationPreferenceFragment
                .SEARCH_INDEX_DATA_PROVIDER.getRawDataToIndex(mContext, true);
@@ -894,6 +928,7 @@ public class ToggleScreenMagnificationPreferenceFragmentTest {
    @EnableFlags({
            com.android.settings.accessibility.Flags.FLAG_FIX_A11Y_SETTINGS_SEARCH,
            Flags.FLAG_ENABLE_MAGNIFICATION_ONE_FINGER_PANNING_GESTURE,
            Flags.FLAG_ENABLE_MAGNIFICATION_MAGNIFY_NAV_BAR_AND_IME,
            com.android.settings.accessibility.Flags
                    .FLAG_ENABLE_MAGNIFICATION_CURSOR_FOLLOWING_DIALOG})
    @Config(shadows = ShadowInputDevice.class)
@@ -980,6 +1015,11 @@ public class ToggleScreenMagnificationPreferenceFragmentTest {
                enabled ? ON : OFF);
    }

    private void setKeyMagnifyNavAndImeEnabled(boolean enabled) {
        Settings.Secure.putInt(mContext.getContentResolver(), KEY_MAGNIFY_NAV_AND_IME,
                enabled ? ON : OFF);
    }

    private void setKeyOneFingerPanEnabled(boolean enabled) {
        Settings.Secure.putInt(mContext.getContentResolver(), KEY_SINGLE_FINGER_PANNING,
                enabled ? ON : OFF);