fix: network connectivity related issues

This commit is contained in:
2026-09-17 14:11:34 +03:00
parent b7183805c2
commit 9e94a67a85
7 changed files with 158 additions and 34 deletions
+2 -1
View File
@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2025 Gleb Zaharov. License: GPLv3 (see LICENSE). --> <!-- Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE). -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:name=".app.UNNApp" android:name=".app.UNNApp"
@@ -38,7 +38,6 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import ru.sweetbread.unn.R import ru.sweetbread.unn.R
import ru.sweetbread.unn.core.AuthRepository import ru.sweetbread.unn.core.AuthRepository
import ru.sweetbread.unn.ui.theme.UNNTheme import ru.sweetbread.unn.ui.theme.UNNTheme
@@ -51,12 +50,10 @@ class LoginActivity : ComponentActivity() {
val authRepo = (application as UNNApp).container.authRepo val authRepo = (application as UNNApp).container.authRepo
if (authRepo.hasSavedCredentials()) {
runBlocking { start<MainActivity>()
if (authRepo.restoreSession()) { finish()
start<MainActivity>() return
finish()
}
} }
setContent { setContent {
@@ -1,5 +1,10 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.app package ru.sweetbread.unn.app
import android.content.Intent
import android.net.ConnectivityManager
import android.net.Network
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@@ -50,23 +55,33 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.sweetbread.unn.R import ru.sweetbread.unn.R
import ru.sweetbread.unn.core.network.LoginResult
import ru.sweetbread.unn.core.network.portalURL import ru.sweetbread.unn.core.network.portalURL
import ru.sweetbread.unn.feature.About
import ru.sweetbread.unn.feature.news.Blogposts import ru.sweetbread.unn.feature.news.Blogposts
import ru.sweetbread.unn.feature.schedule.Schedule import ru.sweetbread.unn.feature.schedule.Schedule
import ru.sweetbread.unn.feature.About
import ru.sweetbread.unn.ui.theme.UNNTheme import ru.sweetbread.unn.ui.theme.UNNTheme
import splitties.resources.appStr import splitties.resources.appStr
import splitties.toast.toast import splitties.toast.toast
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private lateinit var connectivityManager: ConnectivityManager
private var restoreSessionJob: Job? = null
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
restoreSavedSession()
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -74,6 +89,15 @@ class MainActivity : ComponentActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
val authRepo = (application as UNNApp).container.authRepo
lifecycleScope.launch {
authRepo.authenticationFailures.collect {
openLogin()
}
}
connectivityManager = getSystemService(ConnectivityManager::class.java)
restoreSavedSession()
setContent { setContent {
UNNTheme { UNNTheme {
Surface( Surface(
@@ -193,6 +217,44 @@ class MainActivity : ComponentActivity() {
} }
} }
override fun onStart() {
super.onStart()
connectivityManager.registerDefaultNetworkCallback(networkCallback)
}
override fun onStop() {
connectivityManager.unregisterNetworkCallback(networkCallback)
super.onStop()
}
private fun restoreSavedSession() {
val container = (application as UNNApp).container
if (
container.sessionManager.sessionId != null ||
restoreSessionJob?.isActive == true
) {
return
}
restoreSessionJob = lifecycleScope.launch {
if (container.authRepo.restoreSession() == LoginResult.InvalidCredentials) {
openLogin()
}
}
}
private fun openLogin() {
if (isFinishing) return
startActivity(
Intent(this, LoginActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
)
finish()
}
@Composable @Composable
fun DrawerContent(modifier: Modifier = Modifier, navController: NavController) { fun DrawerContent(modifier: Modifier = Modifier, navController: NavController) {
val me = (application as UNNApp).container.sessionManager.user.collectAsState().value val me = (application as UNNApp).container.sessionManager.user.collectAsState().value
@@ -8,7 +8,9 @@ import ru.sweetbread.unn.core.AuthRepository
import ru.sweetbread.unn.core.CredentialsStore import ru.sweetbread.unn.core.CredentialsStore
import ru.sweetbread.unn.core.SessionManager import ru.sweetbread.unn.core.SessionManager
import ru.sweetbread.unn.core.db.AppDatabase import ru.sweetbread.unn.core.db.AppDatabase
import ru.sweetbread.unn.core.network.LoginResult
import ru.sweetbread.unn.core.network.PortalApi import ru.sweetbread.unn.core.network.PortalApi
import ru.sweetbread.unn.core.network.SessionRefreshHandler
import ru.sweetbread.unn.core.network.createHttpClient import ru.sweetbread.unn.core.network.createHttpClient
import ru.sweetbread.unn.feature.news.NewsRepository import ru.sweetbread.unn.feature.news.NewsRepository
import ru.sweetbread.unn.feature.profile.UserRepository import ru.sweetbread.unn.feature.profile.UserRepository
@@ -33,8 +35,9 @@ class UNNApp : Application() {
class AppContainer(context: Context) { class AppContainer(context: Context) {
val credentialsStore = CredentialsStore(context) val credentialsStore = CredentialsStore(context)
val sessionManager = SessionManager() val sessionManager = SessionManager()
val sessionRefreshHandler = SessionRefreshHandler()
val httpClient = createHttpClient() val httpClient = createHttpClient(sessionRefreshHandler)
val portalApi = PortalApi( val portalApi = PortalApi(
client = httpClient, client = httpClient,
@@ -43,6 +46,12 @@ class AppContainer(context: Context) {
val authRepo = AuthRepository(credentialsStore, portalApi) val authRepo = AuthRepository(credentialsStore, portalApi)
init {
sessionRefreshHandler.setRefreshAction {
authRepo.restoreSession() == LoginResult.Success
}
}
val database = roomDb<AppDatabase>(name = "cache") { val database = roomDb<AppDatabase>(name = "cache") {
fallbackToDestructiveMigration(dropAllTables = true) fallbackToDestructiveMigration(dropAllTables = true)
} }
@@ -2,8 +2,10 @@
package ru.sweetbread.unn.core package ru.sweetbread.unn.core
import android.util.Log import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.io.IOException import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import ru.sweetbread.unn.core.network.LoginResult import ru.sweetbread.unn.core.network.LoginResult
import ru.sweetbread.unn.core.network.PortalApi import ru.sweetbread.unn.core.network.PortalApi
@@ -11,6 +13,10 @@ class AuthRepository(
private val credentialsStore: CredentialsStore, private val credentialsStore: CredentialsStore,
private val api: PortalApi private val api: PortalApi
) { ) {
private val restoreMutex = Mutex()
private val _authenticationFailures = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val authenticationFailures = _authenticationFailures.asSharedFlow()
suspend fun login( suspend fun login(
login: String, login: String,
password: String password: String
@@ -26,21 +32,22 @@ class AuthRepository(
return res return res
} }
suspend fun restoreSession(): Boolean { fun hasSavedCredentials(): Boolean = credentialsStore.get() != null
val credentials =
credentialsStore.get()
?: return false
return try { suspend fun restoreSession(): LoginResult? = restoreMutex.withLock {
val res = api.login( val credentials = credentialsStore.get() ?: return@withLock null
credentials.login,
credentials.password
)
res != LoginResult.InvalidCredentials val result = api.login(
} catch (e: IOException) { credentials.login,
Log.e("AuthRepo", "restoreSession error", e) credentials.password
false )
if (result == LoginResult.InvalidCredentials) {
credentialsStore.clear()
api.session.clear()
_authenticationFailures.emit(Unit)
} }
result
} }
} }
@@ -6,14 +6,37 @@ import android.util.Log
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.android.Android import io.ktor.client.engine.android.Android
import io.ktor.client.plugins.HttpRequestRetry import io.ktor.client.plugins.HttpRequestRetry
import io.ktor.client.plugins.HttpSend
import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.cache.HttpCache import io.ktor.client.plugins.cache.HttpCache
import io.ktor.client.plugins.cookies.HttpCookies import io.ktor.client.plugins.cookies.HttpCookies
import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging import io.ktor.client.plugins.logging.Logging
import io.ktor.client.plugins.plugin
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.http.HttpStatusCode
import io.ktor.util.AttributeKey
fun createHttpClient() = HttpClient(Android) { private val SkipSessionRefresh = AttributeKey<Unit>("SkipSessionRefresh")
private val SessionRefreshAttempted = AttributeKey<Unit>("SessionRefreshAttempted")
fun HttpRequestBuilder.skipSessionRefresh() {
attributes.put(SkipSessionRefresh, Unit)
}
class SessionRefreshHandler {
@Volatile
private var refresh: suspend () -> Boolean = { false }
fun setRefreshAction(action: suspend () -> Boolean) {
refresh = action
}
suspend fun refreshSession(): Boolean = refresh()
}
fun createHttpClient(sessionRefreshHandler: SessionRefreshHandler) = HttpClient(Android) {
install(HttpCache) install(HttpCache)
install(HttpCookies) install(HttpCookies)
install(Logging) { install(Logging) {
@@ -34,4 +57,24 @@ fun createHttpClient() = HttpClient(Android) {
request.headers.append("x-retry-count", retryCount.toString()) request.headers.append("x-retry-count", retryCount.toString())
} }
} }
}.apply {
plugin(HttpSend).intercept { request ->
val call = execute(request)
if (
call.response.status == HttpStatusCode.Forbidden &&
request.attributes.getOrNull(SkipSessionRefresh) == null &&
request.attributes.getOrNull(SessionRefreshAttempted) == null
) {
request.attributes.put(SessionRefreshAttempted, Unit)
if (sessionRefreshHandler.refreshSession()) {
execute(request)
} else {
call
}
} else {
call
}
}
} }
@@ -50,7 +50,10 @@ class Auth (
append("USER_LOGIN", login) append("USER_LOGIN", login)
append("USER_PASSWORD", password) append("USER_PASSWORD", password)
} }
) { retry { noRetry() } } ) {
retry { noRetry() }
skipSessionRefresh()
}
if (r.status.value == 302) if (r.status.value == 302)
return """PHPSESSID=([\w\d]+)""".toRegex().find(r.headers["Set-Cookie"]!!)!!.groupValues[1] return """PHPSESSID=([\w\d]+)""".toRegex().find(r.headers["Set-Cookie"]!!)!!.groupValues[1]
@@ -182,7 +185,9 @@ class Bitrix(
val client: HttpClient, val client: HttpClient,
) { ) {
suspend fun getCurrentUser(): User { suspend fun getCurrentUser(): User {
val r = client.get("$vuzapiURL/profile/current") val r = client.get("$vuzapiURL/profile/current") {
skipSessionRefresh()
}
return User.fromProfile(JSONObject(r.bodyAsText())) return User.fromProfile(JSONObject(r.bodyAsText()))
} }