From 4a9ed4576282c5521d4e932c1f577dd4b2bd5d4a Mon Sep 17 00:00:00 2001 From: xtclovver Date: Thu, 7 May 2026 03:38:54 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=80=D0=B5=D0=B4=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D0=B9=D0=BD=20=D0=B8=D0=BA=D0=BE=D0=BD=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 78 ++++++++- .../rknhardering/LauncherIconManager.kt | 55 ++++++ .../rknhardering/LauncherIconVariant.kt | 27 +++ .../notcvnt/rknhardering/RknHarderingApp.kt | 51 ++++++ .../SettingsAccessibilityFragment.kt | 70 ++++++++ .../notcvnt/rknhardering/SettingsActivity.kt | 3 + .../com/notcvnt/rknhardering/SettingsPrefs.kt | 3 + .../res/drawable/ic_launcher_foreground.xml | 74 --------- ..._launcher_n04_deuteranopia_background.xml} | 2 +- ...c_launcher_n04_deuteranopia_foreground.xml | 16 ++ .../ic_launcher_n04_monochrome_background.xml | 10 ++ .../ic_launcher_n04_monochrome_foreground.xml | 16 ++ .../ic_launcher_n04_original_background.xml | 10 ++ .../ic_launcher_n04_original_foreground.xml | 16 ++ .../ic_launcher_n04_protanopia_background.xml | 10 ++ .../ic_launcher_n04_protanopia_foreground.xml | 16 ++ .../res/drawable/ic_launcher_n04_themed.xml | 25 +++ .../ic_launcher_n04_tritanopia_background.xml | 10 ++ .../ic_launcher_n04_tritanopia_foreground.xml | 16 ++ .../fragment_settings_accessibility.xml | 47 ++++++ .../ic_launcher_n04_deuteranopia.xml | 6 + .../ic_launcher_n04_deuteranopia_round.xml | 6 + .../ic_launcher_n04_monochrome.xml | 6 + .../ic_launcher_n04_monochrome_round.xml | 6 + .../ic_launcher_n04_original.xml | 6 + .../ic_launcher_n04_original_round.xml | 6 + .../ic_launcher_n04_protanopia.xml | 6 + .../ic_launcher_n04_protanopia_round.xml | 6 + .../ic_launcher_n04_tritanopia.xml | 6 + .../ic_launcher_n04_tritanopia_round.xml | 6 + .../main/res/mipmap-anydpi/ic_launcher.xml | 6 - .../res/mipmap-anydpi/ic_launcher_round.xml | 6 - app/src/main/res/values-fa/strings.xml | 6 + app/src/main/res/values-ru/strings.xml | 6 + app/src/main/res/values-zh-rCN/strings.xml | 6 + app/src/main/res/values/strings.xml | 6 + .../rknhardering/LauncherIconManagerTest.kt | 108 ++++++++++++ .../rknhardering/LauncherIconVariantTest.kt | 39 +++++ .../rknhardering/RknHarderingAppTest.kt | 104 ++++++++++++ .../SettingsAccessibilityFragmentTest.kt | 156 ++++++++++++++++++ .../android/en-US/changelogs/20612.txt | 1 + .../metadata/android/fa/changelogs/20612.txt | 1 + .../android/ru-RU/changelogs/20612.txt | 1 + .../android/zh-CN/changelogs/20612.txt | 1 + 45 files changed, 973 insertions(+), 93 deletions(-) create mode 100644 app/src/main/java/com/notcvnt/rknhardering/LauncherIconManager.kt create mode 100644 app/src/main/java/com/notcvnt/rknhardering/LauncherIconVariant.kt delete mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml rename app/src/main/res/drawable/{ic_launcher_background.xml => ic_launcher_n04_deuteranopia_background.xml} (89%) create mode 100644 app/src/main/res/drawable/ic_launcher_n04_deuteranopia_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_n04_monochrome_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_n04_monochrome_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_n04_original_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_n04_original_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_n04_protanopia_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_n04_protanopia_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_n04_themed.xml create mode 100644 app/src/main/res/drawable/ic_launcher_n04_tritanopia_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_n04_tritanopia_foreground.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_deuteranopia.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_deuteranopia_round.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_monochrome.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_monochrome_round.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_original.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_original_round.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_protanopia.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_protanopia_round.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_tritanopia.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_tritanopia_round.xml delete mode 100644 app/src/main/res/mipmap-anydpi/ic_launcher.xml delete mode 100644 app/src/main/res/mipmap-anydpi/ic_launcher_round.xml create mode 100644 app/src/test/java/com/notcvnt/rknhardering/LauncherIconManagerTest.kt create mode 100644 app/src/test/java/com/notcvnt/rknhardering/LauncherIconVariantTest.kt create mode 100644 app/src/test/java/com/notcvnt/rknhardering/RknHarderingAppTest.kt create mode 100644 app/src/test/java/com/notcvnt/rknhardering/SettingsAccessibilityFragmentTest.kt create mode 100644 fastlane/metadata/android/en-US/changelogs/20612.txt create mode 100644 fastlane/metadata/android/fa/changelogs/20612.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/20612.txt create mode 100644 fastlane/metadata/android/zh-CN/changelogs/20612.txt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 73186fc..1626103 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,8 +15,8 @@ android { applicationId = "com.notcvnt.rknhardering" minSdk = 26 targetSdk = 36 - versionCode = 20611 - versionName = "2.6.11" + versionCode = 20612 + versionName = "2.6.12" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" androidResources.localeFilters += listOf("en", "ru", "fa", "zh-rCN") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cd5e59b..27c4334 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -56,10 +56,10 @@ android:allowBackup="false" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" - android:icon="@mipmap/ic_launcher" + android:icon="@mipmap/ic_launcher_n04_original" android:label="@string/app_name" android:usesCleartextTraffic="false" - android:roundIcon="@mipmap/ic_launcher_round" + android:roundIcon="@mipmap/ic_launcher_n04_original_round" android:supportsRtl="true" android:theme="@style/Theme.RKNHardering" android:networkSecurityConfig="@xml/network_security_config" @@ -67,13 +67,83 @@ tools:targetApi="tiramisu"> + android:exported="true" /> + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/notcvnt/rknhardering/LauncherIconManager.kt b/app/src/main/java/com/notcvnt/rknhardering/LauncherIconManager.kt new file mode 100644 index 0000000..ddf0804 --- /dev/null +++ b/app/src/main/java/com/notcvnt/rknhardering/LauncherIconManager.kt @@ -0,0 +1,55 @@ +package com.notcvnt.rknhardering + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import android.util.Log + +internal object LauncherIconManager { + private const val TAG = "LauncherIconManager" + + internal var setComponentEnabledSettingForTests: + ((PackageManager, ComponentName, Int) -> Unit)? = null + + fun apply(context: Context, target: LauncherIconVariant): Boolean { + return try { + val pm = context.packageManager + val pkg = context.packageName + + setStateIfNeeded(pm, ComponentName(pkg, target.aliasClass), PackageManager.COMPONENT_ENABLED_STATE_ENABLED) + LauncherIconVariant.entries + .filterNot { it == target } + .forEach { variant -> + setStateIfNeeded( + pm, + ComponentName(pkg, variant.aliasClass), + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + ) + } + true + } catch (e: SecurityException) { + Log.w(TAG, "Failed to toggle launcher alias", e) + false + } + } + + fun current(context: Context): LauncherIconVariant { + val pm = context.packageManager + val pkg = context.packageName + return LauncherIconVariant.entries.firstOrNull { variant -> + pm.getComponentEnabledSetting(ComponentName(pkg, variant.aliasClass)) == + PackageManager.COMPONENT_ENABLED_STATE_ENABLED + } ?: LauncherIconVariant.ORIGINAL + } + + private fun setStateIfNeeded( + pm: PackageManager, + component: ComponentName, + newState: Int, + ) { + if (pm.getComponentEnabledSetting(component) != newState) { + setComponentEnabledSettingForTests?.invoke(pm, component, newState) + ?: pm.setComponentEnabledSetting(component, newState, PackageManager.DONT_KILL_APP) + } + } +} diff --git a/app/src/main/java/com/notcvnt/rknhardering/LauncherIconVariant.kt b/app/src/main/java/com/notcvnt/rknhardering/LauncherIconVariant.kt new file mode 100644 index 0000000..e621f58 --- /dev/null +++ b/app/src/main/java/com/notcvnt/rknhardering/LauncherIconVariant.kt @@ -0,0 +1,27 @@ +package com.notcvnt.rknhardering + +internal enum class LauncherIconVariant( + val aliasClass: String, + val prefValue: String, +) { + ORIGINAL("com.notcvnt.rknhardering.LauncherOriginal", "original"), + PROTANOPIA("com.notcvnt.rknhardering.LauncherProtanopia", "protanopia"), + DEUTERANOPIA("com.notcvnt.rknhardering.LauncherDeuteranopia", "deuteranopia"), + TRITANOPIA("com.notcvnt.rknhardering.LauncherTritanopia", "tritanopia"), + MONOCHROME("com.notcvnt.rknhardering.LauncherMonochrome", "monochrome"); + + companion object { + fun fromCvd( + mode: ColorVisionMode, + redGreenSub: LauncherIconVariant?, + ): LauncherIconVariant { + return when (mode) { + ColorVisionMode.OFF -> ORIGINAL + ColorVisionMode.RED_GREEN -> + redGreenSub?.takeIf { it == PROTANOPIA } ?: DEUTERANOPIA + ColorVisionMode.BLUE_YELLOW -> TRITANOPIA + ColorVisionMode.ACHROMATOPSIA -> MONOCHROME + } + } + } +} diff --git a/app/src/main/java/com/notcvnt/rknhardering/RknHarderingApp.kt b/app/src/main/java/com/notcvnt/rknhardering/RknHarderingApp.kt index eba2c4f..ba35852 100644 --- a/app/src/main/java/com/notcvnt/rknhardering/RknHarderingApp.kt +++ b/app/src/main/java/com/notcvnt/rknhardering/RknHarderingApp.kt @@ -1,6 +1,8 @@ package com.notcvnt.rknhardering import android.app.Application +import android.content.SharedPreferences +import androidx.core.content.edit import com.notcvnt.rknhardering.probe.NativeCurlBridge class RknHarderingApp : Application() { @@ -9,5 +11,54 @@ class RknHarderingApp : Application() { super.onCreate() NativeCurlBridge.initIfNeeded(this) AppUiSettings.applySavedTheme(this) + migrateLauncherIconIfNeeded() + } + + internal fun migrateLauncherIconIfNeeded() { + val prefs = AppUiSettings.prefs(this) + if (prefs.getBoolean(SettingsPrefs.PREF_ICON_MIGRATION_DONE, false)) return + + val rawMode = prefs.getString(SettingsPrefs.PREF_COLOR_VISION_MODE, null) + syncLegacyRedGreenSubVariant(prefs, rawMode) + + val mode = ColorVisionMode.fromPref(rawMode) + val ok = if (mode == ColorVisionMode.OFF) { + true + } else { + LauncherIconManager.apply( + this, + LauncherIconVariant.fromCvd(mode, redGreenSubVariantForMigration(prefs, rawMode)), + ) + } + if (ok) { + prefs.edit { putBoolean(SettingsPrefs.PREF_ICON_MIGRATION_DONE, true) } + } + } + + private fun syncLegacyRedGreenSubVariant(prefs: SharedPreferences, rawMode: String?) { + if (rawMode != LauncherIconVariant.PROTANOPIA.prefValue) return + prefs.edit { + putBoolean(SettingsPrefs.PREF_EASTER_EGG_PROTANOPIA_UNLOCKED, true) + putString(SettingsPrefs.PREF_RED_GREEN_ICON_VARIANT, LauncherIconVariant.PROTANOPIA.prefValue) + } + } + + private fun redGreenSubVariantForMigration( + prefs: SharedPreferences, + rawMode: String?, + ): LauncherIconVariant { + if (rawMode == LauncherIconVariant.PROTANOPIA.prefValue) { + return LauncherIconVariant.PROTANOPIA + } + val unlocked = prefs.getBoolean(SettingsPrefs.PREF_EASTER_EGG_PROTANOPIA_UNLOCKED, false) + val storedVariant = prefs.getString( + SettingsPrefs.PREF_RED_GREEN_ICON_VARIANT, + LauncherIconVariant.DEUTERANOPIA.prefValue, + ) + return if (unlocked && storedVariant == LauncherIconVariant.PROTANOPIA.prefValue) { + LauncherIconVariant.PROTANOPIA + } else { + LauncherIconVariant.DEUTERANOPIA + } } } diff --git a/app/src/main/java/com/notcvnt/rknhardering/SettingsAccessibilityFragment.kt b/app/src/main/java/com/notcvnt/rknhardering/SettingsAccessibilityFragment.kt index c2d4fbb..4459198 100644 --- a/app/src/main/java/com/notcvnt/rknhardering/SettingsAccessibilityFragment.kt +++ b/app/src/main/java/com/notcvnt/rknhardering/SettingsAccessibilityFragment.kt @@ -5,18 +5,23 @@ import android.os.Bundle import android.view.View import android.widget.TextView import androidx.core.content.edit +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import com.google.android.material.chip.ChipGroup +import com.google.android.material.snackbar.Snackbar internal class SettingsAccessibilityFragment : Fragment(R.layout.fragment_settings_accessibility) { private lateinit var prefs: SharedPreferences private lateinit var chipGroupColorVision: ChipGroup + private lateinit var chipGroupRedGreenIcon: ChipGroup + private var easterEggTapCount = 0 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) prefs = AppUiSettings.prefs(requireContext()) chipGroupColorVision = view.findViewById(R.id.chipGroupColorVision) + chipGroupRedGreenIcon = view.findViewById(R.id.chipGroupRedGreenIcon) loadSettings(view) setupListeners(view) } @@ -24,7 +29,9 @@ internal class SettingsAccessibilityFragment : Fragment(R.layout.fragment_settin private fun loadSettings(root: View) { val mode = currentMode() chipGroupColorVision.check(chipIdForMode(mode)) + chipGroupRedGreenIcon.check(chipIdForRedGreenSubVariant(redGreenSubVariantFromPrefs())) renderPreview(root, mode) + refreshRedGreenSubGroup(root, mode) } private fun setupListeners(root: View) { @@ -33,6 +40,33 @@ internal class SettingsAccessibilityFragment : Fragment(R.layout.fragment_settin val mode = modeForChipId(checkedIds.first()) prefs.edit { putString(SettingsPrefs.PREF_COLOR_VISION_MODE, mode.prefValue) } renderPreview(root, mode) + refreshRedGreenSubGroup(root, mode) + applyLauncherIcon(root, mode) + } + + chipGroupRedGreenIcon.setOnCheckedStateChangeListener { _, checkedIds -> + if (checkedIds.isEmpty()) return@setOnCheckedStateChangeListener + val variant = when (checkedIds.first()) { + R.id.chipRedGreenProtanopia -> LauncherIconVariant.PROTANOPIA + else -> LauncherIconVariant.DEUTERANOPIA + } + prefs.edit { putString(SettingsPrefs.PREF_RED_GREEN_ICON_VARIANT, variant.prefValue) } + applyLauncherIcon(root, currentMode()) + } + + root.findViewById(R.id.easterEggTapZone).setOnClickListener { + if (prefs.getBoolean(SettingsPrefs.PREF_EASTER_EGG_PROTANOPIA_UNLOCKED, false)) { + return@setOnClickListener + } + easterEggTapCount++ + if (easterEggTapCount >= EASTER_EGG_TAP_TARGET) { + prefs.edit { + putBoolean(SettingsPrefs.PREF_EASTER_EGG_PROTANOPIA_UNLOCKED, true) + putString(SettingsPrefs.PREF_RED_GREEN_ICON_VARIANT, LauncherIconVariant.DEUTERANOPIA.prefValue) + } + Snackbar.make(root, R.string.settings_easter_egg_protanopia_unlocked, Snackbar.LENGTH_LONG).show() + refreshRedGreenSubGroup(root, currentMode()) + } } } @@ -105,6 +139,31 @@ internal class SettingsAccessibilityFragment : Fragment(R.layout.fragment_settin getString(R.string.settings_accessibility_status_preview_content, label) } + private fun refreshRedGreenSubGroup(root: View, mode: ColorVisionMode) { + val unlocked = prefs.getBoolean(SettingsPrefs.PREF_EASTER_EGG_PROTANOPIA_UNLOCKED, false) + root.findViewById(R.id.redGreenSubGroupContainer).isVisible = + mode == ColorVisionMode.RED_GREEN && unlocked + } + + private fun applyLauncherIcon(root: View, mode: ColorVisionMode) { + val target = LauncherIconVariant.fromCvd(mode, redGreenSubVariantFromPrefs()) + val message = if (LauncherIconManager.apply(requireContext(), target)) { + R.string.settings_color_vision_icon_changed_warning + } else { + R.string.settings_color_vision_icon_change_failed + } + Snackbar.make(root, message, Snackbar.LENGTH_LONG).show() + } + + private fun redGreenSubVariantFromPrefs(): LauncherIconVariant { + val unlocked = prefs.getBoolean(SettingsPrefs.PREF_EASTER_EGG_PROTANOPIA_UNLOCKED, false) + if (!unlocked) return LauncherIconVariant.DEUTERANOPIA + return when (prefs.getString(SettingsPrefs.PREF_RED_GREEN_ICON_VARIANT, LauncherIconVariant.DEUTERANOPIA.prefValue)) { + LauncherIconVariant.PROTANOPIA.prefValue -> LauncherIconVariant.PROTANOPIA + else -> LauncherIconVariant.DEUTERANOPIA + } + } + private fun chipIdForMode(mode: ColorVisionMode): Int { return when (mode) { ColorVisionMode.OFF -> R.id.chipColorVisionOff @@ -122,4 +181,15 @@ internal class SettingsAccessibilityFragment : Fragment(R.layout.fragment_settin else -> ColorVisionMode.OFF } } + + private fun chipIdForRedGreenSubVariant(variant: LauncherIconVariant): Int { + return when (variant) { + LauncherIconVariant.PROTANOPIA -> R.id.chipRedGreenProtanopia + else -> R.id.chipRedGreenDeuteranopia + } + } + + private companion object { + const val EASTER_EGG_TAP_TARGET = 10 + } } diff --git a/app/src/main/java/com/notcvnt/rknhardering/SettingsActivity.kt b/app/src/main/java/com/notcvnt/rknhardering/SettingsActivity.kt index b8841fa..9f2fefb 100644 --- a/app/src/main/java/com/notcvnt/rknhardering/SettingsActivity.kt +++ b/app/src/main/java/com/notcvnt/rknhardering/SettingsActivity.kt @@ -105,6 +105,9 @@ class SettingsActivity : AppCompatActivity() { const val PREF_THEME = SettingsPrefs.PREF_THEME const val PREF_LANGUAGE = SettingsPrefs.PREF_LANGUAGE const val PREF_COLOR_VISION_MODE = SettingsPrefs.PREF_COLOR_VISION_MODE + const val PREF_EASTER_EGG_PROTANOPIA_UNLOCKED = SettingsPrefs.PREF_EASTER_EGG_PROTANOPIA_UNLOCKED + const val PREF_RED_GREEN_ICON_VARIANT = SettingsPrefs.PREF_RED_GREEN_ICON_VARIANT + const val PREF_ICON_MIGRATION_DONE = SettingsPrefs.PREF_ICON_MIGRATION_DONE const val EXTRA_REQUEST_PERMISSIONS = "extra_request_permissions" fun applyTheme(theme: String) { diff --git a/app/src/main/java/com/notcvnt/rknhardering/SettingsPrefs.kt b/app/src/main/java/com/notcvnt/rknhardering/SettingsPrefs.kt index d12964f..b28cff3 100644 --- a/app/src/main/java/com/notcvnt/rknhardering/SettingsPrefs.kt +++ b/app/src/main/java/com/notcvnt/rknhardering/SettingsPrefs.kt @@ -25,4 +25,7 @@ internal object SettingsPrefs { const val PREF_THEME = "pref_theme" const val PREF_LANGUAGE = "pref_language" const val PREF_COLOR_VISION_MODE = "pref_color_vision_mode" + const val PREF_EASTER_EGG_PROTANOPIA_UNLOCKED = "pref_easter_egg_protanopia_unlocked" + const val PREF_RED_GREEN_ICON_VARIANT = "pref_red_green_icon_variant" + const val PREF_ICON_MIGRATION_DONE = "pref_icon_migration_done" } diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 6c5571a..0000000 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_n04_deuteranopia_background.xml similarity index 89% rename from app/src/main/res/drawable/ic_launcher_background.xml rename to app/src/main/res/drawable/ic_launcher_n04_deuteranopia_background.xml index 0522da3..8ec0a4a 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_n04_deuteranopia_background.xml @@ -5,6 +5,6 @@ android:viewportWidth="108" android:viewportHeight="108"> diff --git a/app/src/main/res/drawable/ic_launcher_n04_deuteranopia_foreground.xml b/app/src/main/res/drawable/ic_launcher_n04_deuteranopia_foreground.xml new file mode 100644 index 0000000..e6a3aec --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_n04_deuteranopia_foreground.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_n04_monochrome_background.xml b/app/src/main/res/drawable/ic_launcher_n04_monochrome_background.xml new file mode 100644 index 0000000..8ec0a4a --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_n04_monochrome_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_n04_monochrome_foreground.xml b/app/src/main/res/drawable/ic_launcher_n04_monochrome_foreground.xml new file mode 100644 index 0000000..31ee4d2 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_n04_monochrome_foreground.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_n04_original_background.xml b/app/src/main/res/drawable/ic_launcher_n04_original_background.xml new file mode 100644 index 0000000..8ec0a4a --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_n04_original_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_n04_original_foreground.xml b/app/src/main/res/drawable/ic_launcher_n04_original_foreground.xml new file mode 100644 index 0000000..ff2c431 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_n04_original_foreground.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_n04_protanopia_background.xml b/app/src/main/res/drawable/ic_launcher_n04_protanopia_background.xml new file mode 100644 index 0000000..8ec0a4a --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_n04_protanopia_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_n04_protanopia_foreground.xml b/app/src/main/res/drawable/ic_launcher_n04_protanopia_foreground.xml new file mode 100644 index 0000000..1ef7b03 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_n04_protanopia_foreground.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_n04_themed.xml b/app/src/main/res/drawable/ic_launcher_n04_themed.xml new file mode 100644 index 0000000..9da6f53 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_n04_themed.xml @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_n04_tritanopia_background.xml b/app/src/main/res/drawable/ic_launcher_n04_tritanopia_background.xml new file mode 100644 index 0000000..8ec0a4a --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_n04_tritanopia_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_n04_tritanopia_foreground.xml b/app/src/main/res/drawable/ic_launcher_n04_tritanopia_foreground.xml new file mode 100644 index 0000000..1d80f91 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_n04_tritanopia_foreground.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_settings_accessibility.xml b/app/src/main/res/layout/fragment_settings_accessibility.xml index 0d1ae85..02f6746 100644 --- a/app/src/main/res/layout/fragment_settings_accessibility.xml +++ b/app/src/main/res/layout/fragment_settings_accessibility.xml @@ -73,6 +73,53 @@ android:text="@string/settings_color_vision_achromatopsia" app:checkedIconVisible="false" /> + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_deuteranopia.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_deuteranopia.xml new file mode 100644 index 0000000..2c6b96f --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_deuteranopia.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_deuteranopia_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_deuteranopia_round.xml new file mode 100644 index 0000000..2c6b96f --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_deuteranopia_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_monochrome.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_monochrome.xml new file mode 100644 index 0000000..1a75221 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_monochrome.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_monochrome_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_monochrome_round.xml new file mode 100644 index 0000000..1a75221 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_monochrome_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_original.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_original.xml new file mode 100644 index 0000000..40a87b2 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_original.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_original_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_original_round.xml new file mode 100644 index 0000000..40a87b2 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_original_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_protanopia.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_protanopia.xml new file mode 100644 index 0000000..a8a4b35 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_protanopia.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_protanopia_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_protanopia_round.xml new file mode 100644 index 0000000..a8a4b35 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_protanopia_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_tritanopia.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_tritanopia.xml new file mode 100644 index 0000000..b4b86e7 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_tritanopia.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_tritanopia_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_tritanopia_round.xml new file mode 100644 index 0000000..b4b86e7 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_n04_tritanopia_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml deleted file mode 100644 index 6f3b755..0000000 --- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml deleted file mode 100644 index 6f3b755..0000000 --- a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index c6238dd..b48c85f 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -100,6 +100,12 @@ قرمز-سبز آبی-زرد Achromatopsia + نماد برنامه به‌روزرسانی شد. در بعضی لانچرها ممکن است برای لحظه‌ای ناپدید و دوباره ظاهر شود. + به‌روزرسانی نماد برنامه ناموفق بود. + گونهٔ نماد پروتانوپیا باز شد. + سبک نماد قرمز-سبز + دوترانوپیا + پروتانوپیا وضعیت: %1$s مجوزها مدیریت مجوزهای برنامه diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 24f8193..9b354df 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -100,6 +100,12 @@ Красно-зелёный Сине-жёлтый Ахроматопсия + Иконка приложения обновлена. В некоторых лаунчерах она может ненадолго исчезнуть и появиться снова. + Не удалось обновить иконку приложения. + Вариант иконки для протанопии разблокирован. + Стиль иконки для красно-зелёного режима + Дейтеранопия + Протанопия Статус: %1$s Разрешения Управление разрешениями приложения diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index e5a150e..4e6f0ce 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -100,6 +100,12 @@ 红绿色 蓝黄色 Achromatopsia + 应用图标已更新。在某些启动器上,它可能会短暂消失后重新出现。 + 更新应用图标失败。 + 已解锁红色盲图标变体。 + 红绿色图标样式 + 绿色盲 + 红色盲 状态:%1$s 权限 管理应用权限 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 27f306b..06c33d2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -99,6 +99,12 @@ Red-green Blue-yellow Achromatopsia + App icon updated. On some launchers it may briefly disappear and re-appear. + Failed to update app icon. + Protanopia icon variant unlocked. + Red-green icon style + Deuteranopia + Protanopia %1$s status Permissions Manage app permissions diff --git a/app/src/test/java/com/notcvnt/rknhardering/LauncherIconManagerTest.kt b/app/src/test/java/com/notcvnt/rknhardering/LauncherIconManagerTest.kt new file mode 100644 index 0000000..dd1948f --- /dev/null +++ b/app/src/test/java/com/notcvnt/rknhardering/LauncherIconManagerTest.kt @@ -0,0 +1,108 @@ +package com.notcvnt.rknhardering + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import androidx.test.core.app.ApplicationProvider +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class LauncherIconManagerTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + + @Before + fun setUp() { + resetLauncherAliases() + LauncherIconManager.setComponentEnabledSettingForTests = null + } + + @After + fun tearDown() { + LauncherIconManager.setComponentEnabledSettingForTests = null + resetLauncherAliases() + } + + @Test + fun `apply enables target alias and disables other aliases`() { + assertTrue(LauncherIconManager.apply(context, LauncherIconVariant.TRITANOPIA)) + + LauncherIconVariant.entries.forEach { variant -> + val expected = if (variant == LauncherIconVariant.TRITANOPIA) { + PackageManager.COMPONENT_ENABLED_STATE_ENABLED + } else { + PackageManager.COMPONENT_ENABLED_STATE_DISABLED + } + assertEquals(expected, componentState(variant)) + } + } + + @Test + fun `apply is idempotent for the same target`() { + val calls = mutableListOf>() + LauncherIconManager.setComponentEnabledSettingForTests = { pm, component, state -> + calls += component to state + pm.setComponentEnabledSetting(component, state, PackageManager.DONT_KILL_APP) + } + + assertTrue(LauncherIconManager.apply(context, LauncherIconVariant.PROTANOPIA)) + val firstCallCount = calls.size + assertTrue(LauncherIconManager.apply(context, LauncherIconVariant.PROTANOPIA)) + + assertEquals(firstCallCount, calls.size) + } + + @Test + fun `current returns enabled variant`() { + LauncherIconManager.apply(context, LauncherIconVariant.MONOCHROME) + + assertEquals(LauncherIconVariant.MONOCHROME, LauncherIconManager.current(context)) + } + + @Test + fun `current returns original when no alias is enabled`() { + LauncherIconVariant.entries.forEach { variant -> + context.packageManager.setComponentEnabledSetting( + componentName(variant), + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP, + ) + } + + assertEquals(LauncherIconVariant.ORIGINAL, LauncherIconManager.current(context)) + } + + @Test + fun `apply returns false when package manager rejects alias toggle`() { + LauncherIconManager.setComponentEnabledSettingForTests = { _, _, _ -> + throw SecurityException("blocked") + } + + assertFalse(LauncherIconManager.apply(context, LauncherIconVariant.DEUTERANOPIA)) + } + + private fun resetLauncherAliases() { + LauncherIconVariant.entries.forEach { variant -> + context.packageManager.setComponentEnabledSetting( + componentName(variant), + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, + PackageManager.DONT_KILL_APP, + ) + } + } + + private fun componentState(variant: LauncherIconVariant): Int { + return context.packageManager.getComponentEnabledSetting(componentName(variant)) + } + + private fun componentName(variant: LauncherIconVariant): ComponentName { + return ComponentName(context.packageName, variant.aliasClass) + } +} diff --git a/app/src/test/java/com/notcvnt/rknhardering/LauncherIconVariantTest.kt b/app/src/test/java/com/notcvnt/rknhardering/LauncherIconVariantTest.kt new file mode 100644 index 0000000..9cf9a1d --- /dev/null +++ b/app/src/test/java/com/notcvnt/rknhardering/LauncherIconVariantTest.kt @@ -0,0 +1,39 @@ +package com.notcvnt.rknhardering + +import org.junit.Assert.assertEquals +import org.junit.Test + +class LauncherIconVariantTest { + + @Test + fun `maps color vision modes to launcher icon variants`() { + assertEquals( + LauncherIconVariant.ORIGINAL, + LauncherIconVariant.fromCvd(ColorVisionMode.OFF, LauncherIconVariant.PROTANOPIA), + ) + assertEquals( + LauncherIconVariant.DEUTERANOPIA, + LauncherIconVariant.fromCvd(ColorVisionMode.RED_GREEN, null), + ) + assertEquals( + LauncherIconVariant.PROTANOPIA, + LauncherIconVariant.fromCvd(ColorVisionMode.RED_GREEN, LauncherIconVariant.PROTANOPIA), + ) + assertEquals( + LauncherIconVariant.DEUTERANOPIA, + LauncherIconVariant.fromCvd(ColorVisionMode.RED_GREEN, LauncherIconVariant.DEUTERANOPIA), + ) + assertEquals( + LauncherIconVariant.DEUTERANOPIA, + LauncherIconVariant.fromCvd(ColorVisionMode.RED_GREEN, LauncherIconVariant.ORIGINAL), + ) + assertEquals( + LauncherIconVariant.TRITANOPIA, + LauncherIconVariant.fromCvd(ColorVisionMode.BLUE_YELLOW, LauncherIconVariant.PROTANOPIA), + ) + assertEquals( + LauncherIconVariant.MONOCHROME, + LauncherIconVariant.fromCvd(ColorVisionMode.ACHROMATOPSIA, LauncherIconVariant.PROTANOPIA), + ) + } +} diff --git a/app/src/test/java/com/notcvnt/rknhardering/RknHarderingAppTest.kt b/app/src/test/java/com/notcvnt/rknhardering/RknHarderingAppTest.kt new file mode 100644 index 0000000..bd42582 --- /dev/null +++ b/app/src/test/java/com/notcvnt/rknhardering/RknHarderingAppTest.kt @@ -0,0 +1,104 @@ +package com.notcvnt.rknhardering + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.content.edit +import androidx.test.core.app.ApplicationProvider +import com.notcvnt.rknhardering.probe.NativeCurlBridge +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class RknHarderingAppTest { + + private val app: RknHarderingApp = ApplicationProvider.getApplicationContext() + + @Before + fun setUp() { + NativeCurlBridge.resetForTests() + AppUiSettings.prefs(app).edit().clear().commit() + resetLauncherAliases() + LauncherIconManager.setComponentEnabledSettingForTests = null + } + + @After + fun tearDown() { + NativeCurlBridge.resetForTests() + LauncherIconManager.setComponentEnabledSettingForTests = null + resetLauncherAliases() + } + + @Test + fun `first launch migrates blue yellow mode to tritanopia icon`() { + AppUiSettings.prefs(app).edit { + putString(SettingsPrefs.PREF_COLOR_VISION_MODE, ColorVisionMode.BLUE_YELLOW.prefValue) + } + + app.migrateLauncherIconIfNeeded() + + assertEquals(LauncherIconVariant.TRITANOPIA, LauncherIconManager.current(app)) + assertTrue(AppUiSettings.prefs(app).getBoolean(SettingsPrefs.PREF_ICON_MIGRATION_DONE, false)) + } + + @Test + fun `migration preserves legacy protanopia preference`() { + AppUiSettings.prefs(app).edit { + putString(SettingsPrefs.PREF_COLOR_VISION_MODE, LauncherIconVariant.PROTANOPIA.prefValue) + } + + app.migrateLauncherIconIfNeeded() + + assertEquals(LauncherIconVariant.PROTANOPIA, LauncherIconManager.current(app)) + assertTrue( + AppUiSettings.prefs(app) + .getBoolean(SettingsPrefs.PREF_EASTER_EGG_PROTANOPIA_UNLOCKED, false), + ) + assertEquals( + LauncherIconVariant.PROTANOPIA.prefValue, + AppUiSettings.prefs(app).getString(SettingsPrefs.PREF_RED_GREEN_ICON_VARIANT, null), + ) + } + + @Test + fun `second launch does not rerun migration when flag is set`() { + AppUiSettings.prefs(app).edit { + putString(SettingsPrefs.PREF_COLOR_VISION_MODE, ColorVisionMode.BLUE_YELLOW.prefValue) + putBoolean(SettingsPrefs.PREF_ICON_MIGRATION_DONE, true) + } + + app.migrateLauncherIconIfNeeded() + + assertEquals(LauncherIconVariant.ORIGINAL, LauncherIconManager.current(app)) + } + + @Test + fun `failed icon migration is retried on next launch`() { + AppUiSettings.prefs(app).edit { + putString(SettingsPrefs.PREF_COLOR_VISION_MODE, ColorVisionMode.BLUE_YELLOW.prefValue) + } + LauncherIconManager.setComponentEnabledSettingForTests = { _, _, _ -> + throw SecurityException("blocked") + } + + app.migrateLauncherIconIfNeeded() + + assertFalse(AppUiSettings.prefs(app).getBoolean(SettingsPrefs.PREF_ICON_MIGRATION_DONE, false)) + } + + private fun resetLauncherAliases() { + LauncherIconVariant.entries.forEach { variant -> + app.packageManager.setComponentEnabledSetting( + ComponentName(app.packageName, variant.aliasClass), + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, + PackageManager.DONT_KILL_APP, + ) + } + } +} diff --git a/app/src/test/java/com/notcvnt/rknhardering/SettingsAccessibilityFragmentTest.kt b/app/src/test/java/com/notcvnt/rknhardering/SettingsAccessibilityFragmentTest.kt new file mode 100644 index 0000000..450bec7 --- /dev/null +++ b/app/src/test/java/com/notcvnt/rknhardering/SettingsAccessibilityFragmentTest.kt @@ -0,0 +1,156 @@ +package com.notcvnt.rknhardering + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import android.os.Looper +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.content.edit +import androidx.test.core.app.ApplicationProvider +import com.google.android.material.chip.Chip +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf + +@RunWith(RobolectricTestRunner::class) +class SettingsAccessibilityFragmentTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + + @Before + fun setUp() { + AppUiSettings.prefs(context).edit().clear().commit() + resetLauncherAliases() + LauncherIconManager.setComponentEnabledSettingForTests = null + } + + @After + fun tearDown() { + LauncherIconManager.setComponentEnabledSettingForTests = null + resetLauncherAliases() + } + + @Test + fun `selecting red green applies deuteranopia launcher icon`() { + val scenario = accessibilityFragment() + + scenario.root.findViewById(R.id.chipColorVisionRedGreen).performClick() + shadowOf(Looper.getMainLooper()).idle() + + assertEquals(LauncherIconVariant.DEUTERANOPIA, LauncherIconManager.current(context)) + assertEquals( + scenario.activity.getString(R.string.settings_color_vision_icon_changed_warning), + latestSnackbarText(scenario.activity.findViewById(android.R.id.content)), + ) + } + + @Test + fun `ten easter egg taps unlock protanopia sub group`() { + val scenario = accessibilityFragment() + scenario.root.findViewById(R.id.chipColorVisionRedGreen).performClick() + val tapZone = scenario.root.findViewById(R.id.easterEggTapZone) + + repeat(10) { tapZone.performClick() } + shadowOf(Looper.getMainLooper()).idle() + + assertTrue( + AppUiSettings.prefs(context) + .getBoolean(SettingsPrefs.PREF_EASTER_EGG_PROTANOPIA_UNLOCKED, false), + ) + assertEquals(View.VISIBLE, scenario.root.findViewById(R.id.redGreenSubGroupContainer).visibility) + } + + @Test + fun `fewer than ten easter egg taps keep protanopia locked`() { + val scenario = accessibilityFragment() + val tapZone = scenario.root.findViewById(R.id.easterEggTapZone) + + repeat(9) { tapZone.performClick() } + + assertFalse( + AppUiSettings.prefs(context) + .getBoolean(SettingsPrefs.PREF_EASTER_EGG_PROTANOPIA_UNLOCKED, false), + ) + assertEquals(View.GONE, scenario.root.findViewById(R.id.redGreenSubGroupContainer).visibility) + } + + @Test + fun `selecting protanopia sub variant applies protanopia launcher icon`() { + AppUiSettings.prefs(context).edit { + putString(SettingsPrefs.PREF_COLOR_VISION_MODE, ColorVisionMode.RED_GREEN.prefValue) + putBoolean(SettingsPrefs.PREF_EASTER_EGG_PROTANOPIA_UNLOCKED, true) + putString(SettingsPrefs.PREF_RED_GREEN_ICON_VARIANT, LauncherIconVariant.DEUTERANOPIA.prefValue) + } + val scenario = accessibilityFragment() + + scenario.root.findViewById(R.id.chipRedGreenProtanopia).performClick() + shadowOf(Looper.getMainLooper()).idle() + + assertEquals(LauncherIconVariant.PROTANOPIA, LauncherIconManager.current(context)) + } + + @Test + fun `switching from red green to achromatopsia hides sub group and applies monochrome icon`() { + AppUiSettings.prefs(context).edit { + putString(SettingsPrefs.PREF_COLOR_VISION_MODE, ColorVisionMode.RED_GREEN.prefValue) + putBoolean(SettingsPrefs.PREF_EASTER_EGG_PROTANOPIA_UNLOCKED, true) + } + val scenario = accessibilityFragment() + + scenario.root.findViewById(R.id.chipColorVisionAchromatopsia).performClick() + shadowOf(Looper.getMainLooper()).idle() + + assertEquals(View.GONE, scenario.root.findViewById(R.id.redGreenSubGroupContainer).visibility) + assertEquals(LauncherIconVariant.MONOCHROME, LauncherIconManager.current(context)) + } + + private fun accessibilityFragment(): AccessibilityScenario { + val activity = Robolectric.buildActivity(SettingsActivity::class.java).setup().get() + activity.supportFragmentManager.beginTransaction() + .replace(R.id.settingsFragmentContainer, SettingsAccessibilityFragment()) + .commitNow() + val fragment = activity.supportFragmentManager + .findFragmentById(R.id.settingsFragmentContainer) as SettingsAccessibilityFragment + return AccessibilityScenario(activity, fragment.requireView()) + } + + private fun latestSnackbarText(root: View): String { + return findSnackbarText(root)?.text?.toString().orEmpty() + } + + private fun findSnackbarText(view: View): TextView? { + if (view.id == com.google.android.material.R.id.snackbar_text) { + return view as TextView + } + if (view is ViewGroup) { + for (index in 0 until view.childCount) { + findSnackbarText(view.getChildAt(index))?.let { return it } + } + } + return null + } + + private fun resetLauncherAliases() { + LauncherIconVariant.entries.forEach { variant -> + context.packageManager.setComponentEnabledSetting( + ComponentName(context.packageName, variant.aliasClass), + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, + PackageManager.DONT_KILL_APP, + ) + } + } + + private data class AccessibilityScenario( + val activity: SettingsActivity, + val root: View, + ) +} diff --git a/fastlane/metadata/android/en-US/changelogs/20612.txt b/fastlane/metadata/android/en-US/changelogs/20612.txt new file mode 100644 index 0000000..fb33663 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/20612.txt @@ -0,0 +1 @@ +Launcher icon now follows the selected color-vision accessibility mode. diff --git a/fastlane/metadata/android/fa/changelogs/20612.txt b/fastlane/metadata/android/fa/changelogs/20612.txt new file mode 100644 index 0000000..41d6d16 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/20612.txt @@ -0,0 +1 @@ +نماد برنامه اکنون از حالت دید رنگ انتخاب‌شده پیروی می‌کند. diff --git a/fastlane/metadata/android/ru-RU/changelogs/20612.txt b/fastlane/metadata/android/ru-RU/changelogs/20612.txt new file mode 100644 index 0000000..fda30c1 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/20612.txt @@ -0,0 +1 @@ +Иконка приложения теперь следует выбранному режиму цветового зрения. diff --git a/fastlane/metadata/android/zh-CN/changelogs/20612.txt b/fastlane/metadata/android/zh-CN/changelogs/20612.txt new file mode 100644 index 0000000..9a812d5 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/20612.txt @@ -0,0 +1 @@ +应用图标现在会跟随所选的色觉辅助模式。