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

Commit 20d1a7d1 authored by Yongshun Liu's avatar Yongshun Liu
Browse files

a11y: Add cursor following mode dialog

This a pure UI change that adds a new magnification cursor following
mode dialog behind a flag. The framework support will be added
separately later.

There are 3 modes as the following:
  - continuous mode
  - center mode
  - edge mode

It also renames magnification mode dialog xml file for general purpose
within accessibility.

NO_IFTTT=linter not working

Bug: b/388335935
Flag: com.android.settings.accessibility.enable_magnification_cursor_following_dialog
Test: SettingsRoboTests:com.android.settings.accessibility.ToggleScreenMagnificationPreferenceFragmentTest &&
      SettingsRoboTests:com.android.settings.accessibility.MagnificationModePreferenceControllerTest &&
      SettingsRoboTests:com.android.settings.accessibility.MagnificationCursorFollowingModePreferenceControllerTest
Change-Id: If2672186faf7443cc210d79630b1ea4f3808d7e4
parent 43e0a045
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -21,9 +21,9 @@
    android:padding="?android:attr/dialogPreferredPadding">

    <TextView
        android:id="@+id/accessibility_dialog_header_text_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/accessibility_magnification_area_settings_message"
        android:textSize="16sp"
        style="?android:attr/textAppearanceMedium"
        android:textColor="?android:attr/textColorAlertDialogListItem"/>
+10 −0
Original line number Diff line number Diff line
@@ -5217,6 +5217,16 @@
    <string name="accessibility_screen_magnification_title">Magnification</string>
    <!-- Title for accessibility shortcut preference for magnification. [CHAR LIMIT=60] -->
    <string name="accessibility_screen_magnification_shortcut_title">Magnification shortcut</string>
    <!-- Title of cursor following mode preference for magnification. [CHAR LIMIT=60] -->
    <string name="accessibility_magnification_cursor_following_title">Cursor following</string>
    <!-- Header message of cursor following mode dialog for magnification. [CHAR LIMIT=none] -->
    <string name="accessibility_magnification_cursor_following_header">Choose how Magnification follows your cursor.</string>
    <!-- Option title of cursor following continuous mode in the mode selection dialog. [CHAR LIMIT=none] -->
    <string name="accessibility_magnification_cursor_following_continuous">Move screen continuously as mouse moves</string>
    <!-- Option title of cursor following center mode in the mode selection dialog. [CHAR LIMIT=none] -->
    <string name="accessibility_magnification_cursor_following_center">Move screen keeping mouse at center of screen</string>
    <!-- Option title of cursor following edge mode in the mode selection dialog. [CHAR LIMIT=none] -->
    <string name="accessibility_magnification_cursor_following_edge">Move screen when mouse touches edges of screen</string>
    <!-- Title for accessibility follow typing preference for magnification. [CHAR LIMIT=35] -->
    <string name="accessibility_screen_magnification_follow_typing_title">Magnify typing</string>
    <!-- Summary for accessibility follow typing preference for magnification. [CHAR LIMIT=none] -->
+5 −0
Original line number Diff line number Diff line
@@ -104,6 +104,11 @@ public class AccessibilityDialogUtils {
         * screen / Switch between full and partial screen > Save.
         */
        int DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING = 1011;

        /**
         * OPEN: Settings > Accessibility > Magnification > Cursor following.
         */
        int DIALOG_MAGNIFICATION_CURSOR_FOLLOWING_MODE = 1012;
    }

    /**
+221 −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 android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.DialogInterface;
import android.provider.Settings;
import android.provider.Settings.Secure.AccessibilityMagnificationCursorFollowingMode;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.util.Preconditions;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;

import com.android.settings.DialogCreatable;
import com.android.settings.R;
import com.android.settings.accessibility.AccessibilityDialogUtils.DialogEnums;
import com.android.settings.core.BasePreferenceController;

import java.util.ArrayList;
import java.util.List;

/**
 * Controller that shows the magnification cursor following mode and the preference click behavior.
 */
