feat: редизайн иконки
Some checks are pending
CI / build (push) Waiting to run

This commit is contained in:
xtclovver 2026-05-07 03:38:54 +03:00
parent e2d2d1fb11
commit 4a9ed45762
45 changed files with 973 additions and 93 deletions

View file

@ -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")

View file

@ -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">
<activity
android:name=".MainActivity"
android:exported="true">
android:exported="true" />
<activity-alias
android:name=".LauncherOriginal"
android:targetActivity=".MainActivity"
android:enabled="true"
android:exported="true"
android:icon="@mipmap/ic_launcher_n04_original"
android:roundIcon="@mipmap/ic_launcher_n04_original_round"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</activity-alias>
<activity-alias
android:name=".LauncherProtanopia"
android:targetActivity=".MainActivity"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_n04_protanopia"
android:roundIcon="@mipmap/ic_launcher_n04_protanopia_round"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".LauncherDeuteranopia"
android:targetActivity=".MainActivity"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_n04_deuteranopia"
android:roundIcon="@mipmap/ic_launcher_n04_deuteranopia_round"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".LauncherTritanopia"
android:targetActivity=".MainActivity"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_n04_tritanopia"
android:roundIcon="@mipmap/ic_launcher_n04_tritanopia_round"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".LauncherMonochrome"
android:targetActivity=".MainActivity"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_n04_monochrome"
android:roundIcon="@mipmap/ic_launcher_n04_monochrome_round"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity
android:name=".SettingsActivity"
android:exported="false" />

View file

@ -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)
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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<View>(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<View>(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
}
}

View file

@ -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) {

View file

@ -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"
}

View file

@ -1,74 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Magnifier glass background -->
<path
android:fillColor="#20203A"
android:pathData="M54,54m-28,0a28,28 0,1 1,56 0a28,28 0,1 1,-56 0" />
<!-- Magnifier glass border -->
<path
android:fillColor="#00000000"
android:pathData="M54,54m-28,0a28,28 0,1 1,56 0a28,28 0,1 1,-56 0"
android:strokeColor="#C8C5D0"
android:strokeWidth="3" />
<!-- Outer pulse ring -->
<path
android:fillColor="#00000000"
android:pathData="M54,54m-21,0a21,21 0,1 1,42 0a21,21 0,1 1,-42 0"
android:strokeColor="#B3261E"
android:strokeWidth="1.2"
android:strokeAlpha="0.25" />
<!-- Dashed detection ring (approximated with arc segments) -->
<path
android:fillColor="#00000000"
android:pathData="M54,38 A16,16 0 0,1 70,54"
android:strokeColor="#B3261E"
android:strokeWidth="2"
android:strokeLineCap="round" />
<path
android:fillColor="#00000000"
android:pathData="M70,54 A16,16 0 0,1 54,70"
android:strokeColor="#B3261E"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeAlpha="0.4" />
<path
android:fillColor="#00000000"
android:pathData="M54,70 A16,16 0 0,1 38,54"
android:strokeColor="#B3261E"
android:strokeWidth="2"
android:strokeLineCap="round" />
<path
android:fillColor="#00000000"
android:pathData="M38,54 A16,16 0 0,1 54,38"
android:strokeColor="#B3261E"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeAlpha="0.4" />
<!-- Centre red dot -->
<path
android:fillColor="#B3261E"
android:pathData="M54,54m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0" />
<!-- Centre highlight -->
<path
android:fillColor="#FFDAD6"
android:pathData="M54,54m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0" />
<!-- Handle -->
<path
android:fillColor="#00000000"
android:pathData="M73.8,73.8 L86,86"
android:strokeColor="#C8C5D0"
android:strokeWidth="5.5"
android:strokeLineCap="round" />
</vector>

View file

