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

Commit 1f63639f authored by Danny Wang's avatar Danny Wang
Browse files

Add custom seekbar for mouse keys acceleration

Create the UI for mouse keys accleration seekbar.
Will implement the logic in a follow up cl.

screenshots:
light mode: http://screen/7wCXoShFyuizs7D
dark mode: http://screen/B6YRZ6LJpToQbFk

Bug: 393566398
Test: verified ui on the test device
Flag: com.android.server.accessibility.enable_mouse_key_enhancement
Change-Id: I09fc620ca068e6313aac92312a50149eb1374413
parent f58e5c22
Loading
Loading
Loading
Loading
+79 −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.
-->
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="8dp"
    android:paddingBottom="16dp">

    <TextView
        android:id="@+id/mouse_keys_acceleration"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="8dp"
        android:padding="8dp"
        android:text="@string/mouse_keys_acceleration_seekbar_title"
        android:textAppearance="?android:attr/textAppearanceListItem"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toTopOf="@id/mouse_keys_acceleration_seekbar" />

    <ImageView
        android:id="@+id/slow_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:background="?android:attr/selectableItemBackgroundBorderless"
        android:src="@drawable/ic_remove_24dp"
        android:layout_marginStart="16dp"
        android:contentDescription="@string/mouse_keys_acceleration_slow_icon_desc"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@id/mouse_keys_acceleration_seekbar"
        app:layout_constraintBottom_toBottomOf="@id/mouse_keys_acceleration_seekbar" />

    <ImageView
        android:id="@+id/fast_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:background="?android:attr/selectableItemBackgroundBorderless"
        android:src="@drawable/ic_add_24dp"
        android:contentDescription="@string/mouse_keys_acceleration_fast_icon_desc"
        android:layout_marginEnd="16dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="@id/mouse_keys_acceleration_seekbar"
        app:layout_constraintBottom_toBottomOf="@id/mouse_keys_acceleration_seekbar" />

    <SeekBar
        android:id="@+id/mouse_keys_acceleration_seekbar"
        style="@android:style/Widget.Material.SeekBar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp"
        android:max="10"
        android:min="0"
        app:layout_constraintTop_toBottomOf="@id/mouse_keys_acceleration"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@id/slow_icon"
        app:layout_constraintEnd_toStartOf="@id/fast_icon" />
</androidx.constraintlayout.widget.ConstraintLayout>
+6 −0
Original line number Diff line number Diff line
@@ -4893,6 +4893,12 @@
    <!-- Summary text for Mouse keys click secondary button image. [CHAR LIMIT=NONE] -->
    <string name="mouse_keys_release2_summary">Use the \"<xliff:g id="release_2_label" example="/">%s</xliff:g>\" key to click the secondary mouse button</string>
    <!-- TODO(b/411206889): finalize this string and mark it translatable. -->
    <!-- Title for the mouse keys acceleration seekbar -->
    <string name="mouse_keys_acceleration_seekbar_title" translatable="false">Acceleration</string>
    <string name="mouse_keys_acceleration_fast_icon_desc" translatable="false">Fast</string>
    <string name="mouse_keys_acceleration_slow_icon_desc" translatable="false">Slow</string>
    <!-- Title for the button to trigger the 'keyboard shortcuts helper' dialog. [CHAR LIMIT=35] -->
    <string name="keyboard_shortcuts_helper">View keyboard shortcuts</string>
    <!-- Summary text for the 'keyboard shortcuts helper' dialog. [CHAR LIMIT=100] -->
+5 −0
Original line number Diff line number Diff line
@@ -26,6 +26,11 @@
        android:title="@string/mouse_keys_main_title"
        settings:controller="com.android.settings.inputmethod.KeyboardAccessibilityMouseKeysController"/>

    <com.android.settingslib.widget.LayoutPreference
        android:key="mouse_keys_acceleration_seekbar"
        android:layout="@layout/mouse_keys_acceleration_seekbar"
        settings:controller="com.android.settings.inputmethod.MouseKeysAccelerationController"/>

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

import java.util.Objects;

/** Controller class that controls mouse keys acceleration seekbar settings. */
public class MouseKeysAccelerationController extends BasePreferenceController  {
    private static final float ACCELERATION_STEP = 0.1f;

    private final ContentResolver mContentResolver;
    @SuppressWarnings("NullAway")
    private SeekBar mSeekBar;

    public MouseKeysAccelerationController(@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());
        if (preference == null) {
            return;
        }

        final float accelerationFromSettings = getAccelerationFromSettings();
        mSeekBar = Objects.requireNonNull(
                preference.findViewById(R.id.mouse_keys_acceleration_seekbar));

        // Scale the float acceleration value to the SeekBar's integer range.
        mSeekBar.setProgress(convertAccelerationToProgress(accelerationFromSettings));

