mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-16 12:24:40 +00:00
Fix Android process-death recovery
Persist pending debrid device-code authorization so returning from the browser after Android process death can continue polling the same session. Recover cached profiles during auth rehydration instead of briefly dumping users to login after background kills. Include debrid and downloads preferences in local account cleanup on sign-out.
This commit is contained in:
parent
34c565e89f
commit
2b2d1f56ad
7 changed files with 110 additions and 6 deletions
|
|
@ -14,7 +14,9 @@ internal actual object PlatformLocalAccountDataCleaner {
|
|||
"nuvio_profile_pin_cache",
|
||||
"nuvio_theme_settings",
|
||||
"nuvio_poster_card_style",
|
||||
"nuvio_debrid_settings",
|
||||
"nuvio_mdblist_settings",
|
||||
"nuvio_downloads",
|
||||
"nuvio_trakt_auth",
|
||||
"nuvio_trakt_library",
|
||||
"nuvio_trakt_settings",
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ actual object DebridSettingsStorage {
|
|||
private const val streamPreferencesKey = "debrid_stream_preferences"
|
||||
private const val streamNameTemplateKey = "debrid_stream_name_template"
|
||||
private const val streamDescriptionTemplateKey = "debrid_stream_description_template"
|
||||
private const val pendingDeviceAuthorizationPrefix = "debrid_pending_device_authorization_"
|
||||
private fun syncKeys(): List<String> =
|
||||
listOf(
|
||||
enabledKey,
|
||||
|
|
@ -150,6 +151,20 @@ actual object DebridSettingsStorage {
|
|||
saveString(streamDescriptionTemplateKey, template)
|
||||
}
|
||||
|
||||
actual fun loadPendingDeviceAuthorization(providerId: String): String? =
|
||||
loadString(pendingDeviceAuthorizationKey(providerId))
|
||||
|
||||
actual fun savePendingDeviceAuthorization(providerId: String, payload: String) {
|
||||
saveString(pendingDeviceAuthorizationKey(providerId), payload)
|
||||
}
|
||||
|
||||
actual fun clearPendingDeviceAuthorization(providerId: String) {
|
||||
preferences
|
||||
?.edit()
|
||||
?.remove(ProfileScopedKey.of(pendingDeviceAuthorizationKey(providerId)))
|
||||
?.apply()
|
||||
}
|
||||
|
||||
private fun loadBoolean(key: String): Boolean? =
|
||||
preferences?.let { sharedPreferences ->
|
||||
val scopedKey = ProfileScopedKey.of(key)
|
||||
|
|
@ -249,4 +264,10 @@ actual object DebridSettingsStorage {
|
|||
else -> "debrid_${normalized}_api_key"
|
||||
}
|
||||
}
|
||||
|
||||
private fun pendingDeviceAuthorizationKey(providerId: String): String {
|
||||
val normalized = DebridProviders.byId(providerId)?.id
|
||||
?: providerId.trim().lowercase().replace(Regex("[^a-z0-9_]+"), "_")
|
||||
return "$pendingDeviceAuthorizationPrefix$normalized"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -466,21 +466,26 @@ fun App() {
|
|||
|
||||
LaunchedEffect(authState, networkStatusUiState.condition, profileState.profiles) {
|
||||
val cachedProfiles = profileState.profiles
|
||||
val allowOfflineProfileAccess =
|
||||
val hasCachedProfileAccess =
|
||||
cachedProfiles.isNotEmpty() &&
|
||||
authState !is AuthState.Authenticated &&
|
||||
networkStatusUiState.condition != NetworkCondition.Online
|
||||
authState !is AuthState.Authenticated
|
||||
val allowCachedProfileAccess =
|
||||
hasCachedProfileAccess &&
|
||||
(
|
||||
networkStatusUiState.condition != NetworkCondition.Online ||
|
||||
gateScreen != AppGateScreen.Auth.name
|
||||
)
|
||||
|
||||
when (authState) {
|
||||
is AuthState.Loading -> {
|
||||
if (allowOfflineProfileAccess) {
|
||||
if (hasCachedProfileAccess) {
|
||||
enterProfileGate(cachedProfiles, syncOnEnter = false)
|
||||
} else {
|
||||
gateScreen = AppGateScreen.Loading.name
|
||||
}
|
||||
}
|
||||
is AuthState.Unauthenticated -> {
|
||||
if (allowOfflineProfileAccess) {
|
||||
if (allowCachedProfileAccess) {
|
||||
enterProfileGate(cachedProfiles, syncOnEnter = false)
|
||||
} else {
|
||||
ProfileRepository.clearInMemory()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.nuvio.app.features.debrid
|
|||
import com.nuvio.app.features.streams.StreamClientResolve
|
||||
import com.nuvio.app.features.streams.StreamItem
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
internal interface DebridProviderApi {
|
||||
val provider: DebridProvider
|
||||
|
|
@ -35,6 +36,7 @@ internal object DebridProviderApis {
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class DebridDeviceAuthorization(
|
||||
val providerId: String,
|
||||
val deviceCode: String,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ internal expect object DebridSettingsStorage {
|
|||
fun saveStreamNameTemplate(template: String)
|
||||
fun loadStreamDescriptionTemplate(): String?
|
||||
fun saveStreamDescriptionTemplate(template: String)
|
||||
fun loadPendingDeviceAuthorization(providerId: String): String?
|
||||
fun savePendingDeviceAuthorization(providerId: String, payload: String)
|
||||
fun clearPendingDeviceAuthorization(providerId: String)
|
||||
fun exportToSyncPayload(): JsonObject
|
||||
fun replaceFromSyncPayload(payload: JsonObject)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ import com.nuvio.app.features.debrid.DebridProviderAuthMethod
|
|||
import com.nuvio.app.features.debrid.DebridProviders
|
||||
import com.nuvio.app.features.debrid.DebridSettings
|
||||
import com.nuvio.app.features.debrid.DebridSettingsRepository
|
||||
import com.nuvio.app.features.debrid.DebridSettingsStorage
|
||||
import com.nuvio.app.features.debrid.DebridStreamFormatterDefaults
|
||||
import com.nuvio.app.features.debrid.DebridStreamAudioChannel
|
||||
import com.nuvio.app.features.debrid.DebridStreamAudioTag
|
||||
|
|
@ -74,6 +75,9 @@ import com.nuvio.app.features.debrid.DebridStreamVisualTag
|
|||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.action_cancel
|
||||
import nuvio.composeapp.generated.resources.action_clear
|
||||
|
|
@ -1467,12 +1471,21 @@ private fun DebridDeviceAuthDialog(
|
|||
isStarting = true
|
||||
isPolling = false
|
||||
statusMessage = null
|
||||
if (restartNonce == 0) {
|
||||
loadPendingDeviceAuthorization(provider.id)?.let { pendingSession ->
|
||||
session = pendingSession
|
||||
isStarting = false
|
||||
statusMessage = waitingMessage
|
||||
return@LaunchedEffect
|
||||
}
|
||||
}
|
||||
val startResult = runCatching {
|
||||
DebridProviderApis.apiFor(provider.id)?.startDeviceAuthorization("Nuvio")
|
||||
}.onFailure { error ->
|
||||
if (error is CancellationException) throw error
|
||||
}
|
||||
session = startResult.getOrNull()
|
||||
session?.let(::savePendingDeviceAuthorization)
|
||||
isStarting = false
|
||||
statusMessage = if (session == null) {
|
||||
startResult.exceptionOrNull()?.message?.takeIf { it.contains("PREMIUMIZE_CLIENT_ID") }
|
||||
|
|
@ -1504,6 +1517,7 @@ private fun DebridDeviceAuthDialog(
|
|||
isPolling = false
|
||||
when (result) {
|
||||
is DebridDeviceAuthorizationTokenResult.Authorized -> {
|
||||
clearPendingDeviceAuthorization(provider.id)
|
||||
onConnected(result.accessToken)
|
||||
onDismiss()
|
||||
return@LaunchedEffect
|
||||
|
|
@ -1514,16 +1528,19 @@ private fun DebridDeviceAuthDialog(
|
|||
}
|
||||
|
||||
DebridDeviceAuthorizationTokenResult.Expired -> {
|
||||
clearPendingDeviceAuthorization(provider.id)
|
||||
statusMessage = expiredMessage
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
is DebridDeviceAuthorizationTokenResult.Failed -> {
|
||||
clearPendingDeviceAuthorization(provider.id)
|
||||
statusMessage = result.message.toDeviceAuthStatusMessage(failedMessage)
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
||||
DebridDeviceAuthorizationTokenResult.Unsupported -> {
|
||||
clearPendingDeviceAuthorization(provider.id)
|
||||
statusMessage = failedMessage
|
||||
return@LaunchedEffect
|
||||
}
|
||||
|
|
@ -1621,6 +1638,7 @@ private fun DebridDeviceAuthDialog(
|
|||
if (isConnected) {
|
||||
Button(
|
||||
onClick = {
|
||||
clearPendingDeviceAuthorization(provider.id)
|
||||
onDisconnect()
|
||||
onDismiss()
|
||||
},
|
||||
|
|
@ -1629,7 +1647,12 @@ private fun DebridDeviceAuthDialog(
|
|||
}
|
||||
}
|
||||
if (!isConnected && !isStarting && session == null) {
|
||||
TextButton(onClick = { restartNonce += 1 }) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
clearPendingDeviceAuthorization(provider.id)
|
||||
restartNonce += 1
|
||||
},
|
||||
) {
|
||||
Text(stringResource(Res.string.action_retry))
|
||||
}
|
||||
}
|
||||
|
|
@ -1649,6 +1672,34 @@ private fun DebridDeviceAuthDialog(
|
|||
}
|
||||
}
|
||||
|
||||
private val debridDeviceAuthorizationJson = Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
private fun loadPendingDeviceAuthorization(providerId: String): DebridDeviceAuthorization? =
|
||||
DebridSettingsStorage.loadPendingDeviceAuthorization(providerId)
|
||||
.orEmpty()
|
||||
.trim()
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.let { payload ->
|
||||
runCatching {
|
||||
debridDeviceAuthorizationJson.decodeFromString<DebridDeviceAuthorization>(payload)
|
||||
}.getOrNull()
|
||||
}
|
||||
?.takeIf { it.providerId == providerId }
|
||||
|
||||
private fun savePendingDeviceAuthorization(session: DebridDeviceAuthorization) {
|
||||
DebridSettingsStorage.savePendingDeviceAuthorization(
|
||||
providerId = session.providerId,
|
||||
payload = debridDeviceAuthorizationJson.encodeToString(session),
|
||||
)
|
||||
}
|
||||
|
||||
private fun clearPendingDeviceAuthorization(providerId: String) {
|
||||
DebridSettingsStorage.clearPendingDeviceAuthorization(providerId)
|
||||
}
|
||||
|
||||
private fun Throwable.isCancelledHttpRequest(): Boolean {
|
||||
val text = listOfNotNull(message, toString())
|
||||
.joinToString(" ")
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ actual object DebridSettingsStorage {
|
|||
private const val streamPreferencesKey = "debrid_stream_preferences"
|
||||
private const val streamNameTemplateKey = "debrid_stream_name_template"
|
||||
private const val streamDescriptionTemplateKey = "debrid_stream_description_template"
|
||||
private const val pendingDeviceAuthorizationPrefix = "debrid_pending_device_authorization_"
|
||||
private fun syncKeys(): List<String> =
|
||||
listOf(
|
||||
enabledKey,
|
||||
|
|
@ -142,6 +143,19 @@ actual object DebridSettingsStorage {
|
|||
saveString(streamDescriptionTemplateKey, template)
|
||||
}
|
||||
|
||||
actual fun loadPendingDeviceAuthorization(providerId: String): String? =
|
||||
loadString(pendingDeviceAuthorizationKey(providerId))
|
||||
|
||||
actual fun savePendingDeviceAuthorization(providerId: String, payload: String) {
|
||||
saveString(pendingDeviceAuthorizationKey(providerId), payload)
|
||||
}
|
||||
|
||||
actual fun clearPendingDeviceAuthorization(providerId: String) {
|
||||
NSUserDefaults.standardUserDefaults.removeObjectForKey(
|
||||
ProfileScopedKey.of(pendingDeviceAuthorizationKey(providerId)),
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadBoolean(key: String): Boolean? {
|
||||
val defaults = NSUserDefaults.standardUserDefaults
|
||||
val scopedKey = ProfileScopedKey.of(key)
|
||||
|
|
@ -232,4 +246,10 @@ actual object DebridSettingsStorage {
|
|||
else -> "debrid_${normalized}_api_key"
|
||||
}
|
||||
}
|
||||
|
||||
private fun pendingDeviceAuthorizationKey(providerId: String): String {
|
||||
val normalized = DebridProviders.byId(providerId)?.id
|
||||
?: providerId.trim().lowercase().replace(Regex("[^a-z0-9_]+"), "_")
|
||||
return "$pendingDeviceAuthorizationPrefix$normalized"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue