Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ dependencies {
implementation(libs.androidx.compose.runtime.tracing)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.lifecycle.runtimeCompose)
implementation(libs.androidx.lifecycle.viewModel.navigation3)
implementation(libs.androidx.profileinstaller)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,17 @@ import android.app.Application
import android.content.pm.ApplicationInfo
import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy.Builder
import androidx.datastore.core.DataStore
import coil.ImageLoader
import coil.ImageLoaderFactory
import com.google.samples.apps.nowinandroid.core.datastore.UserPreferences
import com.google.samples.apps.nowinandroid.core.network.di.ApplicationScope
import com.google.samples.apps.nowinandroid.sync.initializers.Sync
import com.google.samples.apps.nowinandroid.util.ProfileVerifierLogger
import com.google.samples.apps.nowinandroid.util.initializeNightModeFromPreferences
import com.google.samples.apps.nowinandroid.util.observeNightModePreferences
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
import javax.inject.Inject

/**
Expand All @@ -35,12 +41,24 @@ class NiaApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var imageLoader: dagger.Lazy<ImageLoader>

@Inject
lateinit var userPrefsDataStore: DataStore<UserPreferences>

@Inject
lateinit var profileVerifierLogger: ProfileVerifierLogger

@Inject
@ApplicationScope
lateinit var applicationScope: CoroutineScope

override fun onCreate() {
super.onCreate()

// Initialize dark mode from user prefs
initializeNightModeFromPreferences(userPrefsDataStore)

observeNightModePreferences(userPrefsDataStore, applicationScope)

setStrictModePolicy()

// Initialize Sync; the system responsible for keeping data in the app up to date.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,28 @@

package com.google.samples.apps.nowinandroid.util

import android.app.UiModeManager
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import androidx.activity.ComponentActivity
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.util.Consumer
import androidx.datastore.core.DataStore
import com.google.samples.apps.nowinandroid.core.datastore.DarkThemeConfigProto
import com.google.samples.apps.nowinandroid.core.datastore.UserPreferences
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

/**
* Convenience wrapper for dark mode checking
Expand All @@ -47,3 +62,81 @@ fun ComponentActivity.isSystemInDarkTheme() = callbackFlow {
}
.distinctUntilChanged()
.conflate()

/**
* Converts [DarkThemeConfig] to AppCompat night mode constant.
*/
fun DarkThemeConfig.toNightMode(): Int = when (this) {
DarkThemeConfig.FOLLOW_SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
DarkThemeConfig.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
DarkThemeConfig.DARK -> AppCompatDelegate.MODE_NIGHT_YES
}

/**
* Maps a [DarkThemeConfig] value to the corresponding night mode setting
* used by UiModeManager on Android 12 (API level 31) and above.
*/
@RequiresApi(Build.VERSION_CODES.S)
fun DarkThemeConfig.toUiNightMode(): Int = when(this) {
DarkThemeConfig.FOLLOW_SYSTEM -> UiModeManager.MODE_NIGHT_AUTO
DarkThemeConfig.LIGHT -> UiModeManager.MODE_NIGHT_NO
DarkThemeConfig.DARK -> UiModeManager.MODE_NIGHT_YES
}

/**
* Applies this [DarkThemeConfig] as default night mode.
*/
fun DarkThemeConfig.applyAsDefaultNightMode(context: Context) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
uiModeManager.setApplicationNightMode(toUiNightMode())
} else {
AppCompatDelegate.setDefaultNightMode(toNightMode())
}
}

/**
* Converts stored proto data into a DarkThemeConfig object.
*/
fun DarkThemeConfigProto.toDarkThemeConfig(): DarkThemeConfig = when (this) {
DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT -> DarkThemeConfig.LIGHT
DarkThemeConfigProto.DARK_THEME_CONFIG_DARK -> DarkThemeConfig.DARK
else -> DarkThemeConfig.FOLLOW_SYSTEM
}

/**
* Sets night mode from user prefs. Call in Application.onCreate() to show correct splash theme.
*/
fun Context.initializeNightModeFromPreferences(userPrefsDataStore: DataStore<UserPreferences>) {
runBlocking {
runCatching {
val darkThemeConfig = userPrefsDataStore.data
.first()
.darkThemeConfig
.toDarkThemeConfig()

darkThemeConfig.applyAsDefaultNightMode(this@initializeNightModeFromPreferences)
}.onFailure {
DarkThemeConfig.FOLLOW_SYSTEM.applyAsDefaultNightMode(this@initializeNightModeFromPreferences)
}
}
}

/**
* Observe theme changes and updates UiModeManager to prevent a bug where the first cold start
* uses the previous theme, while later starts use the correct one.
*/
fun Context.observeNightModePreferences(
userPrefsDataStore: DataStore<UserPreferences>,
scope: CoroutineScope,
) {
scope.launch {
userPrefsDataStore.data
.map { it.darkThemeConfig.toDarkThemeConfig() }
.distinctUntilChanged()
.drop(1) // Skip first emission (already handled by initializeNightModeFromPreferences)
.collect { darkThemeConfig ->
darkThemeConfig.applyAsDefaultNightMode(this@observeNightModePreferences)
}
}
}