        mSeekBar.setOnSeekBarChangeListener(
                new SeekBar.OnSeekBarChangeListener() {
                    @Override
                    public void onProgressChanged(@NonNull SeekBar seekBar, int progress,
                            boolean fromUser) {
                        // Convert the SeekBar's integer progress back to a float value
                        float acceleration = convertProgressToAcceleration(progress);
                        updateAccelerationValue(acceleration);
                    }

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

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

        ImageView slow = preference.findViewById(R.id.fast_icon);
        ImageView fast = preference.findViewById(R.id.slow_icon);
        if (slow == null || fast == null) {
            return;
        }
        slow.setOnClickListener(v -> increaseAccelerationByImageView());
        fast.setOnClickListener(v -> decreaseAccelerationByImageView());
    }

    private void updateAccelerationValue(float acceleration) {
        Settings.Secure.putFloat(mContentResolver,
                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ACCELERATION, acceleration);
    }

    private void decreaseAccelerationByImageView() {
        float currentAcceleration = convertProgressToAcceleration(mSeekBar.getProgress());
        if (currentAcceleration > mSeekBar.getMin()) {
            float newAcceleration = Math.max(mSeekBar.getMin(),
                    currentAcceleration - ACCELERATION_STEP);
            mSeekBar.setProgress(convertAccelerationToProgress(newAcceleration));
            updateAccelerationValue(newAcceleration);
        }
    }

    private void increaseAccelerationByImageView() {
        float currentAcceleration = convertProgressToAcceleration(mSeekBar.getProgress());
        if (currentAcceleration < mSeekBar.getMax()) {
            float newAcceleration = Math.min(mSeekBar.getMax(),
                    currentAcceleration + ACCELERATION_STEP);
            mSeekBar.setProgress(convertAccelerationToProgress(newAcceleration));
            updateAccelerationValue(newAcceleration);
        }
    }

    private float getAccelerationFromSettings() {
        return Settings.Secure.getFloat(mContentResolver,
                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ACCELERATION,
                InputSettings.DEFAULT_MOUSE_KEYS_ACCELERATION);
    }

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

    /** Helper function to convert float acceleration to SeekBar integer progress */
    private int convertAccelerationToProgress(float acceleration) {
        return Math.round((acceleration - mSeekBar.getMin()) / ACCELERATION_STEP);
    }

    /** Helper function to convert SeekBar integer progress back to float acceleration */
    private float convertProgressToAcceleration(int progress) {
        return mSeekBar.getMin() + (progress * ACCELERATION_STEP);
    }
}
+175 −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 MouseKeysAccelerationController}. */
@RunWith(RobolectricTestRunner.class)
public class MouseKeysAccelerationControllerTest {

    private static final String KEY_CUSTOM_SEEKBAR = "mouse_keys_acceleration_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 mSlow;
    private ImageView mFast;
    private SeekBar mSeekBar;
    private MouseKeysAccelerationController mController;

    @Before
    public void setUp() {
        mSlow = new ImageView(mContext);
        mFast = new ImageView(mContext);
        mSeekBar = new SeekBar(mContext);
        mController = new MouseKeysAccelerationController(mContext, KEY_CUSTOM_SEEKBAR);
        doReturn(mLayoutPreference).when(mScreen).findPreference(KEY_CUSTOM_SEEKBAR);
        doReturn(mSeekBar)
                .when(mLayoutPreference)
                .findViewById(R.id.mouse_keys_acceleration_seekbar);
        doReturn(mSlow).when(mLayoutPreference).findViewById(R.id.slow_icon);
        doReturn(mFast).when(mLayoutPreference).findViewById(R.id.fast_icon);
    }

    @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.putFloat(
                mContext.getContentResolver(),
                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ACCELERATION,
                .5f);
        mController.displayPreference(mScreen);

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

    @Test
    public void onSettingsChanged_updateAccelerationValue() {
        Settings.Secure.putFloat(
                mContext.getContentResolver(),
                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ACCELERATION,
                .5f);

        mController.displayPreference(mScreen);
        final float actualAccelerationValue =
                Settings.Secure.getFloat(
                        mContext.getContentResolver(),
                        Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ACCELERATION,
                        /* def= */ 0.0f);

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

    @Test
    public void onSeekBarProgressChanged_updateAccelerationValue() {
        Settings.Secure.putFloat(
                mContext.getContentResolver(),
                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ACCELERATION,
                .5f);
        mController.displayPreference(mScreen);
        mSeekBar.setProgress(8);
        final float actualAccelerationValue =
                Settings.Secure.getFloat(
                        mContext.getContentResolver(),
                        Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ACCELERATION,
                        /* def= */ 0.0f);

        assertThat(mSeekBar.getProgress()).isEqualTo(8);
        assertThat(actualAccelerationValue).isEqualTo(.8f);
    }

    @Test
    public void onDecreaseClicked_updateAccelerationValue() {
        Settings.Secure.putFloat(
                mContext.getContentResolver(),
                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ACCELERATION,
                .5f);
        mController.displayPreference(mScreen);
        mSlow.callOnClick();
        final float actualAccelerationValue =
                Settings.Secure.getFloat(
                        mContext.getContentResolver(),
                        Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ACCELERATION,
                        /* def= */ 0.0f);

        assertThat(mSeekBar.getProgress()).isEqualTo(4);
        assertThat(actualAccelerationValue).isEqualTo(.4f);
    }

    @Test
    public void onIncreaseClicked_updateAccelerationValue() {
        Settings.Secure.putFloat(
                mContext.getContentResolver(),
                Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ACCELERATION,
                .5f);

        mController.displayPreference(mScreen);
        mFast.callOnClick();
        final float actualAccelerationValue =
                Settings.Secure.getFloat(
                        mContext.getContentResolver(),
                        Settings.Secure.ACCESSIBILITY_MOUSE_KEYS_ACCELERATION,
                        /* def= */ 0.0f);

        assertThat(mSeekBar.getProgress()).isEqualTo(6);
        assertThat(actualAccelerationValue).isEqualTo(.6f);
    }
}