pull/1789/merge
tjmtic 3 days ago committed by GitHub
commit 519b7ca4b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -37,6 +37,7 @@ import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper 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.analytics.LocalAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor 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.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
@ -64,6 +65,9 @@ class MainActivity : ComponentActivity() {
@Inject @Inject
lateinit var networkMonitor: NetworkMonitor lateinit var networkMonitor: NetworkMonitor
@Inject
lateinit var errorMonitor: ErrorMonitor
@Inject @Inject
lateinit var timeZoneMonitor: TimeZoneMonitor lateinit var timeZoneMonitor: TimeZoneMonitor
@ -135,6 +139,7 @@ class MainActivity : ComponentActivity() {
setContent { setContent {
val appState = rememberNiaAppState( val appState = rememberNiaAppState(
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
) )

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.ui package com.google.samples.apps.nowinandroid.ui
import android.content.Context
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
@ -32,6 +33,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarDuration.Indefinite import androidx.compose.material3.SnackbarDuration.Indefinite
import androidx.compose.material3.SnackbarDuration.Short import androidx.compose.material3.SnackbarDuration.Short
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
@ -54,6 +56,7 @@ import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
@ -71,6 +74,10 @@ import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopAp
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors import com.google.samples.apps.nowinandroid.core.designsystem.theme.GradientColors
import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors import com.google.samples.apps.nowinandroid.core.designsystem.theme.LocalGradientColors
import com.google.samples.apps.nowinandroid.core.model.data.MessageData
import com.google.samples.apps.nowinandroid.core.model.data.MessageType.MESSAGE
import com.google.samples.apps.nowinandroid.core.model.data.MessageType.OFFLINE
import com.google.samples.apps.nowinandroid.core.model.data.MessageType.UNKNOWN
import com.google.samples.apps.nowinandroid.feature.settings.SettingsDialog import com.google.samples.apps.nowinandroid.feature.settings.SettingsDialog
import com.google.samples.apps.nowinandroid.navigation.NiaNavHost import com.google.samples.apps.nowinandroid.navigation.NiaNavHost
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination
@ -83,6 +90,8 @@ fun NiaApp(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(), windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo(),
) { ) {
val context = LocalContext.current
val shouldShowGradientBackground = val shouldShowGradientBackground =
appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU
var showSettingsDialog by rememberSaveable { mutableStateOf(false) } var showSettingsDialog by rememberSaveable { mutableStateOf(false) }
@ -97,16 +106,30 @@ fun NiaApp(
) { ) {
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val isOffline by appState.isOffline.collectAsStateWithLifecycle() val stateMessage by appState.stateMessage.collectAsStateWithLifecycle()
// If user is not connected to the internet show a snack bar to inform them. LaunchedEffect(stateMessage) {
val notConnectedMessage = stringResource(R.string.not_connected) stateMessage?.let { message ->
LaunchedEffect(isOffline) {
if (isOffline) { // Text and Duration values dictated by the UI
snackbarHostState.showSnackbar( val (text, duration) = getSnackbarValues(context, message)
message = notConnectedMessage,
duration = Indefinite, // Determine whether user clicked action button
) val snackBarResult = snackbarHostState.showSnackbar(
message = text,
actionLabel = message.label,
duration = duration,
) == ActionPerformed
// Handle result action
if (snackBarResult) {
message.onConfirm?.invoke()
} else {
message.onDelay?.invoke()
}
// Remove Message from List
appState.errorMonitor.clearMessage(message)
} }
} }
@ -281,3 +304,11 @@ private fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
this?.hierarchy?.any { this?.hierarchy?.any {
it.hasRoute(route) it.hasRoute(route)
} ?: false } ?: false
private fun getSnackbarValues(context: Context, message: MessageData): Pair<String, SnackbarDuration> {
return when (message.type) {
OFFLINE -> context.getString(R.string.not_connected) to SnackbarDuration.Indefinite
is MESSAGE -> (message.type as MESSAGE).value to SnackbarDuration.Long
UNKNOWN -> context.getString(R.string.unknown_error) to SnackbarDuration.Short
}
}

@ -31,8 +31,11 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions import androidx.navigation.navOptions
import androidx.tracing.trace import androidx.tracing.trace
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor 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.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.model.data.MessageData
import com.google.samples.apps.nowinandroid.core.model.data.MessageType
import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank import com.google.samples.apps.nowinandroid.core.ui.TrackDisposableJank
import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.navigateToBookmarks
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.navigateToForYou import com.google.samples.apps.nowinandroid.feature.foryou.navigation.navigateToForYou
@ -53,6 +56,7 @@ import kotlinx.datetime.TimeZone
@Composable @Composable
fun rememberNiaAppState( fun rememberNiaAppState(
networkMonitor: NetworkMonitor, networkMonitor: NetworkMonitor,
errorMonitor: ErrorMonitor,
userNewsResourceRepository: UserNewsResourceRepository, userNewsResourceRepository: UserNewsResourceRepository,
timeZoneMonitor: TimeZoneMonitor, timeZoneMonitor: TimeZoneMonitor,
coroutineScope: CoroutineScope = rememberCoroutineScope(), coroutineScope: CoroutineScope = rememberCoroutineScope(),
@ -63,6 +67,7 @@ fun rememberNiaAppState(
navController, navController,
coroutineScope, coroutineScope,
networkMonitor, networkMonitor,
errorMonitor,
userNewsResourceRepository, userNewsResourceRepository,
timeZoneMonitor, timeZoneMonitor,
) { ) {
@ -70,6 +75,7 @@ fun rememberNiaAppState(
navController = navController, navController = navController,
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
) )
@ -81,6 +87,7 @@ class NiaAppState(
val navController: NavHostController, val navController: NavHostController,
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
networkMonitor: NetworkMonitor, networkMonitor: NetworkMonitor,
val errorMonitor: ErrorMonitor,
userNewsResourceRepository: UserNewsResourceRepository, userNewsResourceRepository: UserNewsResourceRepository,
timeZoneMonitor: TimeZoneMonitor, timeZoneMonitor: TimeZoneMonitor,
) { ) {
@ -115,6 +122,27 @@ class NiaAppState(
initialValue = false, initialValue = false,
) )
private val errorMessages: StateFlow<List<MessageData?>> = errorMonitor.messages.stateIn(
scope = coroutineScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList(),
)
val stateMessage: StateFlow<MessageData?> = combine(isOffline, errorMessages) { offline, errors ->
if (offline) {
// Priority is given to Offline Error Message over other types
MessageData(type = MessageType.OFFLINE)
}
// Otherwise, Display first from error monitor list if exists
else {
errors.firstOrNull()
}
}.stateIn(
scope = coroutineScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = null,
)
/** /**
* Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the * Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the
* route. * route.

@ -17,4 +17,5 @@
<resources> <resources>
<string name="app_name">Now in Android</string> <string name="app_name">Now in Android</string>
<string name="not_connected">⚠️ You arent connected to the internet</string> <string name="not_connected">⚠️ You arent connected to the internet</string>
<string name="unknown_error">Unknown Error</string>
</resources> </resources>

@ -32,6 +32,7 @@ import com.github.takahirom.roborazzi.captureRoboImage
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor 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.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
@ -80,6 +81,9 @@ class NiaAppScreenSizesScreenshotTests {
@Inject @Inject
lateinit var networkMonitor: NetworkMonitor lateinit var networkMonitor: NetworkMonitor
@Inject
lateinit var errorMonitor: ErrorMonitor
@Inject @Inject
lateinit var timeZoneMonitor: TimeZoneMonitor lateinit var timeZoneMonitor: TimeZoneMonitor
@ -123,6 +127,7 @@ class NiaAppScreenSizesScreenshotTests {
NiaTheme { NiaTheme {
val fakeAppState = rememberNiaAppState( val fakeAppState = rememberNiaAppState(
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
) )

@ -29,6 +29,7 @@ import androidx.navigation.testing.TestNavHostController
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestNewsRepository
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
import com.google.samples.apps.nowinandroid.core.testing.util.TestErrorMonitor
import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor
import com.google.samples.apps.nowinandroid.core.testing.util.TestTimeZoneMonitor import com.google.samples.apps.nowinandroid.core.testing.util.TestTimeZoneMonitor
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
@ -60,6 +61,8 @@ class NiaAppStateTest {
// Create the test dependencies. // Create the test dependencies.
private val networkMonitor = TestNetworkMonitor() private val networkMonitor = TestNetworkMonitor()
private var errorMonitor = TestErrorMonitor()
private val timeZoneMonitor = TestTimeZoneMonitor() private val timeZoneMonitor = TestTimeZoneMonitor()
private val userNewsResourceRepository = private val userNewsResourceRepository =
@ -79,6 +82,7 @@ class NiaAppStateTest {
navController = navController, navController = navController,
coroutineScope = backgroundScope, coroutineScope = backgroundScope,
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
) )
@ -101,6 +105,7 @@ class NiaAppStateTest {
composeTestRule.setContent { composeTestRule.setContent {
state = rememberNiaAppState( state = rememberNiaAppState(
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
) )
@ -119,6 +124,7 @@ class NiaAppStateTest {
navController = NavHostController(LocalContext.current), navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope, coroutineScope = backgroundScope,
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
) )
@ -139,6 +145,7 @@ class NiaAppStateTest {
navController = NavHostController(LocalContext.current), navController = NavHostController(LocalContext.current),
coroutineScope = backgroundScope, coroutineScope = backgroundScope,
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
) )

@ -63,6 +63,7 @@ import com.github.takahirom.roborazzi.captureRoboImage
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor 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.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
@ -113,6 +114,9 @@ class SnackbarInsetsScreenshotTests {
@Inject @Inject
lateinit var networkMonitor: NetworkMonitor lateinit var networkMonitor: NetworkMonitor
@Inject
lateinit var errorMonitor: ErrorMonitor
@Inject @Inject
lateinit var timeZoneMonitor: TimeZoneMonitor lateinit var timeZoneMonitor: TimeZoneMonitor
@ -254,6 +258,7 @@ class SnackbarInsetsScreenshotTests {
NiaTheme { NiaTheme {
val appState = rememberNiaAppState( val appState = rememberNiaAppState(
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
) )

@ -36,6 +36,7 @@ import com.github.takahirom.roborazzi.captureRoboImage
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor 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.data.util.TimeZoneMonitor
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
@ -86,6 +87,9 @@ class SnackbarScreenshotTests {
@Inject @Inject
lateinit var networkMonitor: NetworkMonitor lateinit var networkMonitor: NetworkMonitor
@Inject
lateinit var errorMonitor: ErrorMonitor
@Inject @Inject
lateinit var timeZoneMonitor: TimeZoneMonitor lateinit var timeZoneMonitor: TimeZoneMonitor
@ -203,6 +207,7 @@ class SnackbarScreenshotTests {
NiaTheme { NiaTheme {
val appState = rememberNiaAppState( val appState = rememberNiaAppState(
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
errorMonitor = errorMonitor,
userNewsResourceRepository = userNewsResourceRepository, userNewsResourceRepository = userNewsResourceRepository,
timeZoneMonitor = timeZoneMonitor, timeZoneMonitor = timeZoneMonitor,
) )

@ -0,0 +1,45 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.data.test
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
import com.google.samples.apps.nowinandroid.core.model.data.MessageData
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import javax.inject.Inject
class EmptyErrorMonitor @Inject constructor() : ErrorMonitor {
override fun addMessageByString(message: String): MessageData {
TODO("Not yet implemented")
}
override fun addMessageByData(message: MessageData) {
TODO("Not yet implemented")
}
override fun clearMessage(message: MessageData) {
TODO("Not yet implemented")
}
override fun clearAllMessages() {
TODO("Not yet implemented")
}
override val messages: Flow<List<MessageData?>>
get() = flowOf(emptyList())
}

@ -27,6 +27,7 @@ import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeRecent
import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeSearchContentsRepository import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeSearchContentsRepository
import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeTopicsRepository import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeTopicsRepository
import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository import com.google.samples.apps.nowinandroid.core.data.test.repository.FakeUserDataRepository
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor 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.data.util.TimeZoneMonitor
import dagger.Binds import dagger.Binds
@ -70,6 +71,11 @@ internal interface TestDataModule {
networkMonitor: AlwaysOnlineNetworkMonitor, networkMonitor: AlwaysOnlineNetworkMonitor,
): NetworkMonitor ): NetworkMonitor
@Binds
fun bindsErrorMonitor(
errorMonitor: EmptyErrorMonitor,
): ErrorMonitor
@Binds @Binds
fun binds(impl: DefaultZoneIdTimeZoneMonitor): TimeZoneMonitor fun binds(impl: DefaultZoneIdTimeZoneMonitor): TimeZoneMonitor
} }

@ -27,7 +27,9 @@ import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsR
import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository import com.google.samples.apps.nowinandroid.core.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.data.util.ConnectivityManagerNetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.ConnectivityManagerNetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
import com.google.samples.apps.nowinandroid.core.data.util.StateErrorMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneBroadcastMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneBroadcastMonitor
import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor import com.google.samples.apps.nowinandroid.core.data.util.TimeZoneMonitor
import dagger.Binds import dagger.Binds
@ -69,6 +71,11 @@ abstract class DataModule {
networkMonitor: ConnectivityManagerNetworkMonitor, networkMonitor: ConnectivityManagerNetworkMonitor,
): NetworkMonitor ): NetworkMonitor
@Binds
internal abstract fun bindsErrorMonitor(
errorMonitor: StateErrorMonitor,
): ErrorMonitor
@Binds @Binds
internal abstract fun binds(impl: TimeZoneBroadcastMonitor): TimeZoneMonitor internal abstract fun binds(impl: TimeZoneBroadcastMonitor): TimeZoneMonitor
} }

@ -0,0 +1,35 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.data.util
import com.google.samples.apps.nowinandroid.core.model.data.MessageData
import kotlinx.coroutines.flow.Flow
/**
* Interface for handling messages.
*/
interface ErrorMonitor {
fun addMessageByString(message: String): MessageData
fun addMessageByData(message: MessageData)
fun clearMessage(message: MessageData)
fun clearAllMessages()
val messages: Flow<List<MessageData?>>
}

@ -0,0 +1,67 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.data.util
import com.google.samples.apps.nowinandroid.core.model.data.MessageData
import com.google.samples.apps.nowinandroid.core.model.data.MessageType
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
/**
* Interface implementation for handling general errors.
*/
class StateErrorMonitor @Inject constructor() : ErrorMonitor {
/**
* List of [MessageData] to be shown
*/
override val messages = MutableStateFlow<List<MessageData>>(emptyList())
/**
* Creates a [MessageData] and adds it to the list.
* @param message: String value for message to add.
*/
override fun addMessageByString(message: String): MessageData {
val data = MessageData(type = MessageType.MESSAGE(message))
messages.update { it + data }
return data
}
/**
* Add a [MessageData] to the list.
* @param message: [MessageData] to add.
*/
override fun addMessageByData(message: MessageData) {
messages.update { it + message }
}
/**
* Removes the [MessageData] from the list.
*/
override fun clearMessage(message: MessageData) {
messages.update { list -> list.filterNot { it == message } }
}
/**
* Remove all from list, reset to empty list
*/
override fun clearAllMessages() {
messages.update { emptyList() }
}
}

@ -0,0 +1,147 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.google.samples.apps.nowinandroid.core.data.util.StateErrorMonitor
import com.google.samples.apps.nowinandroid.core.model.data.MessageData
import com.google.samples.apps.nowinandroid.core.model.data.MessageType
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
/**
* Unit tests for [StateErrorMonitor].
*/
class StateErrorMonitorTest {
// Subject under test.
private lateinit var state: StateErrorMonitor
private lateinit var messages: List<MessageData?>
private val testString = "Test Error Message"
private val testData = MessageData(MessageType.MESSAGE("Test Error Message 1"))
private val testData2 = MessageData(MessageType.MESSAGE("Test Error Message 2"))
@Before
fun setup() {
state = StateErrorMonitor()
messages = emptyList()
}
@Test
fun whenErrorIsNotAdded_NullIsPresent() = runTest(UnconfinedTestDispatcher()) {
backgroundScope.launch {
state.messages.collect {
messages = it
}
}
assertEquals(
emptyList(),
messages,
)
}
@Test
fun whenErrorIsAddedByString_ErrorMessageIsPresent() = runTest(UnconfinedTestDispatcher()) {
backgroundScope.launch {
state.messages.collect {
messages = it
}
}
val expect = state.addMessageByString(testString)
assertEquals(
expect,
messages.firstOrNull(),
)
}
@Test
fun whenErrorIsAddedByData_ErrorMessageIsPresent() = runTest(UnconfinedTestDispatcher()) {
backgroundScope.launch {
state.messages.collect {
messages = it
}
}
state.addMessageByData(testData)
assertEquals(
testData,
messages.firstOrNull(),
)
}
@Test
fun whenErrorsAreAdded_FirstErrorMessageIsPresent() =
runTest(UnconfinedTestDispatcher()) {
state.addMessageByData(testData)
state.addMessageByString(testString)
backgroundScope.launch {
state.messages.collect {
messages = it
}
}
assertEquals(
testData,
messages.firstOrNull(),
)
}
@Test
fun whenErrorIsCleared_ErrorMessageIsNotPresent() =
runTest(UnconfinedTestDispatcher()) {
backgroundScope.launch {
state.messages.collect {
messages = it
}
}
state.addMessageByData(testData)
state.clearMessage(testData)
assertEquals(
emptyList(),
messages,
)
}
@Test
fun whenErrorsAreCleared_NextErrorMessageIsPresent() =
runTest(UnconfinedTestDispatcher()) {
backgroundScope.launch {
state.messages.collect {
messages = it
}
}
state.addMessageByData(testData)
state.addMessageByData(testData2)
state.clearMessage(testData)
assertEquals(
testData2,
messages.firstOrNull(),
)
}
}

@ -0,0 +1,37 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.model.data
/**
* [MessageData]
* Class to hold messages type objects with actions
*/
data class MessageData(
val type: MessageType,
val label: String? = null,
val onConfirm: (() -> Unit)? = null,
val onDelay: (() -> Unit)? = null,
)
/**
* Specified Errors
*/
sealed class MessageType {
data object OFFLINE : MessageType()
data class MESSAGE(val value: String) : MessageType()
data object UNKNOWN : MessageType()
}

@ -0,0 +1,71 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.testing.util
import com.google.samples.apps.nowinandroid.core.data.util.ErrorMonitor
import com.google.samples.apps.nowinandroid.core.model.data.MessageData
import com.google.samples.apps.nowinandroid.core.model.data.MessageType.MESSAGE
import com.google.samples.apps.nowinandroid.core.model.data.MessageType.OFFLINE
import com.google.samples.apps.nowinandroid.core.model.data.MessageType.UNKNOWN
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class TestErrorMonitor : ErrorMonitor {
private val _messages = MutableStateFlow<List<MessageData>>(emptyList())
override fun addMessageByString(message: String): MessageData {
TODO("Not yet implemented")
}
override fun addMessageByData(message: MessageData) {
TODO("Not yet implemented")
}
override fun clearMessage(message: MessageData) {
TODO("Not yet implemented")
}
override fun clearAllMessages() {
_messages.value = emptyList()
}
override val messages: Flow<List<MessageData?>>
get() = _messages
/**
* Test-only API to add message types
*/
fun setOfflineMessage() {
_messages.value = listOf(OFFLINE_MESSAGE)
}
fun addMessage() {
_messages.value.plus(MESSAGE_MESSAGE)
}
fun addUnknownMessage() {
_messages.value.plus(UNKNOWN_MESSAGE)
}
companion object {
val OFFLINE_MESSAGE = MessageData(OFFLINE)
val MESSAGE_MESSAGE = MessageData(MESSAGE("Message"), "Title", {}, {})
val UNKNOWN_MESSAGE = MessageData(UNKNOWN)
}
}
Loading…
Cancel
Save