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

Commit 55864923 authored by Yuhan Yang's avatar Yuhan Yang Committed by Android (Google) Code Review
Browse files

Merge "Add mouse keys max speed seek bar" into main

parents 3e90b62b 8f0bb67f
Loading
Loading
Loading
Loading
+76 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  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.
-->

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:gravity="center_horizontal"
        android:text="@string/mouse_keys_max_speed_title"
        android:textColor="?androidprv:attr/textColorPrimary"
        android:textSize="20sp"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical">

        <ImageView
            android:id="@+id/shorter"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:background="?android:attr/selectableItemBackgroundBorderless"
            android:contentDescription="@string/mouse_keys_seek_bar_decrease_button_description"
            android:src="@drawable/ic_remove_24dp"
            android:tint="?android:attr/textColorPrimary"
            android:tintMode="src_in"
            android:scaleType="center"
            android:focusable="true" />

        <SeekBar
            android:id="@+id/max_speed_seekbar"
            android:max="10"
            android:min="0"
            android:layout_width="0dp"
            android:layout_height="48dp"
            android:layout_weight="1"
            android:contentDescription="@string/mouse_keys_max_speed_title" />

        <ImageView
            android:id="@+id/longer"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:background="?android:attr/selectableItemBackgroundBorderless"
            android:contentDescription="@string/mouse_keys_seek_bar_increase_button_description"
            android:src="@drawable/ic_add_24dp"
            android:tint="?android:attr/textColorPrimary"
            android:tintMode="src_in"
            android:scaleType="center"
            android:focusable="true" />

    </LinearLayout>

</LinearLayout>
+6 −0
Original line number Diff line number Diff line
@@ -4890,6 +4890,12 @@
    <string name="accessibility_mouse_keys_shortcut_title">Mouse keys shortcut</string>
    <!-- Summary text for the accessibility setting 'Mouse keys' preference sub-screen. [CHAR LIMIT=100] -->
    <string name="mouse_keys_summary">Use your keyboard to control the pointer</string>
    <!-- Title for the mouse keys max speed slider preference under mouse key main page, an accessibility mouse keys setting that controls how fast the mouse key can move. [CHAR LIMIT=35] -->
    <string name="mouse_keys_max_speed_title">Maximum speed</string>
    <!-- Description for the button that decreases the seekbar in mouse keys settings page. [CHAR_LIMIT=NONE] -->
    <string name="mouse_keys_seek_bar_decrease_button_description">Decrease</string>
    <!-- Description for the button that increases the seekbar in mouse keys settings page. [CHAR_LIMIT=NONE] -->
    <string name="mouse_keys_seek_bar_increase_button_description">Increase</string>
    <!-- Title for the 'Mouse reverse scrolling' preference switch, which reverses the direction of mouse scroll wheels so that moving the wheel up scrolls the content down. [CHAR LIMIT=60] -->
    <string name="mouse_reverse_vertical_scrolling">Reverse scrolling</string>
    <!-- Summary text for the 'Mouse reverse scrolling' preference switch indicating to users that when the setting is enabled that scrolling up with their mouse wheel will move the page content down. [CHAR LIMIT=NONE] -->
+6 −0
Original line number Diff line number Diff line
@@ -37,6 +37,12 @@
        android:layout="@layout/mouse_keys_acceleration_seekbar"
        settings:controller="com.android.settings.inputmethod.MouseKeysAccelerationController"/>

    <com.android.settingslib.widget.LayoutPreference
        android:key="mouse_keys_max_speed_seekbar"
        android:layout="@layout/mouse_keys_max_speed_seekbar"
        android:selectable="false"
        settings:controller="com.android.settings.inputmethod.MouseKeysMaxSpeedController"/>

    <com.android.settingslib.widget.LayoutPreference
        android:key="mouse_keys_list"
        android:layout="@layout/mouse_keys_image_list"/>
+113 −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.inputmethod;