@ -5,6 +5,6 @@
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#1A1A26"
android:fillColor="#F4EFE6"
android:pathData="M0,0h108v108h-108z" />
</vector>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#0E1116"
android:pathData="M-4.219,80.156 L67.5,-4.219 L88.594,0 L16.875,84.375 Z" />
<path
android:fillColor="#D55E00"
android:pathData="M31.640625 35.02880859375H40.4736328125L39.056396484375 41.818359375Q44.000244140625 34.204833984375 49.76806640625 34.204833984375Q51.8115234375 34.204833984375 54.151611328125 35.2265625L50.526123046875 42.971923828125Q49.24072265625 42.510498046875 47.79052734375 42.510498046875Q45.3515625 42.510498046875 42.8302001953125 44.356201171875Q40.308837890625 46.201904296875 38.8916015625 49.2835693359375Q37.474365234375 52.365234375 36.090087890625 59.055908203125L33.782958984375 70.03125H24.32373046875Z M56.91357421875 70.03125H47.454345703125L57.57275390625 21.71337890625H67.031982421875L63.406494140625 38.91796875Q66.603515625 36.4130859375 69.3720703125 35.3089599609375Q72.140625 34.204833984375 75.3046875 34.204833984375Q79.556396484375 34.204833984375 82.0283203125 36.643798828125Q84.500244140625 39.082763671875 84.500244140625 43.037841796875Q84.500244140625 45.147216796875 83.577392578125 49.563720703125L79.292724609375 70.03125H69.83349609375L74.217041015625 49.168212890625Q74.9091796875 45.7734375 74.9091796875 44.817626953125Q74.9091796875 43.07080078125 73.821533203125 42.01611328125Q72.73388671875 40.96142578125 70.855224609375 40.96142578125Q68.482177734375 40.96142578125 66.306884765625 42.609375Q63.472412109375 44.78466796875 61.9892578125 47.94873046875Q61.165283203125 49.695556640625 59.81396484375 56.155517578125Z" />
<path
android:fillColor="#0072B2"
android:pathData="M90.703,16.875m-2.953,0a2.953,2.953 0,1 1,5.906 0a2.953,2.953 0,1 1,-5.906 0" />
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#F4EFE6"
android:pathData="M0,0h108v108h-108z" />
</vector>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#0E1116"
android:pathData="M-4.219,80.156 L67.5,-4.219 L88.594,0 L16.875,84.375 Z" />
<path
android:fillColor="#5A5A5A"
android:pathData="M31.640625 35.02880859375H40.4736328125L39.056396484375 41.818359375Q44.000244140625 34.204833984375 49.76806640625 34.204833984375Q51.8115234375 34.204833984375 54.151611328125 35.2265625L50.526123046875 42.971923828125Q49.24072265625 42.510498046875 47.79052734375 42.510498046875Q45.3515625 42.510498046875 42.8302001953125 44.356201171875Q40.308837890625 46.201904296875 38.8916015625 49.2835693359375Q37.474365234375 52.365234375 36.090087890625 59.055908203125L33.782958984375 70.03125H24.32373046875Z M56.91357421875 70.03125H47.454345703125L57.57275390625 21.71337890625H67.031982421875L63.406494140625 38.91796875Q66.603515625 36.4130859375 69.3720703125 35.3089599609375Q72.140625 34.204833984375 75.3046875 34.204833984375Q79.556396484375 34.204833984375 82.0283203125 36.643798828125Q84.500244140625 39.082763671875 84.500244140625 43.037841796875Q84.500244140625 45.147216796875 83.577392578125 49.563720703125L79.292724609375 70.03125H69.83349609375L74.217041015625 49.168212890625Q74.9091796875 45.7734375 74.9091796875 44.817626953125Q74.9091796875 43.07080078125 73.821533203125 42.01611328125Q72.73388671875 40.96142578125 70.855224609375 40.96142578125Q68.482177734375 40.96142578125 66.306884765625 42.609375Q63.472412109375 44.78466796875 61.9892578125 47.94873046875Q61.165283203125 49.695556640625 59.81396484375 56.155517578125Z" />
<path
android:fillColor="#9A9A9A"
android:pathData="M90.703,16.875m-2.953,0a2.953,2.953 0,1 1,5.906 0a2.953,2.953 0,1 1,-5.906 0" />
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#F4EFE6"
android:pathData="M0,0h108v108h-108z" />
</vector>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#0E1116"
android:pathData="M-4.219,80.156 L67.5,-4.219 L88.594,0 L16.875,84.375 Z" />
<path
android:fillColor="#C8332C"
android:pathData="M31.640625 35.02880859375H40.4736328125L39.056396484375 41.818359375Q44.000244140625 34.204833984375 49.76806640625 34.204833984375Q51.8115234375 34.204833984375 54.151611328125 35.2265625L50.526123046875 42.971923828125Q49.24072265625 42.510498046875 47.79052734375 42.510498046875Q45.3515625 42.510498046875 42.8302001953125 44.356201171875Q40.308837890625 46.201904296875 38.8916015625 49.2835693359375Q37.474365234375 52.365234375 36.090087890625 59.055908203125L33.782958984375 70.03125H24.32373046875Z M56.91357421875 70.03125H47.454345703125L57.57275390625 21.71337890625H67.031982421875L63.406494140625 38.91796875Q66.603515625 36.4130859375 69.3720703125 35.3089599609375Q72.140625 34.204833984375 75.3046875 34.204833984375Q79.556396484375 34.204833984375 82.0283203125 36.643798828125Q84.500244140625 39.082763671875 84.500244140625 43.037841796875Q84.500244140625 45.147216796875 83.577392578125 49.563720703125L79.292724609375 70.03125H69.83349609375L74.217041015625 49.168212890625Q74.9091796875 45.7734375 74.9091796875 44.817626953125Q74.9091796875 43.07080078125 73.821533203125 42.01611328125Q72.73388671875 40.96142578125 70.855224609375 40.96142578125Q68.482177734375 40.96142578125 66.306884765625 42.609375Q63.472412109375 44.78466796875 61.9892578125 47.94873046875Q61.165283203125 49.695556640625 59.81396484375 56.155517578125Z" />
<path
android:fillColor="#1F8A5B"
android:pathData="M90.703,16.875m-2.953,0a2.953,2.953 0,1 1,5.906 0a2.953,2.953 0,1 1,-5.906 0" />
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#F4EFE6"
android:pathData="M0,0h108v108h-108z" />
</vector>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#0E1116"
android:pathData="M-4.219,80.156 L67.5,-4.219 L88.594,0 L16.875,84.375 Z" />
<path
android:fillColor="#0072B2"
android:pathData="M31.640625 35.02880859375H40.4736328125L39.056396484375 41.818359375Q44.000244140625 34.204833984375 49.76806640625 34.204833984375Q51.8115234375 34.204833984375 54.151611328125 35.2265625L50.526123046875 42.971923828125Q49.24072265625 42.510498046875 47.79052734375 42.510498046875Q45.3515625 42.510498046875 42.8302001953125 44.356201171875Q40.308837890625 46.201904296875 38.8916015625 49.2835693359375Q37.474365234375 52.365234375 36.090087890625 59.055908203125L33.782958984375 70.03125H24.32373046875Z M56.91357421875 70.03125H47.454345703125L57.57275390625 21.71337890625H67.031982421875L63.406494140625 38.91796875Q66.603515625 36.4130859375 69.3720703125 35.3089599609375Q72.140625 34.204833984375 75.3046875 34.204833984375Q79.556396484375 34.204833984375 82.0283203125 36.643798828125Q84.500244140625 39.082763671875 84.500244140625 43.037841796875Q84.500244140625 45.147216796875 83.577392578125 49.563720703125L79.292724609375 70.03125H69.83349609375L74.217041015625 49.168212890625Q74.9091796875 45.7734375 74.9091796875 44.817626953125Q74.9091796875 43.07080078125 73.821533203125 42.01611328125Q72.73388671875 40.96142578125 70.855224609375 40.96142578125Q68.482177734375 40.96142578125 66.306884765625 42.609375Q63.472412109375 44.78466796875 61.9892578125 47.94873046875Q61.165283203125 49.695556640625 59.81396484375 56.155517578125Z" />
<path
android:fillColor="#E69F00"
android:pathData="M90.703,16.875m-2.953,0a2.953,2.953 0,1 1,5.906 0a2.953,2.953 0,1 1,-5.906 0" />
</vector>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#FFFFFF"
android:pathData="M-4.219,80.156 L67.5,-4.219 L88.594,0 L16.875,84.375 Z" />
<path
android:fillAlpha="0.22"
android:fillColor="#FFFFFF"
android:pathData="M31.640625 35.02880859375H40.4736328125L39.056396484375 41.818359375Q44.000244140625 34.204833984375 49.76806640625 34.204833984375Q51.8115234375 34.204833984375 54.151611328125 35.2265625L50.526123046875 42.971923828125Q49.24072265625 42.510498046875 47.79052734375 42.510498046875Q45.3515625 42.510498046875 42.8302001953125 44.356201171875Q40.308837890625 46.201904296875 38.8916015625 49.2835693359375Q37.474365234375 52.365234375 36.090087890625 59.055908203125L33.782958984375 70.03125H24.32373046875Z M56.91357421875 70.03125H47.454345703125L57.57275390625 21.71337890625H67.031982421875L63.406494140625 38.91796875Q66.603515625 36.4130859375 69.3720703125 35.3089599609375Q72.140625 34.204833984375 75.3046875 34.204833984375Q79.556396484375 34.204833984375 82.0283203125 36.643798828125Q84.500244140625 39.082763671875 84.500244140625 43.037841796875Q84.500244140625 45.147216796875 83.577392578125 49.563720703125L79.292724609375 70.03125H69.83349609375L74.217041015625 49.168212890625Q74.9091796875 45.7734375 74.9091796875 44.817626953125Q74.9091796875 43.07080078125 73.821533203125 42.01611328125Q72.73388671875 40.96142578125 70.855224609375 40.96142578125Q68.482177734375 40.96142578125 66.306884765625 42.609375Q63.472412109375 44.78466796875 61.9892578125 47.94873046875Q61.165283203125 49.695556640625 59.81396484375 56.155517578125Z" />
<group>
<clip-path
android:name="n04Slash"
android:pathData="M-4.219,80.156 L67.5,-4.219 L88.594,0 L16.875,84.375 Z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M31.640625 35.02880859375H40.4736328125L39.056396484375 41.818359375Q44.000244140625 34.204833984375 49.76806640625 34.204833984375Q51.8115234375 34.204833984375 54.151611328125 35.2265625L50.526123046875 42.971923828125Q49.24072265625 42.510498046875 47.79052734375 42.510498046875Q45.3515625 42.510498046875 42.8302001953125 44.356201171875Q40.308837890625 46.201904296875 38.8916015625 49.2835693359375Q37.474365234375 52.365234375 36.090087890625 59.055908203125L33.782958984375 70.03125H24.32373046875Z M56.91357421875 70.03125H47.454345703125L57.57275390625 21.71337890625H67.031982421875L63.406494140625 38.91796875Q66.603515625 36.4130859375 69.3720703125 35.3089599609375Q72.140625 34.204833984375 75.3046875 34.204833984375Q79.556396484375 34.204833984375 82.0283203125 36.643798828125Q84.500244140625 39.082763671875 84.500244140625 43.037841796875Q84.500244140625 45.147216796875 83.577392578125 49.563720703125L79.292724609375 70.03125H69.83349609375L74.217041015625 49.168212890625Q74.9091796875 45.7734375 74.9091796875 44.817626953125Q74.9091796875 43.07080078125 73.821533203125 42.01611328125Q72.73388671875 40.96142578125 70.855224609375 40.96142578125Q68.482177734375 40.96142578125 66.306884765625 42.609375Q63.472412109375 44.78466796875 61.9892578125 47.94873046875Q61.165283203125 49.695556640625 59.81396484375 56.155517578125Z" />
</group>
<path
android:fillColor="#FFFFFF"
android:pathData="M90.703,16.875m-2.953,0a2.953,2.953 0,1 1,5.906 0a2.953,2.953 0,1 1,-5.906 0" />
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#F4EFE6"
android:pathData="M0,0h108v108h-108z" />
</vector>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#0E1116"
android:pathData="M-4.219,80.156 L67.5,-4.219 L88.594,0 L16.875,84.375 Z" />
<path
android:fillColor="#CC79A7"
android:pathData="M31.640625 35.02880859375H40.4736328125L39.056396484375 41.818359375Q44.000244140625 34.204833984375 49.76806640625 34.204833984375Q51.8115234375 34.204833984375 54.151611328125 35.2265625L50.526123046875 42.971923828125Q49.24072265625 42.510498046875 47.79052734375 42.510498046875Q45.3515625 42.510498046875 42.8302001953125 44.356201171875Q40.308837890625 46.201904296875 38.8916015625 49.2835693359375Q37.474365234375 52.365234375 36.090087890625 59.055908203125L33.782958984375 70.03125H24.32373046875Z M56.91357421875 70.03125H47.454345703125L57.57275390625 21.71337890625H67.031982421875L63.406494140625 38.91796875Q66.603515625 36.4130859375 69.3720703125 35.3089599609375Q72.140625 34.204833984375 75.3046875 34.204833984375Q79.556396484375 34.204833984375 82.0283203125 36.643798828125Q84.500244140625 39.082763671875 84.500244140625 43.037841796875Q84.500244140625 45.147216796875 83.577392578125 49.563720703125L79.292724609375 70.03125H69.83349609375L74.217041015625 49.168212890625Q74.9091796875 45.7734375 74.9091796875 44.817626953125Q74.9091796875 43.07080078125 73.821533203125 42.01611328125Q72.73388671875 40.96142578125 70.855224609375 40.96142578125Q68.482177734375 40.96142578125 66.306884765625 42.609375Q63.472412109375 44.78466796875 61.9892578125 47.94873046875Q61.165283203125 49.695556640625 59.81396484375 56.155517578125Z" />
<path
android:fillColor="#009E73"
android:pathData="M90.703,16.875m-2.953,0a2.953,2.953 0,1 1,5.906 0a2.953,2.953 0,1 1,-5.906 0" />
</vector>

