|
|
|
@ -22,10 +22,7 @@ import androidx.activity.SystemBarStyle
|
|
|
|
|
import androidx.activity.compose.setContent
|
|
|
|
|
import androidx.activity.enableEdgeToEdge
|
|
|
|
|
import androidx.activity.viewModels
|
|
|
|
|
import androidx.compose.foundation.isSystemInDarkTheme
|
|
|
|
|
import androidx.compose.runtime.Composable
|
|
|
|
|
import androidx.compose.runtime.CompositionLocalProvider
|
|
|
|
|
import androidx.compose.runtime.DisposableEffect
|
|
|
|
|
import androidx.compose.runtime.getValue
|
|
|
|
|
import androidx.compose.runtime.mutableStateOf
|
|
|
|
|
import androidx.compose.runtime.setValue
|
|
|
|
@ -35,21 +32,22 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
|
|
|
import androidx.lifecycle.lifecycleScope
|
|
|
|
|
import androidx.lifecycle.repeatOnLifecycle
|
|
|
|
|
import androidx.metrics.performance.JankStats
|
|
|
|
|
import androidx.tracing.trace
|
|
|
|
|
import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading
|
|
|
|
|
import com.google.samples.apps.nowinandroid.MainActivityUiState.Success
|
|
|
|
|
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
|
|
|
|
|
import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
|
|
|
|
|
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
|
|
|
|
|
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
|
|
|
|
|
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
|
|
|
|
|
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
|
|
|
|
|
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
|
|
|
|
|
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
|
|
|
|
|
import com.google.samples.apps.nowinandroid.core.ui.LocalTimeZone
|
|
|
|
|
import com.google.samples.apps.nowinandroid.ui.NiaApp
|
|
|
|
|
import com.google.samples.apps.nowinandroid.ui.rememberNiaAppState
|
|
|
|
|
import com.google.samples.apps.nowinandroid.util.isSystemInDarkTheme
|
|
|
|
|
import dagger.hilt.android.AndroidEntryPoint
|
|
|
|
|
import kotlinx.coroutines.flow.collect
|
|
|
|
|
import kotlinx.coroutines.flow.combine
|
|
|
|
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
|
|
|
import kotlinx.coroutines.flow.map
|
|
|
|
|
import kotlinx.coroutines.flow.onEach
|
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
|
import javax.inject.Inject
|
|
|
|
@ -81,53 +79,60 @@ class MainActivity : ComponentActivity() {
|
|
|
|
|
val splashScreen = installSplashScreen()
|
|
|
|
|
super.onCreate(savedInstanceState)
|
|
|
|
|
|
|
|
|
|
var uiState: MainActivityUiState by mutableStateOf(Loading)
|
|
|
|
|
// We keep this as a mutable state, so that we can track changes inside the composition.
|
|
|
|
|
// This allows us to react to dark/light mode changes.
|
|
|
|
|
var themeSettings by mutableStateOf(
|
|
|
|
|
ThemeSettings(
|
|
|
|
|
darkTheme = resources.configuration.isSystemInDarkTheme,
|
|
|
|
|
androidTheme = Loading.shouldUseAndroidTheme,
|
|
|
|
|
disableDynamicTheming = Loading.shouldDisableDynamicTheming,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Update the uiState
|
|
|
|
|
lifecycleScope.launch {
|
|
|
|
|
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
|
|
|
viewModel.uiState
|
|
|
|
|
.onEach { uiState = it }
|
|
|
|
|
.collect()
|
|
|
|
|
combine(
|
|
|
|
|
isSystemInDarkTheme(),
|
|
|
|
|
viewModel.uiState,
|
|
|
|
|
) { systemDark, uiState ->
|
|
|
|
|
ThemeSettings(
|
|
|
|
|
darkTheme = uiState.shouldUseDarkTheme(systemDark),
|
|
|
|
|
androidTheme = uiState.shouldUseAndroidTheme,
|
|
|
|
|
disableDynamicTheming = uiState.shouldDisableDynamicTheming,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
.onEach { themeSettings = it }
|
|
|
|
|
.map { it.darkTheme }
|
|
|
|
|
.distinctUntilChanged()
|
|
|
|
|
.collect { darkTheme ->
|
|
|
|
|
trace("niaEdgeToEdge") {
|
|
|
|
|
// Turn off the decor fitting system windows, which allows us to handle insets,
|
|
|
|
|
// including IME animations, and go edge-to-edge.
|
|
|
|
|
// This is the same parameters as the default enableEdgeToEdge call, but we manually
|
|
|
|
|
// resolve whether or not to show dark theme using uiState, since it can be different
|
|
|
|
|
// than the configuration's dark theme value based on the user preference.
|
|
|
|
|
enableEdgeToEdge(
|
|
|
|
|
statusBarStyle = SystemBarStyle.auto(
|
|
|
|
|
lightScrim = android.graphics.Color.TRANSPARENT,
|
|
|
|
|
darkScrim = android.graphics.Color.TRANSPARENT,
|
|
|
|
|
) { darkTheme },
|
|
|
|
|
navigationBarStyle = SystemBarStyle.auto(
|
|
|
|
|
lightScrim = lightScrim,
|
|
|
|
|
darkScrim = darkScrim,
|
|
|
|
|
) { darkTheme },
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keep the splash screen on-screen until the UI state is loaded. This condition is
|
|
|
|
|
// evaluated each time the app needs to be redrawn so it should be fast to avoid blocking
|
|
|
|
|
// the UI.
|
|
|
|
|
splashScreen.setKeepOnScreenCondition {
|
|
|
|
|
when (uiState) {
|
|
|
|
|
Loading -> true
|
|
|
|
|
is Success -> false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Turn off the decor fitting system windows, which allows us to handle insets,
|
|
|
|
|
// including IME animations, and go edge-to-edge
|
|
|
|
|
// This also sets up the initial system bar style based on the platform theme
|
|
|
|
|
enableEdgeToEdge()
|
|
|
|
|
splashScreen.setKeepOnScreenCondition { viewModel.uiState.value.shouldKeepSplashScreen() }
|
|
|
|
|
|
|
|
|
|
setContent {
|
|
|
|
|
val darkTheme = shouldUseDarkTheme(uiState)
|
|
|
|
|
|
|
|
|
|
// Update the edge to edge configuration to match the theme
|
|
|
|
|
// This is the same parameters as the default enableEdgeToEdge call, but we manually
|
|
|
|
|
// resolve whether or not to show dark theme using uiState, since it can be different
|
|
|
|
|
// than the configuration's dark theme value based on the user preference.
|
|
|
|
|
DisposableEffect(darkTheme) {
|
|
|
|
|
enableEdgeToEdge(
|
|
|
|
|
statusBarStyle = SystemBarStyle.auto(
|
|
|
|
|
android.graphics.Color.TRANSPARENT,
|
|
|
|
|
android.graphics.Color.TRANSPARENT,
|
|
|
|
|
) { darkTheme },
|
|
|
|
|
navigationBarStyle = SystemBarStyle.auto(
|
|
|
|
|
lightScrim,
|
|
|
|
|
darkScrim,
|
|
|
|
|
) { darkTheme },
|
|
|
|
|
)
|
|
|
|
|
onDispose {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val appState = rememberNiaAppState(
|
|
|
|
|
networkMonitor = networkMonitor,
|
|
|
|
|
userNewsResourceRepository = userNewsResourceRepository,
|
|
|
|
@ -141,9 +146,9 @@ class MainActivity : ComponentActivity() {
|
|
|
|
|
LocalTimeZone provides currentTimeZone,
|
|
|
|
|
) {
|
|
|
|
|
NiaTheme(
|
|
|
|
|
darkTheme = darkTheme,
|
|
|
|
|
androidTheme = shouldUseAndroidTheme(uiState),
|
|
|
|
|
disableDynamicTheming = shouldDisableDynamicTheming(uiState),
|
|
|
|
|
darkTheme = themeSettings.darkTheme,
|
|
|
|
|
androidTheme = themeSettings.androidTheme,
|
|
|
|
|
disableDynamicTheming = themeSettings.disableDynamicTheming,
|
|
|
|
|
) {
|
|
|
|
|
NiaApp(appState)
|
|
|
|
|
}
|
|
|
|
@ -162,47 +167,6 @@ class MainActivity : ComponentActivity() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns `true` if the Android theme should be used, as a function of the [uiState].
|
|
|
|
|
*/
|
|
|
|
|
@Composable
|
|
|
|
|
private fun shouldUseAndroidTheme(
|
|
|
|
|
uiState: MainActivityUiState,
|
|
|
|
|
): Boolean = when (uiState) {
|
|
|
|
|
Loading -> false
|
|
|
|
|
is Success -> when (uiState.userData.themeBrand) {
|
|
|
|
|
ThemeBrand.DEFAULT -> false
|
|
|
|
|
ThemeBrand.ANDROID -> true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns `true` if the dynamic color is disabled, as a function of the [uiState].
|
|
|
|
|
*/
|
|
|
|
|
@Composable
|
|
|
|
|
private fun shouldDisableDynamicTheming(
|
|
|
|
|
uiState: MainActivityUiState,
|
|
|
|
|
): Boolean = when (uiState) {
|
|
|
|
|
Loading -> false
|
|
|
|
|
is Success -> !uiState.userData.useDynamicColor
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns `true` if dark theme should be used, as a function of the [uiState] and the
|
|
|
|
|
* current system context.
|
|
|
|
|
*/
|
|
|
|
|
@Composable
|
|
|
|
|
private fun shouldUseDarkTheme(
|
|
|
|
|
uiState: MainActivityUiState,
|
|
|
|
|
): Boolean = when (uiState) {
|
|
|
|
|
Loading -> isSystemInDarkTheme()
|
|
|
|
|
is Success -> when (uiState.userData.darkThemeConfig) {
|
|
|
|
|
DarkThemeConfig.FOLLOW_SYSTEM -> isSystemInDarkTheme()
|
|
|
|
|
DarkThemeConfig.LIGHT -> false
|
|
|
|
|
DarkThemeConfig.DARK -> true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The default light scrim, as defined by androidx and the platform:
|
|
|
|
|
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=35-38;drc=27e7d52e8604a080133e8b842db10c89b4482598
|
|
|
|
@ -214,3 +178,13 @@ private val lightScrim = android.graphics.Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
|
|
|
|
|
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=40-44;drc=27e7d52e8604a080133e8b842db10c89b4482598
|
|
|
|
|
*/
|
|
|
|
|
private val darkScrim = android.graphics.Color.argb(0x80, 0x1b, 0x1b, 0x1b)
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Class for the system theme settings.
|
|
|
|
|
* This wrapping class allows us to combine all the changes and prevent unnecessary recompositions.
|
|
|
|
|
*/
|
|
|
|
|
data class ThemeSettings(
|
|
|
|
|
val darkTheme: Boolean,
|
|
|
|
|
val androidTheme: Boolean,
|
|
|
|
|
val disableDynamicTheming: Boolean,
|
|
|
|
|
)
|
|
|
|
|