diff --git a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt index fd85af505..147e18168 100644 --- a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt +++ b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt @@ -106,6 +106,7 @@ class Preferences @Inject constructor( val nightStart = rxPrefs.getString("nightStart", "18:00") val nightEnd = rxPrefs.getString("nightEnd", "6:00") val black = rxPrefs.getBoolean("black", false) + val dynamicColors = rxPrefs.getBoolean("dynamicColors", false) val autoColor = rxPrefs.getBoolean("autoColor", true) val systemFont = rxPrefs.getBoolean("systemFont", false) val showStt = rxPrefs.getBoolean("showStt", true) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/base/QkThemedActivity.kt b/presentation/src/main/java/com/moez/QKSMS/common/base/QkThemedActivity.kt index cd83ebfbb..1ca7c67ae 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/base/QkThemedActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/base/QkThemedActivity.kt @@ -100,7 +100,14 @@ abstract class QkThemedActivity : QkActivity() { super.onCreate(savedInstanceState) // When certain preferences change, we need to recreate the activity - val triggers = listOf(prefs.nightMode, prefs.night, prefs.black, prefs.textSize, prefs.systemFont) + val triggers = listOf( + prefs.nightMode, + prefs.night, + prefs.black, + prefs.dynamicColors, + prefs.textSize, + prefs.systemFont + ) Observable.merge(triggers.map { it.asObservable().skip(1) }) .debounce(400, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) @@ -155,6 +162,7 @@ abstract class QkThemedActivity : QkActivity() { */ open fun getActivityThemeRes(black: Boolean) = when { black -> R.style.AppTheme_Black + colors.dynamicColorsSupported && prefs.dynamicColors.get() -> R.style.AppTheme_Dynamic else -> R.style.AppTheme } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt index c93f4dc5a..4a45f01c9 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt @@ -19,6 +19,7 @@ package dev.octoshrimpy.quik.common.util import android.content.Context +import android.content.res.Configuration import android.graphics.Color import androidx.core.content.res.getColorOrThrow import dev.octoshrimpy.quik.R @@ -26,6 +27,7 @@ import dev.octoshrimpy.quik.common.util.extensions.getColorCompat import dev.octoshrimpy.quik.model.Recipient import dev.octoshrimpy.quik.util.Preferences import io.reactivex.Observable +import io.reactivex.rxkotlin.Observables import javax.inject.Inject import javax.inject.Singleton import kotlin.math.absoluteValue @@ -37,6 +39,9 @@ class Colors @Inject constructor( private val prefs: Preferences ) { + val dynamicColorsSupported: Boolean + get() = context.resources.getBoolean(R.bool.dynamic_colors_supported) + data class Theme(val theme: Int, private val colors: Colors) { val highlight by lazy { colors.highlightColorForTheme(theme) } val textPrimary by lazy { colors.textPrimaryOnThemeForColor(theme) } @@ -80,7 +85,8 @@ class Colors @Inject constructor( fun theme(recipient: Recipient? = null): Theme { val pref = prefs.theme(recipient?.id ?: 0) val color = when { - recipient == null || !prefs.autoColor.get() || pref.isSet -> pref.get() + recipient == null -> dynamicThemeColor() ?: pref.get() + !prefs.autoColor.get() || pref.isSet -> pref.get() else -> generateColor(recipient) } return Theme(color, this) @@ -92,10 +98,41 @@ class Colors @Inject constructor( prefs.autoColor.get() -> prefs.theme(recipient.id, generateColor(recipient)) else -> prefs.theme(recipient.id, prefs.theme().get()) } - return pref.asObservable() + val colors = when { + recipient == null -> Observables.combineLatest( + pref.asObservable(), + prefs.dynamicColors.asObservable() + ) { color, _ -> dynamicThemeColor() ?: color } + else -> pref.asObservable() + } + return colors .map { color -> Theme(color, this) } } + private fun dynamicThemeColor(): Int? { + val isNight = prefs.night.get() || + (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == + Configuration.UI_MODE_NIGHT_YES + + return dynamicColor( + android.R.color.system_accent1_600, + android.R.color.system_accent1_200, + isNight + ) + } + + fun dynamicBackgroundColor(isNight: Boolean): Int? = dynamicColor( + android.R.color.system_neutral1_10, + android.R.color.system_neutral1_900, + isNight + ) + + private fun dynamicColor(lightColor: Int, darkColor: Int, isNight: Boolean): Int? { + if (!dynamicColorsSupported || !prefs.dynamicColors.get()) return null + + return context.getColor(if (isNight) darkColor else lightColor) + } + fun highlightColorForTheme(theme: Int): Int = FloatArray(3) .apply { Color.colorToHSV(theme, this) } .let { hsv -> hsv.apply { set(2, 0.75f) } } // 75% value diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivity.kt index fb6ca8bc0..303166fcc 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivity.kt @@ -20,6 +20,7 @@ package dev.octoshrimpy.quik.feature.qkreply import android.app.Activity import android.content.Intent +import android.os.Build import android.os.Bundle import android.speech.RecognizerIntent import android.view.GestureDetector @@ -177,6 +178,7 @@ class QkReplyActivity : QkThemedActivity(), QkReplyView { override fun getActivityThemeRes(black: Boolean) = when { black -> R.style.AppThemeDialog_Black + colors.dynamicColorsSupported && prefs.dynamicColors.get() -> R.style.AppThemeDialog_Dynamic else -> R.style.AppThemeDialog } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt index 07e55208c..9f12e862d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt @@ -143,6 +143,9 @@ class SettingsController : QkController = messageLinkHandlingDialog.adapter.menuItemClicks override fun render(state: SettingsState) { + val dynamicColorsEnabled = colors.dynamicColorsSupported && state.dynamicColors + binding.theme.isEnabled = !dynamicColorsEnabled + binding.theme.alpha = if (dynamicColorsEnabled) 0.5f else 1f binding.theme.findViewById(R.id.themePreview)?.setBackgroundTint(state.theme) binding.night.summary = state.nightModeSummary nightModeDialog.adapter.selectedItem = state.nightModeId @@ -154,6 +157,9 @@ class SettingsController : QkController(SettingsState( nightModeId = prefs.nightMode.get() )) { @@ -79,6 +81,9 @@ class SettingsPresenter @Inject constructor( disposables += prefs.black.asObservable() .subscribe { black -> newState { copy(black = black) } } + disposables += prefs.dynamicColors.asObservable() + .subscribe { dynamicColors -> newState { copy(dynamicColors = dynamicColors) } } + disposables += prefs.notifications().asObservable() .subscribe { enabled -> newState { copy(notificationsEnabled = enabled) } } @@ -178,6 +183,11 @@ class SettingsPresenter @Inject constructor( R.id.black -> prefs.black.set(!prefs.black.get()) + R.id.dynamicColors -> { + prefs.dynamicColors.set(!prefs.dynamicColors.get()) + widgetManager.updateTheme() + } + R.id.autoEmoji -> prefs.autoEmoji.set(!prefs.autoEmoji.get()) R.id.notifications -> externalNavigator.showNotificationSettings() diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt index ed9dfc80a..86d4dd06d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsState.kt @@ -28,6 +28,7 @@ data class SettingsState( val nightStart: String = "", val nightEnd: String = "", val black: Boolean = false, + val dynamicColors: Boolean = false, val autoColor: Boolean = true, val autoEmojiEnabled: Boolean = true, val notificationsEnabled: Boolean = true, diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerPresenter.kt index 7c7ab313e..e532d5bdb 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerPresenter.kt @@ -27,6 +27,7 @@ import dev.octoshrimpy.quik.common.util.Colors import dev.octoshrimpy.quik.manager.BillingManager import dev.octoshrimpy.quik.manager.WidgetManager import dev.octoshrimpy.quik.util.Preferences +import io.reactivex.Observable import io.reactivex.rxkotlin.Observables import javax.inject.Inject import javax.inject.Named @@ -45,7 +46,12 @@ class ThemePickerPresenter @Inject constructor( override fun bindIntents(view: ThemePickerView) { super.bindIntents(view) - theme.asObservable() + val currentTheme: Observable = when (recipientId) { + 0L -> colors.themeObservable().map { it.theme } + else -> theme.asObservable() + } + + currentTheme .autoDisposable(view.scope()) .subscribe { color -> view.setCurrentTheme(color) } @@ -68,7 +74,7 @@ class ThemePickerPresenter @Inject constructor( .subscribe() // Toggle the visibility of the apply group - Observables.combineLatest(theme.asObservable(), view.hsvThemeSelected()) { old, new -> old != new } + Observables.combineLatest(currentTheme, view.hsvThemeSelected()) { old, new -> old != new } .autoDisposable(view.scope()) .subscribe { themeChanged -> newState { copy(applyThemeVisible = themeChanged) } } @@ -95,7 +101,7 @@ class ThemePickerPresenter @Inject constructor( // Reset the theme view.clearHsvThemeClicks() - .withLatestFrom(theme.asObservable()) { _, color -> color } + .withLatestFrom(currentTheme) { _, color -> color } .autoDisposable(view.scope()) .subscribe { color -> view.setCurrentTheme(color) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetProvider.kt b/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetProvider.kt index 120b14f82..0170f157a 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetProvider.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetProvider.kt @@ -116,18 +116,23 @@ class WidgetProvider : AppWidgetProvider() { // Apply colors from theme val night = prefs.night.get() || isNightMode val black = prefs.black.get() + val dynamicBackground = colors.dynamicBackgroundColor(night) - remoteViews.setInt(R.id.background, "setColorFilter", context.getColorCompat(when { - night && black -> R.color.black - night && !black -> R.color.backgroundDark - else -> R.color.white - })) + val backgroundColor = when { + night && black -> context.getColorCompat(R.color.black) + dynamicBackground != null -> dynamicBackground + night -> context.getColorCompat(R.color.backgroundDark) + else -> context.getColorCompat(R.color.white) + } + remoteViews.setInt(R.id.background, "setColorFilter", backgroundColor) - remoteViews.setInt(R.id.toolbar, "setColorFilter", context.getColorCompat(when { - night && black -> R.color.black - night && !black -> R.color.backgroundDark - else -> R.color.backgroundLight - })) + val toolbarColor = when { + night && black -> context.getColorCompat(R.color.black) + dynamicBackground != null -> dynamicBackground + night -> context.getColorCompat(R.color.backgroundDark) + else -> context.getColorCompat(R.color.backgroundLight) + } + remoteViews.setInt(R.id.toolbar, "setColorFilter", toolbarColor) remoteViews.setTextColor(R.id.title, context.getColorCompat(when (night) { true -> R.color.textPrimaryDark diff --git a/presentation/src/main/res/layout/settings_controller.xml b/presentation/src/main/res/layout/settings_controller.xml index 30938a50f..b65e2b7f6 100644 --- a/presentation/src/main/res/layout/settings_controller.xml +++ b/presentation/src/main/res/layout/settings_controller.xml @@ -61,6 +61,14 @@ app:title="@string/settings_black_title" app:widget="@layout/settings_switch_widget" /> + + + + + + + + + + + diff --git a/presentation/src/main/res/values-v31/bools.xml b/presentation/src/main/res/values-v31/bools.xml new file mode 100644 index 000000000..3838df396 --- /dev/null +++ b/presentation/src/main/res/values-v31/bools.xml @@ -0,0 +1,4 @@ + + + true + diff --git a/presentation/src/main/res/values-v31/themes.xml b/presentation/src/main/res/values-v31/themes.xml new file mode 100644 index 000000000..cd016eb8b --- /dev/null +++ b/presentation/src/main/res/values-v31/themes.xml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/presentation/src/main/res/values/bools.xml b/presentation/src/main/res/values/bools.xml new file mode 100644 index 000000000..81e59b7dc --- /dev/null +++ b/presentation/src/main/res/values/bools.xml @@ -0,0 +1,4 @@ + + + false + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index cfbed5b8a..66935f8aa 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -266,6 +266,7 @@ Theme Night mode Pure black night mode + Dynamic colors Start time End time Automatic contact colors diff --git a/presentation/src/main/res/values/themes.xml b/presentation/src/main/res/values/themes.xml index b58737d53..184127399 100644 --- a/presentation/src/main/res/values/themes.xml +++ b/presentation/src/main/res/values/themes.xml @@ -75,6 +75,10 @@ @color/textPrimary +