View file

@ -73,6 +73,53 @@
android:text="@string/settings_color_vision_achromatopsia"
app:checkedIconVisible="false" />
</com.google.android.material.chip.ChipGroup>
<View
android:id="@+id/easterEggTapZone"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@android:color/transparent" />
<LinearLayout
android:id="@+id/redGreenSubGroupContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings_color_vision_red_green_icon_label"
android:textColor="?android:attr/textColorPrimary"
android:textSize="15sp"
android:textStyle="bold" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroupRedGreenIcon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:selectionRequired="true"
app:singleSelection="true">
<com.google.android.material.chip.Chip
android:id="@+id/chipRedGreenDeuteranopia"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings_color_vision_icon_deuteranopia"
app:checkedIconVisible="false" />
<com.google.android.material.chip.Chip
android:id="@+id/chipRedGreenProtanopia"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings_color_vision_icon_protanopia"
app:checkedIconVisible="false" />
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_n04_deuteranopia_background" />
<foreground android:drawable="@drawable/ic_launcher_n04_deuteranopia_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_n04_themed" />
</adaptive-icon>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_n04_deuteranopia_background" />
<foreground android:drawable="@drawable/ic_launcher_n04_deuteranopia_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_n04_themed" />
</adaptive-icon>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_n04_monochrome_background" />
<foreground android:drawable="@drawable/ic_launcher_n04_monochrome_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_n04_themed" />
</adaptive-icon>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_n04_monochrome_background" />
<foreground android:drawable="@drawable/ic_launcher_n04_monochrome_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_n04_themed" />
</adaptive-icon>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_n04_original_background" />
<foreground android:drawable="@drawable/ic_launcher_n04_original_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_n04_themed" />
</adaptive-icon>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_n04_original_background" />
<foreground android:drawable="@drawable/ic_launcher_n04_original_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_n04_themed" />
</adaptive-icon>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_n04_protanopia_background" />
<foreground android:drawable="@drawable/ic_launcher_n04_protanopia_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_n04_themed" />
</adaptive-icon>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_n04_protanopia_background" />
<foreground android:drawable="@drawable/ic_launcher_n04_protanopia_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_n04_themed" />
</adaptive-icon>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_n04_tritanopia_background" />
<foreground android:drawable="@drawable/ic_launcher_n04_tritanopia_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_n04_themed" />
</adaptive-icon>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_n04_tritanopia_background" />
<foreground android:drawable="@drawable/ic_launcher_n04_tritanopia_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_n04_themed" />
</adaptive-icon>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -100,6 +100,12 @@
<string name="settings_color_vision_red_green">قرمز-سبز</string>
<string name="settings_color_vision_blue_yellow">آبی-زرد</string>
<string name="settings_color_vision_achromatopsia">Achromatopsia</string>
<string name="settings_color_vision_icon_changed_warning">نماد برنامه به‌روزرسانی شد. در بعضی لانچرها ممکن است برای لحظه‌ای ناپدید و دوباره ظاهر شود.</string>
<string name="settings_color_vision_icon_change_failed">به‌روزرسانی نماد برنامه ناموفق بود.</string>
<string name="settings_easter_egg_protanopia_unlocked">گونهٔ نماد پروتانوپیا باز شد.</string>
<string name="settings_color_vision_red_green_icon_label">سبک نماد قرمز-سبز</string>
<string name="settings_color_vision_icon_deuteranopia">دوترانوپیا</string>
<string name="settings_color_vision_icon_protanopia">پروتانوپیا</string>
<string name="settings_accessibility_status_preview_content">وضعیت: %1$s</string>
<string name="settings_permissions">مجوزها</string>
<string name="settings_permissions_desc">مدیریت مجوزهای برنامه</string>

View file

@ -100,6 +100,12 @@
<string name="settings_color_vision_red_green">Красно-зелёный</string>
<string name="settings_color_vision_blue_yellow">Сине-жёлтый</string>
<string name="settings_color_vision_achromatopsia">Ахроматопсия</string>
<string name="settings_color_vision_icon_changed_warning">Иконка приложения обновлена. В некоторых лаунчерах она может ненадолго исчезнуть и появиться снова.</string>
<string name="settings_color_vision_icon_change_failed">Не удалось обновить иконку приложения.</string>
<string name="settings_easter_egg_protanopia_unlocked">Вариант иконки для протанопии разблокирован.</string>
<string name="settings_color_vision_red_green_icon_label">Стиль иконки для красно-зелёного режима</string>
<string name="settings_color_vision_icon_deuteranopia">Дейтеранопия</string>
<string name="settings_color_vision_icon_protanopia">Протанопия</string>
<string name="settings_accessibility_status_preview_content">Статус: %1$s</string>
<string name="settings_permissions">Разрешения</string>
<string name="settings_permissions_desc">Управление разрешениями приложения</string>

View file

@ -100,6 +100,12 @@
<string name="settings_color_vision_red_green">红绿色</string>
<string name="settings_color_vision_blue_yellow">蓝黄色</string>
<string name="settings_color_vision_achromatopsia">Achromatopsia</string>
<string name="settings_color_vision_icon_changed_warning">应用图标已更新。在某些启动器上,它可能会短暂消失后重新出现。</string>
<string name="settings_color_vision_icon_change_failed">更新应用图标失败。</string>
<string name="settings_easter_egg_protanopia_unlocked">已解锁红色盲图标变体。</string>
<string name="settings_color_vision_red_green_icon_label">红绿色图标样式</string>
<string name="settings_color_vision_icon_deuteranopia">绿色盲</string>
<string name="settings_color_vision_icon_protanopia">红色盲</string>
<string name="settings_accessibility_status_preview_content">状态:%1$s</string>
<string name="settings_permissions">权限</string>
<string name="settings_permissions_desc">管理应用权限</string>