public class MagnificationCursorFollowingModePreferenceController extends
        BasePreferenceController implements DialogCreatable {
    static final String PREF_KEY =
            Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE;

    private static final String TAG =
            MagnificationCursorFollowingModePreferenceController.class.getSimpleName();

    private final List<ModeInfo> mModeList = new ArrayList<>();
    @Nullable
    private DialogHelper mDialogHelper;
    @VisibleForTesting
    @Nullable
    ListView mModeListView;
    @Nullable
    private Preference mModePreference;

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

    public void setDialogHelper(@NonNull DialogHelper dialogHelper) {
        mDialogHelper = dialogHelper;
    }

    private void initModeList() {
        mModeList.add(new ModeInfo(mContext.getString(
                R.string.accessibility_magnification_cursor_following_continuous),
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS));
        mModeList.add(new ModeInfo(
                mContext.getString(R.string.accessibility_magnification_cursor_following_center),
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CENTER));
        mModeList.add(new ModeInfo(
                mContext.getString(R.string.accessibility_magnification_cursor_following_edge),
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE));
    }

    @Override
    public int getAvailabilityStatus() {
        return AVAILABLE;
    }

    @NonNull
    @Override
    public CharSequence getSummary() {
        return getCursorFollowingModeSummary(getCurrentMagnificationCursorFollowingMode());
    }

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

    @Override
    public boolean handlePreferenceTreeClick(@NonNull Preference preference) {
        if (!TextUtils.equals(preference.getKey(), getPreferenceKey()) || mModePreference == null) {
            return super.handlePreferenceTreeClick(preference);
        }

        Preconditions.checkNotNull(mDialogHelper).showDialog(
                    DialogEnums.DIALOG_MAGNIFICATION_CURSOR_FOLLOWING_MODE);
        return true;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(int dialogId) {
        Preconditions.checkArgument(
                dialogId == DialogEnums.DIALOG_MAGNIFICATION_CURSOR_FOLLOWING_MODE,
                "This only handles cursor following mode dialog");
        return createMagnificationCursorFollowingModeDialog();
    }

    @Override
    public int getDialogMetricsCategory(int dialogId) {
        Preconditions.checkArgument(
                dialogId == DialogEnums.DIALOG_MAGNIFICATION_CURSOR_FOLLOWING_MODE,
                "This only handles cursor following mode dialog");
        return SettingsEnums.DIALOG_MAGNIFICATION_CURSOR_FOLLOWING;
    }

    @NonNull
    private Dialog createMagnificationCursorFollowingModeDialog() {
        mModeListView = AccessibilityDialogUtils.createSingleChoiceListView(mContext, mModeList,
                /* itemListener= */null);
        final View headerView = LayoutInflater.from(mContext).inflate(
                R.layout.accessibility_dialog_header, mModeListView,
                /* attachToRoot= */false);
        final TextView textView = Preconditions.checkNotNull(headerView.findViewById(
                R.id.accessibility_dialog_header_text_view));
        textView.setText(
                mContext.getString(R.string.accessibility_magnification_cursor_following_header));
        textView.setVisibility(View.VISIBLE);
        mModeListView.addHeaderView(headerView, /* data= */null, /* isSelectable= */false);
        final int selectionIndex = computeSelectionIndex();
        if (selectionIndex != AdapterView.INVALID_POSITION) {
            mModeListView.setItemChecked(selectionIndex, /* value= */true);
        }
        final CharSequence title = mContext.getString(
                R.string.accessibility_magnification_cursor_following_title);
        final CharSequence positiveBtnText = mContext.getString(R.string.save);
        final CharSequence negativeBtnText = mContext.getString(R.string.cancel);
        return AccessibilityDialogUtils.createCustomDialog(mContext, title, mModeListView,
                positiveBtnText,
                this::onMagnificationCursorFollowingModeDialogPositiveButtonClicked,
                negativeBtnText, /* negativeListener= */null);
    }

    void onMagnificationCursorFollowingModeDialogPositiveButtonClicked(
            DialogInterface dialogInterface, int which) {
        ListView listView = Preconditions.checkNotNull(mModeListView);
        final int selectionIndex = listView.getCheckedItemPosition();
        if (selectionIndex == AdapterView.INVALID_POSITION) {
            Log.w(TAG, "Selected positive button with INVALID_POSITION index");
            return;
        }
        ModeInfo cursorFollowingMode = (ModeInfo) listView.getItemAtPosition(selectionIndex);
        if (cursorFollowingMode != null) {
            Preconditions.checkNotNull(mModePreference).setSummary(
                    getCursorFollowingModeSummary(cursorFollowingMode.mMode));
            Settings.Secure.putInt(mContext.getContentResolver(), PREF_KEY,
                    cursorFollowingMode.mMode);
        }
    }

    private int computeSelectionIndex() {
        ListView listView = Preconditions.checkNotNull(mModeListView);
        @AccessibilityMagnificationCursorFollowingMode
        final int currentMode = getCurrentMagnificationCursorFollowingMode();
        for (int i = 0; i < listView.getCount(); i++) {
            final ModeInfo mode = (ModeInfo) listView.getItemAtPosition(i);
            if (mode != null && mode.mMode == currentMode) {
                return i;
            }
        }
        return AdapterView.INVALID_POSITION;
    }

    @NonNull
    private CharSequence getCursorFollowingModeSummary(
            @AccessibilityMagnificationCursorFollowingMode int cursorFollowingMode) {
        int stringId = switch (cursorFollowingMode) {
            case Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CENTER ->
                    R.string.accessibility_magnification_cursor_following_center;
            case Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_EDGE ->
                    R.string.accessibility_magnification_cursor_following_edge;
            default ->
                    R.string.accessibility_magnification_cursor_following_continuous;
        };
        return mContext.getString(stringId);
    }

    private @AccessibilityMagnificationCursorFollowingMode int
            getCurrentMagnificationCursorFollowingMode() {
        return Settings.Secure.getInt(mContext.getContentResolver(), PREF_KEY,
                Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CURSOR_FOLLOWING_MODE_CONTINUOUS);
    }

    static class ModeInfo extends ItemInfoArrayAdapter.ItemInfo {
        @AccessibilityMagnificationCursorFollowingMode
        public final int mMode;

        ModeInfo(@NonNull CharSequence title,
                @AccessibilityMagnificationCursorFollowingMode int mode) {
            super(title, /* summary= */null, /* drawableId= */null);
            mMode = mode;
        }
    }
}
+6 −2
Original line number Diff line number Diff line
@@ -176,8 +176,12 @@ public class MagnificationModePreferenceController extends BasePreferenceControl
                mContext, mModeInfos, this::onMagnificationModeSelected);

        final View headerView = LayoutInflater.from(mContext).inflate(
                R.layout.accessibility_magnification_mode_header,
                getMagnificationModesListView(), /* attachToRoot= */false);
                R.layout.accessibility_dialog_header, getMagnificationModesListView(),
                /* attachToRoot= */false);
        final TextView textView = Preconditions.checkNotNull(headerView.findViewById(
                R.id.accessibility_dialog_header_text_view));
        textView.setText(
                mContext.getString(R.string.accessibility_magnification_area_settings_message));
        getMagnificationModesListView().addHeaderView(headerView, /* data= */null,
                /* isSelectable= */false);

Loading