import android.content.ContentResolver;
import android.content.Context;
import android.hardware.input.InputSettings;
import android.provider.Settings;
import android.widget.ImageView;
import android.widget.SeekBar;

import androidx.annotation.NonNull;
import androidx.preference.PreferenceScreen;

import com.android.server.accessibility.Flags;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.widget.LayoutPreference;

/** Controller class that controls mouse keys max speed seekbar settings. */
public class MouseKeysMaxSpeedController extends BasePreferenceController {

    private static final int SEEK_BAR_STEP = 1;

    private final @NonNull ContentResolver mContentResolver;

    public MouseKeysMaxSpeedController(@NonNull Context context, @NonNull String preferenceKey) {
        super(context, preferenceKey);
        mContentResolver = context.getContentResolver();
    }

    @Override
    public void displayPreference(@NonNull PreferenceScreen screen) {
        super.displayPreference(screen);
        final LayoutPreference preference = screen.findPreference(getPreferenceKey());

        final int maxSpeedFromSettings = getMaxSpeedFromSettings();
        // Initialize seek bar preference. Sets seek bar size to the number of possible delay
        // values.
        @NonNull SeekBar seekBar = preference.findViewById(R.id.max_speed_seekbar);
        seekBar.setProgress(maxSpeedFromSettings);
        seekBar.setOnSeekBarChangeListener(
            new SeekBar.OnSeekBarChangeListener() {

                @Override
                public void onProgressChanged(@NonNull SeekBar seekBar, int progress,
                        boolean fromUser) {
                    updateMaxSpeedValue(seekBar, progress);
                }

                @Override
                public void onStartTrackingTouch(@NonNull SeekBar seekBar) {
                    // Nothing to do.
                }

                @Override
                public void onStopTrackingTouch(@NonNull SeekBar seekBar) {
                    // Nothing to do.
                }
            });

        @NonNull ImageView mShorter = preference.findViewById(R.id.shorter);
        mShorter.setOnClickListener(v -> minusDelayByImageView(seekBar));

        @NonNull ImageView mLonger = preference.findViewById(R.id.longer);
        mLonger.setOnClickListener(v -> plusDelayByImageView(seekBar));
    }

    private void updateMaxSpeedValue(@NonNull SeekBar seekBar, int position) {
        Settings.Secure.putInt(mContentResolver,
                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_MAX_SPEED, position);
        seekBar.setProgress(position);
    }

    private void minusDelayByImageView(@NonNull SeekBar seekBar) {
        final int maxSpeed = getMaxSpeedFromSettings();
        if (maxSpeed > seekBar.getMin()) {
            updateMaxSpeedValue(seekBar, maxSpeed - SEEK_BAR_STEP);
        }
    }

    private void plusDelayByImageView(@NonNull SeekBar seekBar) {
        final int maxSpeed = getMaxSpeedFromSettings();
        if (maxSpeed < seekBar.getMax()) {
            updateMaxSpeedValue(seekBar, maxSpeed + SEEK_BAR_STEP);
        }
    }

    private int getMaxSpeedFromSettings() {
        return Settings.Secure.getInt(mContentResolver,
                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_MAX_SPEED,
                InputSettings.DEFAULT_MOUSE_KEYS_MAX_SPEED);
    }

    @Override
    public int getAvailabilityStatus() {
        return Flags.enableMouseKeyEnhancement() ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
    }
}
+168 −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.inputmethod;

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.doReturn;

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 android.widget.ImageView;
import android.widget.SeekBar;

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

import com.android.settings.R;
import com.android.settingslib.widget.LayoutPreference;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;

/** Tests for {@link MouseKeysMaxSpeedController}. */
@RunWith(RobolectricTestRunner.class)
public class MouseKeysMaxSpeedControllerTest {

    private static final String KEY_CUSTOM_SEEKBAR = "mouse_keys_max_speed_seekbar";