View file

@ -99,6 +99,12 @@
<string name="settings_color_vision_red_green">Red-green</string>
<string name="settings_color_vision_blue_yellow">Blue-yellow</string>
<string name="settings_color_vision_achromatopsia">Achromatopsia</string>
<string name="settings_color_vision_icon_changed_warning">App icon updated. On some launchers it may briefly disappear and re-appear.</string>
<string name="settings_color_vision_icon_change_failed">Failed to update app icon.</string>
<string name="settings_easter_egg_protanopia_unlocked">Protanopia icon variant unlocked.</string>
<string name="settings_color_vision_red_green_icon_label">Red-green icon style</string>
<string name="settings_color_vision_icon_deuteranopia">Deuteranopia</string>
<string name="settings_color_vision_icon_protanopia">Protanopia</string>
<string name="settings_accessibility_status_preview_content">%1$s status</string>
<string name="settings_permissions">Permissions</string>
<string name="settings_permissions_desc">Manage app permissions</string>

View file

@ -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<Pair<ComponentName, Int>>()
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)
}
}

View file

@ -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),
)
}
}

View file

@ -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,
)
}
}
}

View file

@ -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<Chip>(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<Chip>(R.id.chipColorVisionRedGreen).performClick()
val tapZone = scenario.root.findViewById<View>(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<View>(R.id.redGreenSubGroupContainer).visibility)
}
@Test
fun `fewer than ten easter egg taps keep protanopia locked`() {
val scenario = accessibilityFragment()
val tapZone = scenario.root.findViewById<View>(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<View>(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<Chip>(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<Chip>(R.id.chipColorVisionAchromatopsia).performClick()
shadowOf(Looper.getMainLooper()).idle()
assertEquals(View.GONE, scenario.root.findViewById<View>(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,
)
}

View file

@ -0,0 +1 @@
Launcher icon now follows the selected color-vision accessibility mode.

View file

@ -0,0 +1 @@
نماد برنامه اکنون از حالت دید رنگ انتخاب‌شده پیروی می‌کند.

View file

@ -0,0 +1 @@
Иконка приложения теперь следует выбранному режиму цветового зрения.

View file

@ -0,0 +1 @@
应用图标现在会跟随所选的色觉辅助模式。