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

Commit 0d71a654 authored by Chun-Ku Lin's avatar Chun-Ku Lin
Browse files

refactor: Modify ToggleColorInversionPreferenceFragment to plain

xml/PreferenceController style

- Modify the xml so it's easier to glance what's included in this
  fragment
- Add preference controllers for each preference item
- Update the fragment test to be closer to integration test that could
  be used to verify the user interactions

Test: com.android.settings.accessibility
Test: manually verify the user interactions and UI on the screens
Bug: 406052931
Flag: EXEMPT risk taken (it would be cleaner for refactor without
feature flagging)

Change-Id: Id9f7a69a27472a9a972de7081c33817422d319e6
parent 273b726b
Loading
Loading
Loading
Loading
+34 −2
Original line number Diff line number Diff line
@@ -14,8 +14,40 @@
     limitations under the License.
-->

<PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android"
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:persistent="false"
    android:title="@string/accessibility_display_inversion_preference_title">

    <com.android.settingslib.widget.TopIntroPreference
        android:key="top_intro"
        android:title="@string/accessibility_display_inversion_preference_intro_text"
        app:searchable="false" />

    <com.android.settingslib.widget.IllustrationPreference
        android:key="animated_image"
        app:controller="com.android.settings.accessibility.ColorInversionIllustrationPreferenceController"
        app:searchable="false" />

    <com.android.settingslib.widget.MainSwitchPreference
        android:key="color_inversion_switch_preference_key"
        android:title="@string/accessibility_display_inversion_switch_title"
        app:controller="com.android.settings.accessibility.ColorInversionMainSwitchPreferenceController" />

    <PreferenceCategory
        android:key="general_categories"
        android:title="@string/accessibility_screen_option"
        app:searchable="false">
        <com.android.settings.accessibility.ShortcutPreference
            android:key="color_inversion_shortcut_key"
            android:persistent="false"
            android:title="@string/accessibility_display_inversion_shortcut_title"
            app:controller="com.android.settings.accessibility.ToggleShortcutPreferenceController" />
    </PreferenceCategory>

    <com.android.settings.accessibility.AccessibilityFooterPreference
        android:key="html_description"
        app:controller="com.android.settings.accessibility.ColorInversionFooterPreferenceController"
        app:searchable="false" />

</PreferenceScreen>
 No newline at end of file
+15 −0
Original line number Diff line number Diff line
@@ -18,7 +18,9 @@ package com.android.settings.accessibility;

import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;

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

import com.android.settings.core.BasePreferenceController;
@@ -34,6 +36,8 @@ public class AccessibilityFooterPreferenceController extends BasePreferenceContr
    private String mLearnMoreText;
    private String mIntroductionTitle;

    private CharSequence mSummary;

    public AccessibilityFooterPreferenceController(Context context, String key) {
        super(context, key);
    }
@@ -99,6 +103,13 @@ public class AccessibilityFooterPreferenceController extends BasePreferenceContr
        return mIntroductionTitle;
    }

    /**
     * Overrides ths summary of {@link AccessibilityFooterPreference}
     */
    protected void setSummary(@NonNull CharSequence summary) {
        mSummary = summary;
    }

    private void updateFooterPreferences(AccessibilityFooterPreference footerPreference) {
        final StringBuffer sb = new StringBuffer();
        sb.append(getIntroductionTitle()).append("\n\n").append(footerPreference.getTitle());
@@ -123,6 +134,10 @@ public class AccessibilityFooterPreferenceController extends BasePreferenceContr
            footerPreference.setLinkEnabled(false);
        }

        if (!TextUtils.isEmpty(mSummary)) {
            footerPreference.setSummary(mSummary);
        }

        // Grouping subcomponents to make more accessible.
        footerPreference.setSelectable(false);
    }
+68 −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.content.ComponentName
import android.content.Context
import android.text.Html
import com.android.settings.R

/**
 * A preferenceController that handles displaying the footer with html format in
 * [AccessibilityFooterPreference]
 */
open class HtmlFooterPreferenceController(context: Context, prefKey: String) :
    AccessibilityFooterPreferenceController(context, prefKey) {

    protected var componentName: ComponentName? = null

    /**
     * Initializes the [ComponentName] this [AccessibilityFooterPreference] is attached to.
     */
    open fun initialize(componentName: ComponentName) {
        this.componentName = componentName
    }

    /**
     * Converts the summary to html spannable by utilizing the [Html.fromHtml] with custom image
     * getter.
     */
    override fun setSummary(summary: CharSequence) {
        val htmlDescription: CharSequence =
            Html.fromHtml(
                summary.toString(),
                Html.FROM_HTML_MODE_COMPACT,
                /* imageGetter= */ null,
                /* tagHandler= */ null,
            )
        super.setSummary(htmlDescription)
    }
}