    @Rule
    public final MockitoRule mMockitoRule = MockitoJUnit.rule();
    @Rule
    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
    @Mock
    private PreferenceScreen mScreen;
    @Mock
    private LayoutPreference mLayoutPreference;
    @Spy
    private Context mContext = ApplicationProvider.getApplicationContext();
    private ImageView mShorter;
    private ImageView mLonger;
    private SeekBar mSeekBar;
    private MouseKeysMaxSpeedController mController;

    @Before
    public void setUp() {
        mShorter = new ImageView(mContext);
        mLonger = new ImageView(mContext);
        mSeekBar = new SeekBar(mContext);
        mController = new MouseKeysMaxSpeedController(mContext, KEY_CUSTOM_SEEKBAR);
        doReturn(mLayoutPreference).when(mScreen).findPreference(KEY_CUSTOM_SEEKBAR);
        doReturn(mSeekBar).when(mLayoutPreference).findViewById(R.id.max_speed_seekbar);
        doReturn(mShorter).when(mLayoutPreference).findViewById(R.id.shorter);
        doReturn(mLonger).when(mLayoutPreference).findViewById(R.id.longer);
    }

    @Test
    @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_MOUSE_KEY_ENHANCEMENT)
    public void getAvailabilityStatus_available_whenFlagOn() {
        assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
    }

    @Test
    @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_MOUSE_KEY_ENHANCEMENT)
    public void getAvailabilityStatus_unavailable_whenFlagOff() {
        assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE);
    }

    @Test
    public void displayPreference_initSeekBar() {
        Settings.Secure.putInt(
                mContext.getContentResolver(),
                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_MAX_SPEED, 5);
        mController.displayPreference(mScreen);

        assertThat(mSeekBar.getProgress()).isEqualTo(5);
    }

    @Test
    public void onSettingsChanged_updateMaxSpeedValue() {
        Settings.Secure.putInt(
                mContext.getContentResolver(),
                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_MAX_SPEED, 5);

        mController.displayPreference(mScreen);
        final int actualDelayValue =
                Settings.Secure.getInt(mContext.getContentResolver(),
                        Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_MAX_SPEED, /* def= */ 0);

        assertThat(mSeekBar.getProgress()).isEqualTo(5);
        assertThat(actualDelayValue).isEqualTo(5);
    }

    @Test
    public void onSeekBarProgressChanged_updateMaxSpeedValue() {
        Settings.Secure.putInt(
                mContext.getContentResolver(),
                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_MAX_SPEED, 5);
        mController.displayPreference(mScreen);
        // mController.mSeekBarChangeListener.onProgressChanged(mock(SeekBar.class),
        //         /* value= */ 8,
        //         true);
        mSeekBar.setProgress(8);
        final int actualDelayValue =
                Settings.Secure.getInt(mContext.getContentResolver(),
                        Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_MAX_SPEED, /* def= */ 0);

        assertThat(mSeekBar.getProgress()).isEqualTo(8);
        assertThat(actualDelayValue).isEqualTo(8);
    }

    @Test
    public void onShorterClicked_updateMaxSpeedValue() {
        Settings.Secure.putInt(
                mContext.getContentResolver(),
                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_MAX_SPEED, 5);
        mController.displayPreference(mScreen);
        mShorter.callOnClick();
        final int actualDelayValue =
                Settings.Secure.getInt(mContext.getContentResolver(),
                        Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_MAX_SPEED, /* def= */ 0);

        assertThat(mSeekBar.getProgress()).isEqualTo(4);
        assertThat(actualDelayValue).isEqualTo(4);
    }

    @Test
    public void onLongerClicked_updateMaxSpeedValue() {
        Settings.Secure.putInt(
                mContext.getContentResolver(),
                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_MAX_SPEED, 5);

        mController.displayPreference(mScreen);
        mLonger.callOnClick();
        final int actualDelayValue =
                Settings.Secure.getInt(mContext.getContentResolver(),
                        Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_MAX_SPEED, /* def= */ 0);

        assertThat(mSeekBar.getProgress()).isEqualTo(6);
        assertThat(actualDelayValue).isEqualTo(6);
    }
}