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

Commit 3ba83732 authored by Chun-Ku Lin's avatar Chun-Ku Lin Committed by Android (Google) Code Review
Browse files

Merge "Create FooterPreferenceController for magnification screen" into main

parents 7c8cf662 d38343d1
Loading
Loading
Loading
Loading
+4 −1
Original line number Diff line number Diff line
@@ -110,7 +110,10 @@ public class AccessibilityFooterPreferenceController extends BasePreferenceContr
        mSummary = summary;
    }

    private void updateFooterPreferences(AccessibilityFooterPreference footerPreference) {
    /**
     * Updates the footer preference with the given data set to this PreferenceController
     */
    public void updateFooterPreferences(@NonNull AccessibilityFooterPreference footerPreference) {
        final StringBuffer sb = new StringBuffer();
        sb.append(getIntroductionTitle()).append("\n\n").append(footerPreference.getTitle());
        footerPreference.setContentDescription(sb);
+157 −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.detail.screenmagnification

import android.content.Context
import android.content.pm.PackageManager
import android.database.ContentObserver
import android.icu.text.MessageFormat
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import com.android.server.accessibility.Flags
import com.android.settings.R
import com.android.settings.accessibility.AccessibilityFooterPreference
import com.android.settings.accessibility.HtmlFooterPreferenceController
import com.android.settings.inputmethod.InputPeripheralsSettingsUtils

/** PreferenceController for Screen Magnification detail page's footer */
class FooterPreferenceController(context: Context, prefKey: String) :
    HtmlFooterPreferenceController(context, prefKey), DefaultLifecycleObserver {
    init {
        introductionTitle =
            context.getString(R.string.accessibility_screen_magnification_about_title)
        setupHelpLink(
            R.string.help_url_magnification,
            context.getString(
                R.string.accessibility_screen_magnification_footer_learn_more_content_description
            ),
        )

        updateSummary()
    }

    private var preference: AccessibilityFooterPreference? = null
    private var contentObserver: ContentObserver =
        object : ContentObserver(Handler(Looper.getMainLooper())) {
            override fun onChange(selfChange: Boolean) {
                updateState(preference)
            }
        }

    override fun onStart(owner: LifecycleOwner) {
        mContext.contentResolver.registerContentObserver(
            Settings.Secure.getUriFor(OneFingerPanningPreferenceController.SETTING_KEY),
            /* notifyForDescendants= */ false,
            contentObserver,
        )
    }

    override fun onStop(owner: LifecycleOwner) {
        mContext.contentResolver.unregisterContentObserver(contentObserver)
    }

    override fun displayPreference(screen: PreferenceScreen?) {
        super.displayPreference(screen)
        preference = screen?.findPreference(preferenceKey)
    }

    override fun updateState(preference: Preference?) {
        (preference as? AccessibilityFooterPreference)?.let {
            updateSummary()
            updateFooterPreferences(it)
        }
    }

    private fun updateSummary() {
        val hasTouchScreen =
            mContext.packageManager.run {
                hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN) ||
                    hasSystemFeature(PackageManager.FEATURE_FAKETOUCH)
            }

        val showKeyboardSummary =
            Flags.enableMagnificationKeyboardControl() &&
                InputPeripheralsSettingsUtils.isHardKeyboard()
        val showDefaultSummary = hasTouchScreen || !showKeyboardSummary

        val stringBuilder = StringBuilder()
        if (showKeyboardSummary) {
            stringBuilder.append(getKeyboardSummary())
        }

        if (showDefaultSummary) {
            if (stringBuilder.isNotBlank()) {
                stringBuilder.append("<br/><br/>")
            }

            stringBuilder.append(getDefaultSummary())
        }

        setSummary(stringBuilder.toString())
    }

    private fun getDefaultSummary(): String {
        if (Flags.enableMagnificationOneFingerPanningGesture()) {
            val isOneFingerPanningOn =
                OneFingerPanningPreferenceController.isOneFingerPanningEnabled(mContext)
            return MessageFormat.format(
                mContext.getString(
                    if (isOneFingerPanningOn)
                        R.string.accessibility_screen_magnification_summary_one_finger_panning_on
                    else R.string.accessibility_screen_magnification_summary_one_finger_panning_off
                ),
                1,
                2,
                3,
                4,
                5,
            )
        } else {
            return MessageFormat.format(
                mContext.getString(R.string.accessibility_screen_magnification_summary),
                1,
                2,
                3,
                4,
                5,
            )
        }
    }

    private fun getKeyboardSummary(): String {
        val meta: String? = mContext.getString(R.string.modifier_keys_meta)
        val alt: String? = mContext.getString(R.string.modifier_keys_alt)
        return MessageFormat.format(
            mContext.getString(
                R.string.accessibility_screen_magnification_keyboard_summary,
                meta,
                alt,
                meta,
                alt,
            ),
            1,
            2,
            3,
            4,
        )
    }
}
+323 −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.detail.screenmagnification

import android.content.Context
import android.content.pm.PackageManager
import android.icu.text.MessageFormat
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.text.Html
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.preference.PreferenceManager
import androidx.test.core.app.ApplicationProvider
import com.android.server.accessibility.Flags
import com.android.settings.R
import com.android.settings.accessibility.AccessibilityFooterPreference
import com.android.settings.accessibility.AccessibilityUtil
import com.android.settings.testutils.AccessibilityTestUtils.setWindowMagnificationSupported
import com.android.settings.testutils.shadow.SettingsShadowResources
import com.android.settings.testutils.shadow.ShadowInputDevice
import com.android.settings.testutils.shadow.ShadowInputDevice.makeFullKeyboardInputDevicebyId
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestParameterInjector
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.robolectric.shadows.ShadowLooper
import org.robolectric.shadows.ShadowPackageManager

/** Tests for [FooterPreferenceController] */
@EnableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_KEYBOARD_CONTROL)
@Config(shadows = [SettingsShadowResources::class, ShadowInputDevice::class])
@RunWith(RobolectricTestParameterInjector::class)
class FooterPreferenceControllerTest {
    @get:Rule val setFlagsRule = SetFlagsRule()
    private val prefKey = "prefKey"
    private val lifeCycleOwner = TestLifecycleOwner(initialState = Lifecycle.State.INITIALIZED)
    private val context: Context = ApplicationProvider.getApplicationContext()
    private val oneFingerPanningOnDefaultSummary = Html.fromHtml(
        MessageFormat.format(
            context.getString(
                R.string.accessibility_screen_magnification_summary_one_finger_panning_on
            ),
            1,
            2,
            3,
            4,
            5,
        ),
        Html.FROM_HTML_MODE_COMPACT
    ).toString()
    private val oneFingerPanningOffDefaultSummary = Html.fromHtml(
        MessageFormat.format(
            context.getString(
                R.string.accessibility_screen_magnification_summary_one_finger_panning_off
            ),
            1,
            2,
            3,
            4,
            5,
        ),
        Html.FROM_HTML_MODE_COMPACT
    ).toString()
    private val defaultSummary =Html.fromHtml(
        MessageFormat.format(context.getString(R.string.accessibility_screen_magnification_summary),
            1,
            2,
            3,
            4,
            5,
        ),
        Html.FROM_HTML_MODE_COMPACT
    ).toString()
    private val metaString = context.getString(R.string.modifier_keys_meta)
    private val altString = context.getString(R.string.modifier_keys_alt)
    private val keyboardSummary = Html.fromHtml(
        MessageFormat.format(
            context.getString(
                R.string.accessibility_screen_magnification_keyboard_summary,
                metaString,
                altString,
                metaString,
                altString,
            ),
            1,
            2,
            3,
            4,
        ),
        Html.FROM_HTML_MODE_COMPACT
    ).toString()
    private val shadowContentResolver = shadowOf(context.contentResolver)
    private val controller = FooterPreferenceController(context, prefKey)
    private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context)
    private val preference = AccessibilityFooterPreference(context).apply { key = prefKey }

    @Before
    fun setUp() {
        SettingsShadowResources.overrideResource(
            com.android.internal.R.bool.config_enable_a11y_magnification_single_panning,
            false,
        )
        setWindowMagnificationSupported(context, /* supported= */ true)
        preferenceScreen.addPreference(preference)
        lifeCycleOwner.lifecycle.addObserver(controller)
        controller.displayPreference(preferenceScreen)
    }

    @After
    fun cleanUp() {
        lifeCycleOwner.lifecycle.removeObserver(controller)
    }

    @Test
    fun onStart_verifyRegisterOneFingerPanningObserver() {
        lifeCycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START)

        assertThat(
                shadowContentResolver.getContentObservers(
                    Settings.Secure.getUriFor(OneFingerPanningPreferenceController.SETTING_KEY)
                )
            )
            .hasSize(1)
    }

    @Test
    fun onStop_verifyUnregisterOneFingerPanningObserver() {
        onStart_verifyRegisterOneFingerPanningObserver()
        lifeCycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_STOP)

        assertThat(
                shadowContentResolver.getContentObservers(
                    Settings.Secure.getUriFor(OneFingerPanningPreferenceController.SETTING_KEY)
                )
            )
            .isEmpty()
    }

    @Test
    fun verifyContentDescription() {
        controller.updateState(preference)

        assertThat(preference.contentDescription.toString())
            .isEqualTo("About magnification\n\n$defaultSummary")
    }

    @DisableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_ONE_FINGER_PANNING_GESTURE)
    @Test
    fun touchScreenSupported_hasHardKeyboard_oneFingerPanningFlagOff_verifySummary() {
        assertSummary(
            touchScreenSupported = true,
            hardKeyboardAvailable = true,
            oneFingerPanningEnabled = false,
            expectedSummary = "$keyboardSummary\n\n$defaultSummary",
        )
    }

    @DisableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_ONE_FINGER_PANNING_GESTURE)
    @Test
    fun touchScreenSupported_noHardKeyboard_oneFingerPanningFlagOff_verifySummary() {
        assertSummary(
            touchScreenSupported = true,
            hardKeyboardAvailable = false,
            oneFingerPanningEnabled = false,
            expectedSummary = defaultSummary,
        )
    }

    @DisableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_ONE_FINGER_PANNING_GESTURE)
    @Test
    fun touchScreenNotSupported_hasHardKeyboard_oneFingerPanningFlagOff_verifySummary() {
        assertSummary(
            touchScreenSupported = false,
            hardKeyboardAvailable = true,
            oneFingerPanningEnabled = false,
            expectedSummary = keyboardSummary,
        )
    }

    @DisableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_ONE_FINGER_PANNING_GESTURE)
    @Test
    fun touchScreenNotSupported_noHardKeyboard_oneFingerPanningFlagOff_verifySummary() {
        assertSummary(
            touchScreenSupported = false,
            hardKeyboardAvailable = false,
            oneFingerPanningEnabled = false,
            expectedSummary = defaultSummary,
        )
    }

    @EnableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_ONE_FINGER_PANNING_GESTURE)
    @Test
    fun touchScreenSupported_hasHardKeyboard_oneFingerSettingsOff_verifySummary() {
        assertSummary(
            touchScreenSupported = true,
            hardKeyboardAvailable = true,
            oneFingerPanningEnabled = false,
            expectedSummary = "$keyboardSummary\n\n$oneFingerPanningOffDefaultSummary",
        )
    }

    @EnableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_ONE_FINGER_PANNING_GESTURE)
    @Test
    fun touchScreenSupported_noHardKeyboard_oneFingerSettingsOff_verifySummary() {
        assertSummary(
            touchScreenSupported = true,
            hardKeyboardAvailable = false,
            oneFingerPanningEnabled = false,
            expectedSummary = oneFingerPanningOffDefaultSummary,
        )
    }

    @EnableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_ONE_FINGER_PANNING_GESTURE)
    @Test
    fun touchScreenNotSupported_hasHardKeyboard_oneFingerSettingsOff_verifySummary() {
        assertSummary(
            touchScreenSupported = false,
            hardKeyboardAvailable = true,
            oneFingerPanningEnabled = false,
            expectedSummary = keyboardSummary,
        )
    }

    @EnableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_ONE_FINGER_PANNING_GESTURE)
    @Test
    fun touchScreenNotSupported_noHardKeyboard_oneFingerSettingsOn_verifySummary() {
        assertSummary(
            touchScreenSupported = false,
            hardKeyboardAvailable = false,
            oneFingerPanningEnabled = true,
            expectedSummary = oneFingerPanningOnDefaultSummary,
        )
    }

    @EnableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_ONE_FINGER_PANNING_GESTURE)
    @Test
    fun touchScreenNotSupported_noHardKeyboard_oneFingerSettingsOff_verifySummary() {
        assertSummary(
            touchScreenSupported = false,
            hardKeyboardAvailable = false,
            oneFingerPanningEnabled = false,
            expectedSummary = oneFingerPanningOffDefaultSummary,
        )
    }

    @EnableFlags(Flags.FLAG_ENABLE_MAGNIFICATION_ONE_FINGER_PANNING_GESTURE)
    @Test
    fun onStart_turnOnOneFingerPanning_verifySummary() {
        lifeCycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START)
        assertSummary(
            touchScreenSupported = false,
            hardKeyboardAvailable = false,
            oneFingerPanningEnabled = false,
            expectedSummary = oneFingerPanningOffDefaultSummary,
        )

        setOneFingerPanningEnabled(true)
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks()

        assertThat(preference.summary.toString()).isEqualTo(oneFingerPanningOnDefaultSummary)
    }

    private fun assertSummary(
        touchScreenSupported: Boolean,
        hardKeyboardAvailable: Boolean,
        oneFingerPanningEnabled: Boolean,
        expectedSummary: String,
    ) {
        setTouchScreenSupported(touchScreenSupported)
        setHardKeyboardAvailable(hardKeyboardAvailable)
        setOneFingerPanningEnabled(oneFingerPanningEnabled)

        controller.updateState(preference)

        assertThat(preference.summary.toString()).isEqualTo(expectedSummary)
    }

    private fun setTouchScreenSupported(supported: Boolean) {
        val shadowPackageManager: ShadowPackageManager = shadowOf(context.packageManager)
        shadowPackageManager.setSystemFeature(PackageManager.FEATURE_TOUCHSCREEN, supported)
        shadowPackageManager.setSystemFeature(PackageManager.FEATURE_FAKETOUCH, supported)
    }

    private fun setOneFingerPanningEnabled(enabled: Boolean) {
        Settings.Secure.putInt(
            context.contentResolver,
            OneFingerPanningPreferenceController.SETTING_KEY,
            if (enabled) AccessibilityUtil.State.ON else AccessibilityUtil.State.OFF,
        )
    }

    private fun setHardKeyboardAvailable(available: Boolean) {
        if (available) {
            // The deviceId needs to be >= 1 in order to be considered as a hard keyboard
            val deviceId = 2
            ShadowInputDevice.addDevice(deviceId, makeFullKeyboardInputDevicebyId(deviceId))
        } else {
            ShadowInputDevice.reset()
        }
    }
}