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