diff --git a/core/src/main/java/hu/bbara/purefin/core/data/NetworkMonitor.kt b/core/src/main/java/hu/bbara/purefin/core/data/NetworkMonitor.kt index ffa3e9a2..97aa8b53 100644 --- a/core/src/main/java/hu/bbara/purefin/core/data/NetworkMonitor.kt +++ b/core/src/main/java/hu/bbara/purefin/core/data/NetworkMonitor.kt @@ -4,4 +4,8 @@ import kotlinx.coroutines.flow.Flow interface NetworkMonitor { val isOnline: Flow + + fun reportRequestSucceeded() + + fun reportRequestFailed(error: Throwable) } diff --git a/data/src/main/java/hu/bbara/purefin/data/jellyfin/ConnectivityNetworkMonitor.kt b/data/src/main/java/hu/bbara/purefin/data/jellyfin/ConnectivityNetworkMonitor.kt index 5a954123..a4246676 100644 --- a/data/src/main/java/hu/bbara/purefin/data/jellyfin/ConnectivityNetworkMonitor.kt +++ b/data/src/main/java/hu/bbara/purefin/data/jellyfin/ConnectivityNetworkMonitor.kt @@ -7,28 +7,54 @@ import android.net.NetworkCapabilities import android.net.NetworkRequest import dagger.hilt.android.qualifiers.ApplicationContext import hu.bbara.purefin.core.data.NetworkMonitor +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.stateIn +import java.net.ConnectException +import java.net.NoRouteToHostException +import java.net.SocketTimeoutException +import java.net.UnknownHostException import javax.inject.Inject import javax.inject.Singleton @Singleton class ConnectivityNetworkMonitor @Inject constructor( - @ApplicationContext private val context: Context, + @param:ApplicationContext private val context: Context, ) : NetworkMonitor { - override val isOnline: Flow = callbackFlow { - val connectivityManager = context.getSystemService(ConnectivityManager::class.java) + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val connectivityManager = context.getSystemService(ConnectivityManager::class.java) + private val isServerReachable = MutableStateFlow(true) + + private val isDeviceOnline: Flow = callbackFlow { + var wasDeviceOnline = connectivityManager.isCurrentlyConnected() val callback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - trySend(true) + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + val isCurrentlyConnected = connectivityManager.isCurrentlyConnected() + if (isCurrentlyConnected && !wasDeviceOnline) { + isServerReachable.value = true + } + wasDeviceOnline = isCurrentlyConnected + trySend(isCurrentlyConnected) } override fun onLost(network: Network) { - trySend(connectivityManager.isCurrentlyConnected()) + val isCurrentlyConnected = connectivityManager.isCurrentlyConnected() + wasDeviceOnline = isCurrentlyConnected + trySend(isCurrentlyConnected) } } @@ -36,14 +62,51 @@ class ConnectivityNetworkMonitor @Inject constructor( .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .build() - trySend(connectivityManager.isCurrentlyConnected()) + trySend(wasDeviceOnline) connectivityManager.registerNetworkCallback(request, callback) awaitClose { connectivityManager.unregisterNetworkCallback(callback) } }.distinctUntilChanged() + override val isOnline: StateFlow = combine( + isDeviceOnline, + isServerReachable + ) { isDeviceOnline, isServerReachable -> + isDeviceOnline && isServerReachable + }.distinctUntilChanged() + .stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = connectivityManager.isCurrentlyConnected() && isServerReachable.value + ) + + override fun reportRequestSucceeded() { + isServerReachable.value = true + } + + override fun reportRequestFailed(error: Throwable) { + if (error.isConnectivityFailure()) { + isServerReachable.value = false + } + } + private fun ConnectivityManager.isCurrentlyConnected(): Boolean { val network = activeNetwork ?: return false val caps = getNetworkCapabilities(network) ?: return false - return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + } + + private fun Throwable.isConnectivityFailure(): Boolean { + var current: Throwable? = this + while (current != null) { + when (current) { + is UnknownHostException, + is ConnectException, + is NoRouteToHostException, + is SocketTimeoutException -> return true + } + current = current.cause + } + return false } } diff --git a/data/src/main/java/hu/bbara/purefin/data/jellyfin/client/JellyfinApiClient.kt b/data/src/main/java/hu/bbara/purefin/data/jellyfin/client/JellyfinApiClient.kt index 5fc68a43..83a6a819 100644 --- a/data/src/main/java/hu/bbara/purefin/data/jellyfin/client/JellyfinApiClient.kt +++ b/data/src/main/java/hu/bbara/purefin/data/jellyfin/client/JellyfinApiClient.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.SystemClock import dagger.hilt.android.qualifiers.ApplicationContext import hu.bbara.purefin.core.data.JellyfinServerCandidate +import hu.bbara.purefin.core.data.NetworkMonitor import hu.bbara.purefin.core.data.PlaybackMethod import hu.bbara.purefin.core.data.PlaybackReportContext import hu.bbara.purefin.core.data.QuickConnectSession @@ -64,8 +65,9 @@ import javax.inject.Singleton @Singleton class JellyfinApiClient @Inject constructor( - @ApplicationContext private val applicationContext: Context, + @param:ApplicationContext private val applicationContext: Context, private val userSessionRepository: UserSessionRepository, + private val networkMonitor: NetworkMonitor, ) { private val jellyfin = createJellyfin { context = applicationContext @@ -583,12 +585,14 @@ class JellyfinApiClient @Inject constructor( val startedAt = SystemClock.elapsedRealtime() return try { val result = block() + networkMonitor.reportRequestSucceeded() val elapsedMs = SystemClock.elapsedRealtime() - startedAt Timber.tag(TAG).d("$functionName took ${elapsedMs}ms") result } catch (error: CancellationException) { throw error } catch (error: Exception) { + networkMonitor.reportRequestFailed(error) val elapsedMs = SystemClock.elapsedRealtime() - startedAt Timber.tag(TAG).e(error, "$functionName failed after ${elapsedMs}ms") throw error