From 2503a01182bd553d2b6d82d2edc26f33feed0cc8 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Wed, 3 Jun 2026 00:12:43 +0530 Subject: [PATCH] Improve theme uniformity across OEM devices - Make semantic colors and the page indicator theme-aware so they keep adequate contrast in dark mode instead of using fixed values. - Seed a full brand color scheme (#6C63FF) for devices without dynamic color (Android 11 and below) instead of falling back to Material's purple baseline; align the XML AppTheme accent accordingly. - Stop manually forcing transparent system bar colors in the Compose theme; those setters are no-ops on Android 15+ and stripped the system contrast scrim, causing the half-gray navigation bar on One UI. Keep syncing the bar icon appearance with the active theme. - Add a dynamic-color opt-out toggle (Android 12+), defaulting off on One UI where Samsung's palette extraction tends to look off. --- .../java/com/aurora/extensions/Platform.kt | 4 + .../main/java/com/aurora/store/AuroraApp.kt | 9 +- .../store/compose/composable/PageIndicator.kt | 10 +- .../com/aurora/store/compose/theme/Color.kt | 99 ++++++++++++++++++- .../com/aurora/store/compose/theme/Theme.kt | 53 ++++++---- .../ui/preferences/UIPreferenceScreen.kt | 34 +++++++ .../java/com/aurora/store/util/Preferences.kt | 9 ++ app/src/main/res/values-night/themes.xml | 2 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/themes.xml | 2 + 10 files changed, 191 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/aurora/extensions/Platform.kt b/app/src/main/java/com/aurora/extensions/Platform.kt index abedc307b..547c530d1 100644 --- a/app/src/main/java/com/aurora/extensions/Platform.kt +++ b/app/src/main/java/com/aurora/extensions/Platform.kt @@ -68,6 +68,10 @@ val isHuawei: Boolean Build.HARDWARE.lowercase(Locale.getDefault()).contains("kirin") || Build.HARDWARE.lowercase(Locale.getDefault()).contains("hi3") +val isOneUI: Boolean + get() = !getSystemProperty("ro.build.version.oneui").isNullOrBlank() || + Build.MANUFACTURER.equals("samsung", ignoreCase = true) + @get:SuppressLint("PrivateApi") val isMiuiOptimizationDisabled: Boolean get() { diff --git a/app/src/main/java/com/aurora/store/AuroraApp.kt b/app/src/main/java/com/aurora/store/AuroraApp.kt index 650dd515d..5c5aff140 100644 --- a/app/src/main/java/com/aurora/store/AuroraApp.kt +++ b/app/src/main/java/com/aurora/store/AuroraApp.kt @@ -85,8 +85,13 @@ class AuroraApp : Application(), Configuration.Provider, SingletonImageLoader.Fa val themeStyle = Preferences.getInteger(this, Preferences.PREFERENCE_THEME_STYLE) setAppTheme(themeStyle) - // Apply dynamic colors to activities - DynamicColors.applyToActivitiesIfAvailable(this) + // Apply dynamic colors to activities, unless disabled (opt-out, off by default on One UI) + val dynamicColors = Preferences.getBoolean( + this, + Preferences.PREFERENCE_DYNAMIC_COLORS, + Preferences.dynamicColorsDefault + ) + if (dynamicColors) DynamicColors.applyToActivitiesIfAvailable(this) // Required for Shizuku installer if (isPAndAbove) HiddenApiBypass.addHiddenApiExemptions("I", "L") diff --git a/app/src/main/java/com/aurora/store/compose/composable/PageIndicator.kt b/app/src/main/java/com/aurora/store/compose/composable/PageIndicator.kt index 680015acb..495a8d2bf 100644 --- a/app/src/main/java/com/aurora/store/compose/composable/PageIndicator.kt +++ b/app/src/main/java/com/aurora/store/compose/composable/PageIndicator.kt @@ -15,12 +15,12 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics @@ -47,15 +47,13 @@ fun PageIndicator(modifier: Modifier = Modifier, totalPages: Int, currentPage: I Alignment.CenterHorizontally ) ) { + val selectedColor = MaterialTheme.colorScheme.primary + val unselectedColor = MaterialTheme.colorScheme.outlineVariant repeat(totalPages) { iteration -> val page = stringResource(R.string.page, iteration) val isSelected = currentPage == iteration val color by animateColorAsState( - targetValue = if (isSelected) { - Color.DarkGray - } else { - Color.LightGray - }, + targetValue = if (isSelected) selectedColor else unselectedColor, animationSpec = tween() ) val size by animateDpAsState( diff --git a/app/src/main/java/com/aurora/store/compose/theme/Color.kt b/app/src/main/java/com/aurora/store/compose/theme/Color.kt index 8818eebb2..3995533e9 100644 --- a/app/src/main/java/com/aurora/store/compose/theme/Color.kt +++ b/app/src/main/java/com/aurora/store/compose/theme/Color.kt @@ -5,10 +5,101 @@ package com.aurora.store.compose.theme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance -val warningColor = Color(0xFFFF7600) // Amber -val successColor = Color(0xFF1B8738) // Green +/** + * Whether the active [MaterialTheme] is dark. + * + * Derived from the resolved color scheme rather than [androidx.compose.foundation.isSystemInDarkTheme] + * so it stays correct when the user forces a light/dark theme that differs from the system setting. + */ +@Composable +@ReadOnlyComposable +private fun isAppInDarkTheme(): Boolean = MaterialTheme.colorScheme.surface.luminance() < 0.5f -val colorGreen = Color(0xFF4CAF50) -val colorRed = Color(0xFFF44336) +/** + * Amber used to flag warnings/caveats. Lightened in dark theme for adequate contrast. + */ +val warningColor: Color + @Composable @ReadOnlyComposable + get() = if (isAppInDarkTheme()) Color(0xFFFFB74D) else Color(0xFFFF7600) + +/** + * Green used to flag positive/success states. Lightened in dark theme for adequate contrast. + */ +val successColor: Color + @Composable @ReadOnlyComposable + get() = if (isAppInDarkTheme()) Color(0xFF5BD27A) else Color(0xFF1B8738) + +val colorGreen: Color + @Composable @ReadOnlyComposable + get() = if (isAppInDarkTheme()) Color(0xFF81C784) else Color(0xFF388E3C) + +val colorRed: Color + @Composable @ReadOnlyComposable + get() = if (isAppInDarkTheme()) Color(0xFFE57373) else Color(0xFFD32F2F) + +/** + * Brand color schemes seeded from Aurora's accent (#6C63FF), used on devices that don't support + * dynamic color (Android 11 and below) so the full palette stays on-brand instead of falling back + * to Material's default purple baseline. + */ +val BrandLightColorScheme = lightColorScheme( + primary = Color(0xFF6C63FF), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFFE4DFFF), + onPrimaryContainer = Color(0xFF1A0066), + secondary = Color(0xFF5C5D72), + onSecondary = Color(0xFFFFFFFF), + secondaryContainer = Color(0xFFE1E0F9), + onSecondaryContainer = Color(0xFF191A2C), + tertiary = Color(0xFF78536B), + onTertiary = Color(0xFFFFFFFF), + tertiaryContainer = Color(0xFFFFD8EE), + onTertiaryContainer = Color(0xFF2E1126), + background = Color(0xFFFCF8FF), + onBackground = Color(0xFF1B1B21), + surface = Color(0xFFFCF8FF), + onSurface = Color(0xFF1B1B21), + surfaceVariant = Color(0xFFE4E1EC), + onSurfaceVariant = Color(0xFF47464F), + outline = Color(0xFF777680), + outlineVariant = Color(0xFFC8C5D0), + error = Color(0xFFBA1A1A), + onError = Color(0xFFFFFFFF), + errorContainer = Color(0xFFFFDAD6), + onErrorContainer = Color(0xFF410002) +) + +val BrandDarkColorScheme = darkColorScheme( + primary = Color(0xFFC8BFFF), + onPrimary = Color(0xFF31149C), + primaryContainer = Color(0xFF534BD6), + onPrimaryContainer = Color(0xFFE4DFFF), + secondary = Color(0xFFC5C4DD), + onSecondary = Color(0xFF2E2F42), + secondaryContainer = Color(0xFF444559), + onSecondaryContainer = Color(0xFFE1E0F9), + tertiary = Color(0xFFE8B9D5), + onTertiary = Color(0xFF46263B), + tertiaryContainer = Color(0xFF5E3C52), + onTertiaryContainer = Color(0xFFFFD8EE), + background = Color(0xFF131318), + onBackground = Color(0xFFE4E1E9), + surface = Color(0xFF131318), + onSurface = Color(0xFFE4E1E9), + surfaceVariant = Color(0xFF47464F), + onSurfaceVariant = Color(0xFFC8C5D0), + outline = Color(0xFF918F9A), + outlineVariant = Color(0xFF47464F), + error = Color(0xFFFFB4AB), + onError = Color(0xFF690005), + errorContainer = Color(0xFF93000A), + onErrorContainer = Color(0xFFFFDAD6) +) diff --git a/app/src/main/java/com/aurora/store/compose/theme/Theme.kt b/app/src/main/java/com/aurora/store/compose/theme/Theme.kt index 1b5d8cbb7..2740ee724 100644 --- a/app/src/main/java/com/aurora/store/compose/theme/Theme.kt +++ b/app/src/main/java/com/aurora/store/compose/theme/Theme.kt @@ -11,27 +11,23 @@ import android.os.Build import androidx.activity.compose.LocalActivity import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialExpressiveTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.colorResource import androidx.core.view.WindowCompat -import com.aurora.store.R import com.aurora.store.util.Preferences /** - * App theme for Aurora Store based on [MaterialTheme] + * App theme for Aurora Store based on [MaterialExpressiveTheme] */ @Composable fun AuroraTheme(content: @Composable () -> Unit) { @@ -40,28 +36,42 @@ fun AuroraTheme(content: @Composable () -> Unit) { var themeStyle by remember { mutableIntStateOf(Preferences.getInteger(context, Preferences.PREFERENCE_THEME_STYLE)) } + var dynamicColor by remember { + mutableStateOf( + Preferences.getBoolean( + context, + Preferences.PREFERENCE_DYNAMIC_COLORS, + Preferences.dynamicColorsDefault + ) + ) + } DisposableEffect(Unit) { val prefs = Preferences.getPrefs(context) val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - if (key == Preferences.PREFERENCE_THEME_STYLE) { - themeStyle = Preferences.getInteger(context, Preferences.PREFERENCE_THEME_STYLE) + when (key) { + Preferences.PREFERENCE_THEME_STYLE -> + themeStyle = Preferences.getInteger(context, key) + + Preferences.PREFERENCE_DYNAMIC_COLORS -> + dynamicColor = + Preferences.getBoolean(context, key, Preferences.dynamicColorsDefault) } } prefs.registerOnSharedPreferenceChangeListener(listener) onDispose { prefs.unregisterOnSharedPreferenceChangeListener(listener) } } - val isDynamicColorSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + val useDynamicColor = dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S - val lightScheme = if (isDynamicColorSupported) { + val lightScheme = if (useDynamicColor) { dynamicLightColorScheme(context) } else { - lightColorScheme(primary = colorResource(id = R.color.colorAccent)) + BrandLightColorScheme } - val darkScheme = if (isDynamicColorSupported) { + val darkScheme = if (useDynamicColor) { dynamicDarkColorScheme(context) } else { - darkColorScheme(primary = colorResource(id = R.color.colorAccent)) + BrandDarkColorScheme } val colorScheme = when (themeStyle) { @@ -77,21 +87,22 @@ fun AuroraTheme(content: @Composable () -> Unit) { } /** - * Side effect to update the system bars to be transparent and match the theme's light/dark mode. - * This is necessary on OEM devices that don't properly support dynamic theming and may have issues with light/dark status bar icons. + * Keep the status/navigation bar icon appearance in sync with the theme's light/dark mode. + * + * The bars themselves are kept transparent and edge-to-edge by [androidx.activity.enableEdgeToEdge] + * (called once in the host activity); we deliberately don't touch `window.statusBarColor` / + * `navigationBarColor` here since those setters are deprecated no-ops on Android 15+ and would + * otherwise strip the system contrast scrim. Re-applying the appearance is still required because + * the user can force a light/dark theme that differs from the system, and that isn't known when + * edge-to-edge is first configured. */ val view = LocalView.current val activity = LocalActivity.current if (!view.isInEditMode) { SideEffect { val currentActivity = activity ?: return@SideEffect - val window = currentActivity.window - - window.statusBarColor = android.graphics.Color.TRANSPARENT - window.navigationBarColor = android.graphics.Color.TRANSPARENT - WindowCompat - .getInsetsController(window, view) + .getInsetsController(currentActivity.window, view) .apply { isAppearanceLightStatusBars = !darkTheme isAppearanceLightNavigationBars = !darkTheme diff --git a/app/src/main/java/com/aurora/store/compose/ui/preferences/UIPreferenceScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/preferences/UIPreferenceScreen.kt index 872400775..f77dc6c0e 100644 --- a/app/src/main/java/com/aurora/store/compose/ui/preferences/UIPreferenceScreen.kt +++ b/app/src/main/java/com/aurora/store/compose/ui/preferences/UIPreferenceScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewWrapper import androidx.core.net.toUri +import com.aurora.extensions.isSAndAbove import com.aurora.extensions.isTAndAbove import com.aurora.extensions.setAppTheme import com.aurora.store.R @@ -38,6 +39,7 @@ import com.aurora.store.compose.preview.ThemePreviewProvider import com.aurora.store.compose.ui.preferences.network.SingleChoiceDialog import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB +import com.aurora.store.util.Preferences.PREFERENCE_DYNAMIC_COLORS import com.aurora.store.util.Preferences.PREFERENCE_FOR_YOU import com.aurora.store.util.Preferences.PREFERENCE_THEME_STYLE import com.aurora.store.util.save @@ -62,6 +64,15 @@ private fun ScreenContent() { var forYou by remember { mutableStateOf(Preferences.getBoolean(context, PREFERENCE_FOR_YOU, true)) } + var dynamicColors by remember { + mutableStateOf( + Preferences.getBoolean( + context, + PREFERENCE_DYNAMIC_COLORS, + Preferences.dynamicColorsDefault + ) + ) + } var showThemeDialog by remember { mutableStateOf(false) } var showTabDialog by remember { mutableStateOf(false) } @@ -132,6 +143,29 @@ private fun ScreenContent() { supportingContent = { Text(themeEntries.getOrElse(themeStyle) { "" }) } ) } + if (isSAndAbove) { + item { + ListItem( + modifier = Modifier.clickable { + dynamicColors = !dynamicColors + context.save(PREFERENCE_DYNAMIC_COLORS, dynamicColors) + }, + headlineContent = { Text(stringResource(R.string.pref_ui_dynamic_color)) }, + supportingContent = { + Text(stringResource(R.string.pref_ui_dynamic_color_desc)) + }, + trailingContent = { + Switch( + checked = dynamicColors, + onCheckedChange = { checked -> + dynamicColors = checked + context.save(PREFERENCE_DYNAMIC_COLORS, checked) + } + ) + } + ) + } + } item { HorizontalDivider() } item { ListItem(headlineContent = { Text(stringResource(R.string.pref_ui_layout)) }) diff --git a/app/src/main/java/com/aurora/store/util/Preferences.kt b/app/src/main/java/com/aurora/store/util/Preferences.kt index 8741e82c4..b3396683f 100644 --- a/app/src/main/java/com/aurora/store/util/Preferences.kt +++ b/app/src/main/java/com/aurora/store/util/Preferences.kt @@ -24,15 +24,24 @@ import android.content.SharedPreferences import androidx.core.content.edit import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager +import com.aurora.extensions.isOneUI import com.aurora.store.BuildConfig object Preferences { const val PREFERENCE_DEFAULT = "PREFERENCE_DEFAULT" + /** + * Default for [PREFERENCE_DYNAMIC_COLORS]. Dynamic color is opt-out everywhere except One UI, + * where Samsung's palette extraction tends to look off, so it defaults off and users can opt in. + */ + val dynamicColorsDefault: Boolean + get() = !isOneUI + const val PREFERENCE_AUTH_DATA = "PREFERENCE_AUTH_DATA" const val PREFERENCE_INSTALLER_ID = "PREFERENCE_INSTALLER_ID" const val PREFERENCE_THEME_STYLE = "PREFERENCE_THEME_STYLE" + const val PREFERENCE_DYNAMIC_COLORS = "PREFERENCE_DYNAMIC_COLORS" const val PREFERENCE_FOR_YOU = "PREFERENCE_FOR_YOU" const val PREFERENCE_DEFAULT_SELECTED_TAB = "PREFERENCE_DEFAULT_SELECTED_TAB" const val PREFERENCE_INTRO = "PREFERENCE_INTRO" diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 7bbd3f99c..6e503530a 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -4,5 +4,7 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index afc965525..bf7280757 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -414,6 +414,8 @@ Follow system Light Dark + Dynamic colors + Use colors from your wallpaper. Turn off for Aurora\'s brand palette. Layout Select default tab App links diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index f5a9b060e..0171d8f97 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -23,6 +23,8 @@ @style/Chip.Filter @style/AppTheme.BottomSheetStyle @android:color/transparent + + @color/colorAccent