From 9e94a67a85ac781aad89a9ec39bbdc4f0c03fec7 Mon Sep 17 00:00:00 2001 From: Sweetbread Date: Thu, 17 Sep 2026 14:11:34 +0300 Subject: [PATCH] fix: network connectivity related issues --- app/src/main/AndroidManifest.xml | 3 +- .../ru/sweetbread/unn/app/LoginActivity.kt | 13 ++-- .../ru/sweetbread/unn/app/MainActivity.kt | 66 ++++++++++++++++++- .../main/java/ru/sweetbread/unn/app/UNNApp.kt | 13 +++- .../ru/sweetbread/unn/core/AuthRepository.kt | 39 ++++++----- .../ru/sweetbread/unn/core/network/client.kt | 47 ++++++++++++- .../sweetbread/unn/core/network/endpoints.kt | 11 +++- 7 files changed, 158 insertions(+), 34 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f277ff2..72e4ec8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,10 +1,11 @@ - + + () - finish() - } + if (authRepo.hasSavedCredentials()) { + start() + finish() + return } setContent { @@ -161,4 +158,4 @@ fun LoginPanel( Text(stringResource(R.string.sign_in)) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/ru/sweetbread/unn/app/MainActivity.kt b/app/src/main/java/ru/sweetbread/unn/app/MainActivity.kt index a5b6257..e1e423b 100644 --- a/app/src/main/java/ru/sweetbread/unn/app/MainActivity.kt +++ b/app/src/main/java/ru/sweetbread/unn/app/MainActivity.kt @@ -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 @@ -287,4 +349,4 @@ class MainActivity : ComponentActivity() { Text("User not found") } } -} \ No newline at end of file +} diff --git a/app/src/main/java/ru/sweetbread/unn/app/UNNApp.kt b/app/src/main/java/ru/sweetbread/unn/app/UNNApp.kt index 2e2df7b..f108551 100644 --- a/app/src/main/java/ru/sweetbread/unn/app/UNNApp.kt +++ b/app/src/main/java/ru/sweetbread/unn/app/UNNApp.kt @@ -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(name = "cache") { fallbackToDestructiveMigration(dropAllTables = true) } @@ -61,4 +70,4 @@ class AppContainer(context: Context) { api = portalApi, db = database, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/ru/sweetbread/unn/core/AuthRepository.kt b/app/src/main/java/ru/sweetbread/unn/core/AuthRepository.kt index b027c61..d8800b4 100644 --- a/app/src/main/java/ru/sweetbread/unn/core/AuthRepository.kt +++ b/app/src/main/java/ru/sweetbread/unn/core/AuthRepository.kt @@ -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(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 } -} \ No newline at end of file +} diff --git a/app/src/main/java/ru/sweetbread/unn/core/network/client.kt b/app/src/main/java/ru/sweetbread/unn/core/network/client.kt index 7c7cebb..e5c5245 100644 --- a/app/src/main/java/ru/sweetbread/unn/core/network/client.kt +++ b/app/src/main/java/ru/sweetbread/unn/core/network/client.kt @@ -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("SkipSessionRefresh") +private val SessionRefreshAttempted = AttributeKey("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()) } } -} \ No newline at end of file +}.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 + } + } +} diff --git a/app/src/main/java/ru/sweetbread/unn/core/network/endpoints.kt b/app/src/main/java/ru/sweetbread/unn/core/network/endpoints.kt index 2269876..3737a76 100644 --- a/app/src/main/java/ru/sweetbread/unn/core/network/endpoints.kt +++ b/app/src/main/java/ru/sweetbread/unn/core/network/endpoints.kt @@ -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())) } @@ -198,4 +203,4 @@ class Bitrix( } return NewsDto.fromJsonArray(JSONArray(r.bodyAsText())) } -} \ No newline at end of file +}