class ColorInversionFooterPreferenceController(context: Context, prefKey: String) :
    HtmlFooterPreferenceController(context, prefKey) {
    init {
        introductionTitle = context.getString(R.string.accessibility_color_inversion_about_title)
        summary = context.getText(R.string.accessibility_display_inversion_preference_subtitle)
        setupHelpLink(
            R.string.help_url_color_inversion,
            context.getString(
                R.string.accessibility_color_inversion_footer_learn_more_content_description
            ),
        )
    }
}
+91 −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.content.ContentResolver
import android.content.Context
import android.net.Uri
import androidx.preference.PreferenceScreen
import com.airbnb.lottie.LottieAnimationView
import com.android.settings.R
import com.android.settings.core.BasePreferenceController
import com.android.settingslib.utils.ThreadUtils
import com.android.settingslib.widget.IllustrationPreference

/** BasePreferenceController for [IllustrationPreference] */
open class IllustrationPreferenceController(context: Context, prefKey: String) :
    BasePreferenceController(context, prefKey) {

    private var imageUri: Uri? = null
    private var contentDescription: CharSequence? = null

    /** Initialize the image and content description of the image */
    open fun initialize(imageUri: Uri?, contentDescription: CharSequence?) {
        this.imageUri = imageUri
        this.contentDescription = contentDescription
    }

    override fun getAvailabilityStatus(): Int = AVAILABLE_UNSEARCHABLE

    override fun displayPreference(screen: PreferenceScreen?) {
        super.displayPreference(screen)
        val illustrationPref: IllustrationPreference? = screen?.findPreference(preferenceKey)
        if (illustrationPref == null || imageUri == null) return

        val displayHalfHeight =
            AccessibilityUtil.getDisplayBounds(illustrationPref.context).height() / 2
        illustrationPref.apply {
            imageUri = this@IllustrationPreferenceController.imageUri
            isSelectable = false
            setMaxHeight(displayHalfHeight)
            setOnBindListener { view: LottieAnimationView? ->
                // isAnimatable is decided in
                // [IllustrationPreference#onBindViewHolder(PreferenceViewHolder)]. Therefore, we
                // wait until the view is bond to set the content description for it.
                // The content description is added for an animation illustration only.
                // Since the static images are decorative.
                ThreadUtils.getUiThreadHandler()
                    .post(
                        Runnable {
                            if (this.isAnimatable) {
                                this.contentDescription =
                                    this@IllustrationPreferenceController.contentDescription
                            }
                        }
                    )
            }
        }
    }
}

class ColorInversionIllustrationPreferenceController(context: Context, prefKey: String) :
    IllustrationPreferenceController(context, prefKey) {
    init {
        val imageUri =
            Uri.Builder()
                .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
                .authority(context.packageName)
                .appendPath(java.lang.String.valueOf(R.raw.accessibility_color_inversion_banner))
                .build()
        val contentDescription =
            context.getString(
                R.string.accessibility_illustration_content_description,
                context.getText(R.string.accessibility_display_inversion_preference_title),
            )
        initialize(imageUri, contentDescription)
    }
}
+118 −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.content.ComponentName
import android.content.Context
import android.database.ContentObserver
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 androidx.preference.TwoStatePreference
import com.android.internal.accessibility.AccessibilityShortcutController
import com.android.settings.core.BasePreferenceController

/**
 * A base preference controller that triggers a Settings to be on/off when toggling the preference
 * UI.
 */
abstract class SimpleSettingSwitchPreferenceController(context: Context, prefKey: String) :
    BasePreferenceController(context, prefKey),
    DefaultLifecycleObserver,
    Preference.OnPreferenceChangeListener {

    private var preference: TwoStatePreference? = null
    private var contentObserver: ContentObserver =
        object : ContentObserver(Looper.myLooper()?.run { Handler(/* async= */ true) }) {
            override fun onChange(selfChange: Boolean) {
                preference?.let { updateState(it) }
            }
        }

    abstract fun getSettingKey(): String

    abstract fun getComponentName(): ComponentName

    override fun getAvailabilityStatus(): Int = AVAILABLE

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

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

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

    override fun updateState(preference: Preference?) {
        super.updateState(preference)
        if (preference is TwoStatePreference) {
            preference.isChecked = isChecked()
        }
    }

    private fun setChecked(preference: TwoStatePreference, checked: Boolean) {
        val isEnabled = isChecked()
        if (checked == isEnabled) {
            return
        }
        AccessibilityStatsLogUtils.logAccessibilityServiceEnabled(getComponentName(), checked)
        Settings.Secure.putInt(
            mContext.contentResolver,
            getSettingKey(),
            if (checked) AccessibilityUtil.State.ON else AccessibilityUtil.State.OFF,
        )
        updateState(preference)
    }

    private fun isChecked(): Boolean {
        return Settings.Secure.getInt(
            mContext.contentResolver,
            getSettingKey(),
            AccessibilityUtil.State.OFF,
        ) == AccessibilityUtil.State.ON
    }

    override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean {
        if (preference is TwoStatePreference && newValue is Boolean) {
            setChecked(preference, newValue)
        }
        return false
    }
}

class ColorInversionMainSwitchPreferenceController(context: Context, prefKey: String) :
    SimpleSettingSwitchPreferenceController(context, prefKey) {
    override fun getSettingKey(): String = Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED

    override fun getComponentName(): ComponentName =
        AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME
}
Loading