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"?>
<!-- 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"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
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.unit.dp
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import ru.sweetbread.unn.R
import ru.sweetbread.unn.core.AuthRepository
import ru.sweetbread.unn.ui.theme.UNNTheme
@@ -51,12 +50,10 @@ class LoginActivity : ComponentActivity() {
val authRepo = (application as UNNApp).container.authRepo
runBlocking {
if (authRepo.restoreSession()) {
start<MainActivity>()
finish()
}
if (authRepo.hasSavedCredentials()) {
start<MainActivity>()
finish()
return
}
setContent {
@@ -1,5 +1,10 @@
// Copyright (c) 2026 Gleb Zaharov. License: GPLv3 (see LICENSE).
package ru.sweetbread.unn.app
import android.content.Intent
import android.net.ConnectivityManager
import android.net.Network
import android.os.Bundle
import androidx.activity.ComponentActivity
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.unit.dp
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import coil3.compose.AsyncImage
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import ru.sweetbread.unn.R
import ru.sweetbread.unn.core.network.LoginResult
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.schedule.Schedule
import ru.sweetbread.unn.feature.About
import ru.sweetbread.unn.ui.theme.UNNTheme
import splitties.resources.appStr
import splitties.toast.toast
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)
override fun onCreate(savedInstanceState: Bundle?) {
@@ -74,6 +89,15 @@ class MainActivity : ComponentActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false)
val authRepo = (application as UNNApp).container.authRepo
lifecycleScope.launch {
authRepo.authenticationFailures.collect {
openLogin()
}
}
connectivityManager = getSystemService(ConnectivityManager::class.java)
restoreSavedSession()
setContent {
UNNTheme {
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
fun DrawerContent(modifier: Modifier = Modifier, navController: NavController) {
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.SessionManager
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.SessionRefreshHandler
import ru.sweetbread.unn.core.network.createHttpClient
import ru.sweetbread.unn.feature.news.NewsRepository
import ru.sweetbread.unn.feature.profile.UserRepository
@@ -33,8 +35,9 @@ class UNNApp : Application() {
class AppContainer(context: Context) {
val credentialsStore = CredentialsStore(context)
val sessionManager = SessionManager()
val sessionRefreshHandler = SessionRefreshHandler()
val httpClient = createHttpClient()
val httpClient = createHttpClient(sessionRefreshHandler)
val portalApi = PortalApi(
client = httpClient,
@@ -43,6 +46,12 @@ class AppContainer(context: Context) {
val authRepo = AuthRepository(credentialsStore, portalApi)
init {
sessionRefreshHandler.setRefreshAction {
authRepo.restoreSession() == LoginResult.Success
}
}
val database = roomDb<AppDatabase>(name = "cache") {
fallbackToDestructiveMigration(dropAllTables = true)
}
@@ -2,8 +2,10 @@
package ru.sweetbread.unn.core
import android.util.Log
import kotlinx.io.IOException
import kotlinx.coroutines.flow.MutableSharedFlow
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.PortalApi
@@ -11,6 +13,10 @@ class AuthRepository(
private val credentialsStore: CredentialsStore,
private val api: PortalApi
) {
private val restoreMutex = Mutex()
private val _authenticationFailures = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val authenticationFailures = _authenticationFailures.asSharedFlow()
suspend fun login(
login: String,
password: String
@@ -26,21 +32,22 @@ class AuthRepository(
return res
}
suspend fun restoreSession(): Boolean {
val credentials =
credentialsStore.get()
?: return false
fun hasSavedCredentials(): Boolean = credentialsStore.get() != null
return try {
val res = api.login(
credentials.login,
credentials.password
)
suspend fun restoreSession(): LoginResult? = restoreMutex.withLock {
val credentials = credentialsStore.get() ?: return@withLock null
res != LoginResult.InvalidCredentials
} catch (e: IOException) {
Log.e("AuthRepo", "restoreSession error", e)
false
val result = api.login(
credentials.login,
credentials.password
)
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.engine.android.Android
import io.ktor.client.plugins.HttpRequestRetry
import io.ktor.client.plugins.HttpSend
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.cache.HttpCache
import io.ktor.client.plugins.cookies.HttpCookies
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
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(HttpCookies)
install(Logging) {
@@ -34,4 +57,24 @@ fun createHttpClient() = HttpClient(Android) {
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_PASSWORD", password)
}
) { retry { noRetry() } }
) {
retry { noRetry() }
skipSessionRefresh()
}
if (r.status.value == 302)
return """PHPSESSID=([\w\d]+)""".toRegex().find(r.headers["Set-Cookie"]!!)!!.groupValues[1]
@@ -182,7 +185,9 @@ class Bitrix(
val client: HttpClient,
) {
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()))
}