mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-08 12:42:44 -05:00
add network monitor and error message popups
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
|
||||
@@ -2,11 +2,14 @@ package com.owenlejeune.tvtime
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.owenlejeune.tvtime.extensions.launchActivity
|
||||
import com.owenlejeune.tvtime.preferences.AppPreferences
|
||||
import com.owenlejeune.tvtime.ui.viewmodel.ConfigurationViewModel
|
||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.getViewModel
|
||||
|
||||
@@ -17,15 +20,17 @@ class AppRoutingActivity: AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
lifecycleScope.launchWhenCreated {
|
||||
val configurationViewModel = getViewModel<ConfigurationViewModel>()
|
||||
configurationViewModel.getConfigurations()
|
||||
TmdbUtils.setup(configurationViewModel)
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
val configurationViewModel = getViewModel<ConfigurationViewModel>()
|
||||
configurationViewModel.getConfigurations()
|
||||
TmdbUtils.setup(configurationViewModel)
|
||||
|
||||
if (preferences.firstLaunchTesting || preferences.firstLaunch) {
|
||||
launchActivity(OnboardingActivity::class.java)
|
||||
} else {
|
||||
launchActivity(MainActivity::class.java)
|
||||
if (preferences.firstLaunchTesting || preferences.firstLaunch) {
|
||||
launchActivity(OnboardingActivity::class.java)
|
||||
} else {
|
||||
launchActivity(MainActivity::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
package com.owenlejeune.tvtime
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.kieronquinn.monetcompat.app.MonetCompatActivity
|
||||
import com.owenlejeune.tvtime.extensions.rememberWindowSizeClass
|
||||
import com.owenlejeune.tvtime.ui.navigation.AppNavigationHost
|
||||
import com.owenlejeune.tvtime.ui.navigation.HomeScreenNavItem
|
||||
import com.owenlejeune.tvtime.ui.theme.TVTimeTheme
|
||||
import com.owenlejeune.tvtime.ui.viewmodel.ConfigurationViewModel
|
||||
import com.owenlejeune.tvtime.utils.KeyboardManager
|
||||
import com.owenlejeune.tvtime.utils.NetworkStatus
|
||||
import com.owenlejeune.tvtime.utils.SessionManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -22,6 +37,7 @@ import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity : MonetCompatActivity() {
|
||||
|
||||
@SuppressLint("AutoboxingStateValueProperty")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -31,19 +47,53 @@ class MainActivity : MonetCompatActivity() {
|
||||
|
||||
val mainNavStartRoute = HomeScreenNavItem.SortedItems[0].route
|
||||
|
||||
lifecycleScope.launchWhenCreated {
|
||||
monet.awaitMonetReady()
|
||||
setContent {
|
||||
AppKeyboardFocusManager()
|
||||
TVTimeTheme(monetCompat = monet) {
|
||||
val windowSize = rememberWindowSizeClass()
|
||||
val appNavController = rememberNavController()
|
||||
Box {
|
||||
AppNavigationHost(
|
||||
appNavController = appNavController,
|
||||
mainNavStartRoute = mainNavStartRoute,
|
||||
windowSize = windowSize
|
||||
)
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
monet.awaitMonetReady()
|
||||
setContent {
|
||||
AppKeyboardFocusManager()
|
||||
TVTimeTheme(monetCompat = monet) {
|
||||
val snackbarHostState = SnackbarHostState()
|
||||
|
||||
val configViewModel = viewModel<ConfigurationViewModel>()
|
||||
val networkStatus = configViewModel.networkStatus.collectAsState()
|
||||
if (networkStatus.value == NetworkStatus.Disconnected) {
|
||||
LaunchedEffect(networkStatus) {
|
||||
snackbarHostState.showSnackbar(getString(R.string.network_disconnected_message))
|
||||
}
|
||||
}
|
||||
|
||||
val lastResponseCode = remember { configViewModel.lastResponseCode }
|
||||
val code = lastResponseCode.value
|
||||
LaunchedEffect(code) {
|
||||
when (code) {
|
||||
429 -> Toast.makeText(
|
||||
this@MainActivity,
|
||||
getString(R.string.network_api_rate_limit_reached),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
|
||||
in 400..599 -> Toast.makeText(
|
||||
this@MainActivity,
|
||||
getString(R.string.network_error_occurred, code),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||
) {
|
||||
val windowSize = rememberWindowSizeClass()
|
||||
val appNavController = rememberNavController()
|
||||
Box(modifier = Modifier.padding(it)) {
|
||||
AppNavigationHost(
|
||||
appNavController = appNavController,
|
||||
mainNavStartRoute = mainNavStartRoute,
|
||||
windowSize = windowSize
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.os.BuildCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.google.accompanist.pager.*
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.kieronquinn.monetcompat.app.MonetCompatActivity
|
||||
@@ -58,11 +60,13 @@ class OnboardingActivity: MonetCompatActivity() {
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launchWhenCreated {
|
||||
monet.awaitMonetReady()
|
||||
setContent {
|
||||
TVTimeTheme(monetCompat = monet) {
|
||||
OnboardingUi()
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
monet.awaitMonetReady()
|
||||
setContent {
|
||||
TVTimeTheme(monetCompat = monet) {
|
||||
OnboardingUi()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import com.owenlejeune.tvtime.di.modules.networkModule
|
||||
import com.owenlejeune.tvtime.di.modules.preferencesModule
|
||||
import com.owenlejeune.tvtime.di.modules.viewModelModule
|
||||
import com.owenlejeune.tvtime.preferences.AppPreferences
|
||||
import dev.kdrag0n.monet.factory.ColorSchemeFactory
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.owenlejeune.tvtime.api.tmdb
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import com.owenlejeune.tvtime.BuildConfig
|
||||
import com.owenlejeune.tvtime.api.Client
|
||||
import com.owenlejeune.tvtime.api.QueryParam
|
||||
@@ -17,6 +19,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v4.AuthenticationV4Api
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v4.ListV4Api
|
||||
import com.owenlejeune.tvtime.extensions.addQueryParams
|
||||
import com.owenlejeune.tvtime.preferences.AppPreferences
|
||||
import com.owenlejeune.tvtime.ui.viewmodel.ConfigurationViewModel
|
||||
import com.owenlejeune.tvtime.utils.SessionManager
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
@@ -33,7 +36,6 @@ class TmdbClient: KoinComponent {
|
||||
|
||||
private val client: Client by inject { parametersOf(V_3_BASE_URL) }
|
||||
private val clientV4: Client by inject { parametersOf(V_4_BASE_URL) }
|
||||
private val preferences: AppPreferences by inject()
|
||||
|
||||
init {
|
||||
client.addInterceptor(TmdbInterceptor())
|
||||
@@ -84,6 +86,21 @@ class TmdbClient: KoinComponent {
|
||||
return clientV4.create(ListV4Api::class.java)
|
||||
}
|
||||
|
||||
@SuppressLint("AutoboxingStateValueProperty")
|
||||
private fun handleResponseCode(response: Response) {
|
||||
when (response.code) {
|
||||
429 -> {
|
||||
ConfigurationViewModel.lastResponseCode.value = 429
|
||||
}
|
||||
in 400..499 -> {
|
||||
ConfigurationViewModel.lastResponseCode.value = response.code
|
||||
}
|
||||
in 500..599 -> {
|
||||
ConfigurationViewModel.lastResponseCode.value = response.code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class TmdbInterceptor: Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val apiParam = QueryParam("api_key", BuildConfig.TMDB_ApiKey)
|
||||
@@ -106,7 +123,11 @@ class TmdbClient: KoinComponent {
|
||||
val requestBuilder = chain.request().newBuilder().url(builder.build())
|
||||
|
||||
val request = requestBuilder.build()
|
||||
return chain.proceed(request)
|
||||
val response = chain.proceed(request)
|
||||
|
||||
handleResponseCode(response)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
private fun sessionIdParam(urlSegments: List<String>): QueryParam? {
|
||||
@@ -154,7 +175,11 @@ class TmdbClient: KoinComponent {
|
||||
}
|
||||
}
|
||||
|
||||
return chain.proceed(builder.build())
|
||||
val response = chain.proceed(builder.build())
|
||||
|
||||
handleResponseCode(response)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
private fun shouldIncludeLanguageParam(urlSegments: List<String>): Boolean {
|
||||
|
||||
@@ -34,6 +34,8 @@ import com.owenlejeune.tvtime.api.tmdb.api.v4.model.ListItem
|
||||
import com.owenlejeune.tvtime.preferences.AppPreferences
|
||||
import com.owenlejeune.tvtime.ui.viewmodel.ConfigurationViewModel
|
||||
import com.owenlejeune.tvtime.ui.viewmodel.SettingsViewModel
|
||||
import com.owenlejeune.tvtime.utils.NetworkConnectivityService
|
||||
import com.owenlejeune.tvtime.utils.NetworkConnectivityServiceImpl
|
||||
import com.owenlejeune.tvtime.utils.ResourceUtils
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
@@ -97,9 +99,10 @@ val preferencesModule = module {
|
||||
|
||||
val appModule = module {
|
||||
factory { ResourceUtils(get()) }
|
||||
|
||||
single<NetworkConnectivityService> { NetworkConnectivityServiceImpl() }
|
||||
}
|
||||
|
||||
val viewModelModule = module {
|
||||
viewModel { ConfigurationViewModel() }
|
||||
viewModel { SettingsViewModel() }
|
||||
}
|
||||
@@ -20,16 +20,6 @@ fun <T: Any> LazyGridScope.lazyPagingItems(
|
||||
}
|
||||
}
|
||||
|
||||
fun <T: Any> LazyGridScope.listItems(
|
||||
items: List<T>,
|
||||
key: (T?) -> Any,
|
||||
itemContent: @Composable (value: T) -> Unit
|
||||
) {
|
||||
items(items.size) { index ->
|
||||
itemContent(items[index])
|
||||
}
|
||||
}
|
||||
|
||||
fun LazyGridScope.header(
|
||||
content: @Composable LazyGridItemScope.() -> Unit
|
||||
) {
|
||||
@@ -42,21 +32,11 @@ fun LazyGridScope.header(
|
||||
}
|
||||
|
||||
fun <T: Any> LazyListScope.listItems(
|
||||
items: Collection<T>,
|
||||
key: (T?) -> Any,
|
||||
items: List<T>,
|
||||
key: ((T) -> Any)? = null,
|
||||
itemContent: @Composable (value: T) -> Unit
|
||||
) {
|
||||
items(items.size) { index ->
|
||||
itemContent(items.elementAt(index))
|
||||
}
|
||||
}
|
||||
|
||||
fun <T: Any?> LazyListScope.listItems(
|
||||
items: List<T?>,
|
||||
key: (T?) -> Any,
|
||||
itemContent: @Composable (value: T?) -> Unit
|
||||
) {
|
||||
items(items.size, key = { key(items[it]) }) { index ->
|
||||
items(items.size, key = key?.let { { key(items[it]) } }) { index ->
|
||||
itemContent(items[index])
|
||||
}
|
||||
}
|
||||
@@ -66,7 +46,7 @@ fun <T: Any> LazyListScope.lazyPagingItems(
|
||||
key: ((index: Int) -> Any)? = null,
|
||||
itemContent: @Composable LazyItemScope.(value: T?) -> Unit
|
||||
) {
|
||||
items(lazyPagingItems.itemCount) { index ->
|
||||
items(lazyPagingItems.itemCount, key = key) { index ->
|
||||
itemContent(lazyPagingItems[index])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
package com.owenlejeune.tvtime.ui.viewmodel
|
||||
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.ConfigurationService
|
||||
import com.owenlejeune.tvtime.utils.NetworkConnectivityService
|
||||
import com.owenlejeune.tvtime.utils.NetworkStatus
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class ConfigurationViewModel: ViewModel(), KoinComponent {
|
||||
|
||||
private val service: ConfigurationService by inject()
|
||||
private val networkConnectivityService: NetworkConnectivityService by inject()
|
||||
|
||||
companion object Storage {
|
||||
val lastResponseCode = mutableIntStateOf(0)
|
||||
}
|
||||
|
||||
val detailsConfiguration = service.detailsConfiguration
|
||||
val countriesConfiguration = service.countriesConfiguration
|
||||
@@ -16,6 +28,14 @@ class ConfigurationViewModel: ViewModel(), KoinComponent {
|
||||
val primaryTranslationsConfiguration = service.primaryTranslationsConfiguration
|
||||
val timezonesConfiguration = service.timezonesConfiguration
|
||||
|
||||
val lastResponseCode = Storage.lastResponseCode
|
||||
|
||||
val networkStatus: StateFlow<NetworkStatus> = networkConnectivityService.networkStatus.stateIn(
|
||||
initialValue = NetworkStatus.Unknown,
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000)
|
||||
)
|
||||
|
||||
suspend fun getConfigurations() {
|
||||
service.getDetailsConfiguration()
|
||||
service.getCountriesConfiguration()
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.owenlejeune.tvtime.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.ConnectivityManager.NetworkCallback
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
sealed class NetworkStatus {
|
||||
object Unknown: NetworkStatus()
|
||||
object Connected: NetworkStatus()
|
||||
object Disconnected: NetworkStatus()
|
||||
}
|
||||
|
||||
interface NetworkConnectivityService {
|
||||
val networkStatus: Flow<NetworkStatus>
|
||||
}
|
||||
|
||||
class NetworkConnectivityServiceImpl: NetworkConnectivityService, KoinComponent {
|
||||
|
||||
private val context: Context by inject()
|
||||
|
||||
private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
override val networkStatus: Flow<NetworkStatus> = callbackFlow {
|
||||
val connectivityCallback = object : NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
trySend(NetworkStatus.Connected)
|
||||
}
|
||||
override fun onUnavailable() {
|
||||
trySend(NetworkStatus.Disconnected)
|
||||
}
|
||||
override fun onLost(network: Network) {
|
||||
trySend(NetworkStatus.Disconnected)
|
||||
}
|
||||
}
|
||||
|
||||
val request = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
.build()
|
||||
|
||||
connectivityManager.registerNetworkCallback(request, connectivityCallback)
|
||||
|
||||
awaitClose {
|
||||
connectivityManager.unregisterNetworkCallback(connectivityCallback)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
||||
}
|
||||
@@ -241,4 +241,7 @@
|
||||
<string name="latest_season_title">Latest Season</string>
|
||||
<string name="season_ep_count">%1$d | %2$d Episodes</string>
|
||||
<string name="see_all_seasons_text">See all seasons</string>
|
||||
<string name="network_disconnected_message">Network Disconnected</string>
|
||||
<string name="network_api_rate_limit_reached">API Rate limit reached</string>
|
||||
<string name="network_error_occurred">%1$d: An error occurred</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user