Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f96b608b authored by Abhishek Aggarwal's avatar Abhishek Aggarwal
Browse files

refactor(auth): align legacy failure handling with auth store boundaries

parent 95dbb543
Loading
Loading
Loading
Loading
+50 −13
Original line number Diff line number Diff line
@@ -52,9 +52,10 @@ class SessionStateHolder @Inject constructor(
    }

    private val refreshMutex = Mutex()
    private val faultyTokenReportMutex = Mutex()
    private val faultyTokenReportLock = Any()
    private var inFlightRefresh: InFlightRefresh? = null

    private var faultyTokenReportVersion: Long = 0
    private var latestFaultyTokenReport: FaultyTokenReportPayload? = null

    private val _activeSessions = MutableStateFlow<List<AuthSession>>(emptyList())
@@ -146,7 +147,7 @@ class SessionStateHolder @Inject constructor(
    private fun publishRefreshOutcome(authRefreshOutcome: AuthRefreshOutcome) {
        val authRefreshSnapshot = authRefreshOutcome.snapshot

        latestFaultyTokenReport = authRefreshOutcome.faultyTokenReport
        replaceFaultyTokenReport(authRefreshOutcome.faultyTokenReport)
        _activeSessions.value = authRefreshSnapshot.activeSessions
        _authRefreshState.value = AuthRefreshState.Completed(authRefreshSnapshot)
        _authRefreshSnapshot.value = authRefreshSnapshot
@@ -204,26 +205,20 @@ class SessionStateHolder @Inject constructor(
    }

    override suspend fun reportFaultyTokenIfNeeded() {
        val faultyTokenReport = faultyTokenReportMutex.withLock {
            latestFaultyTokenReport?.also {
                latestFaultyTokenReport = null
            }
        } ?: return
        val consumedFaultyTokenReport = consumeFaultyTokenReport() ?: return

        val reportFailure = runCatching {
            faultyTokenReporter.report(faultyTokenReport)
            faultyTokenReporter.report(consumedFaultyTokenReport.payload)
        }.exceptionOrNull()

        if (reportFailure != null) {
            faultyTokenReportMutex.withLock {
                latestFaultyTokenReport = latestFaultyTokenReport ?: faultyTokenReport
            }
            restoreFaultyTokenReport(consumedFaultyTokenReport)
            throw reportFailure
        }
    }

    override fun clearLoadedSessions() {
        latestFaultyTokenReport = null
        clearFaultyTokenReport()
        _activeSessions.value = emptyList()
        _authRefreshState.value = AuthRefreshState.Pending
        _authRefreshSnapshot.value = null
@@ -231,17 +226,59 @@ class SessionStateHolder @Inject constructor(

    override fun markLoggedOut() {
        val loggedOutSnapshot = AuthRefreshSnapshot(emptyList())
        latestFaultyTokenReport = null
        clearFaultyTokenReport()
        _activeSessions.value = emptyList()
        _authRefreshState.value = AuthRefreshState.Completed(loggedOutSnapshot)
        _authRefreshSnapshot.value = loggedOutSnapshot
    }

    private fun replaceFaultyTokenReport(payload: FaultyTokenReportPayload?) {
        synchronized(faultyTokenReportLock) {
            faultyTokenReportVersion += 1
            latestFaultyTokenReport = payload
        }
    }

    private fun consumeFaultyTokenReport(): ConsumedFaultyTokenReport? {
        return synchronized(faultyTokenReportLock) {
            latestFaultyTokenReport?.let { payload ->
                latestFaultyTokenReport = null
                ConsumedFaultyTokenReport(
                    payload = payload,
                    version = faultyTokenReportVersion,
                )
            }
        }
    }

    private fun restoreFaultyTokenReport(consumedFaultyTokenReport: ConsumedFaultyTokenReport) {
        synchronized(faultyTokenReportLock) {
            if (
                faultyTokenReportVersion == consumedFaultyTokenReport.version &&
                latestFaultyTokenReport == null
            ) {
                latestFaultyTokenReport = consumedFaultyTokenReport.payload
            }
        }
    }

    private fun clearFaultyTokenReport() {
        synchronized(faultyTokenReportLock) {
            faultyTokenReportVersion += 1
            latestFaultyTokenReport = null
        }
    }

    private data class InFlightRefresh(
        val storesToReset: List<AuthStore>,
        val completion: CompletableDeferred<Unit>,
    )

    private data class ConsumedFaultyTokenReport(
        val payload: FaultyTokenReportPayload,
        val version: Long,
    )

    private sealed interface RefreshRequest {
        data class StartNew(val refresh: InFlightRefresh) : RefreshRequest
        data class JoinExisting(val completion: CompletableDeferred<Unit>) : RefreshRequest
+1 −2
Original line number Diff line number Diff line
@@ -40,7 +40,6 @@ import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.install.pkg.AppLoungePackageManager
import foundation.e.apps.data.install.pkg.PwaManager
import foundation.e.apps.data.login.core.StoreType
import foundation.e.apps.databinding.FragmentApplicationBinding
import foundation.e.apps.domain.auth.AuthRefreshSnapshot
import foundation.e.apps.domain.auth.AuthSession
@@ -185,7 +184,7 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) {
                loadFailureDialogs.handleFailuresCommon(
                    failures = listOf(
                        playStoreFailure.toLegacyLoadFailure(
                            storeType = StoreType.PLAY_STORE,
                            authStore = AuthStore.PLAY_STORE,
                            loginMode = sessionRepository.awaitLoginIntent().toPlayStoreLoginModeOrNull(),
                        ),
                    ),
+2 −2
Original line number Diff line number Diff line
@@ -32,7 +32,7 @@ import foundation.e.apps.data.login.exceptions.CleanApkException
import foundation.e.apps.data.login.exceptions.GPlayException
import foundation.e.apps.ui.parentFragment.LegacyLoadFailure
import foundation.e.apps.ui.parentFragment.toLegacyLoadFailure
import foundation.e.apps.ui.parentFragment.toLegacyStoreType
import foundation.e.apps.ui.parentFragment.toLegacyAuthStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -72,7 +72,7 @@ class ApplicationListViewModel @Inject constructor(
            }

            if (!result.isSuccess()) {
                val loadFailure = result.toLegacyLoadFailure(sourceType.toLegacyStoreType()) { isTimeout, message ->
                val loadFailure = result.toLegacyLoadFailure(sourceType.toLegacyAuthStore()) { isTimeout, message ->
                    if (isCleanApk) {
                        CleanApkException(isTimeout, message)
                    } else {
+2 −2
Original line number Diff line number Diff line
@@ -32,7 +32,7 @@ import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.login.exceptions.CleanApkException
import foundation.e.apps.data.login.exceptions.GPlayException
import foundation.e.apps.ui.parentFragment.LegacyLoadFailure
import foundation.e.apps.ui.parentFragment.toLegacyStoreType
import foundation.e.apps.ui.parentFragment.toLegacyAuthStore
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -92,7 +92,7 @@ class CategoriesViewModel @Inject constructor(
                    exceptions.add(
                        LegacyLoadFailure.fromException(
                            exception = error,
                            storeType = data.source.toLegacyStoreType(),
                            authStore = data.source.toLegacyAuthStore(),
                        )
                    )
                    continue
+53 −35
Original line number Diff line number Diff line
@@ -45,7 +45,7 @@ import java.net.HttpURLConnection
data class LegacyLoadFailure(
    val exception: Exception,
    val kind: LegacyLoadFailureKind,
    val storeType: StoreType? = null,
    val authStore: AuthStore? = null,
    val authError: AuthError? = null,
    val loginMode: PlayStoreLoginMode? = null,
    val storesToResetOnRetry: List<AuthStore> = emptyList(),
@@ -55,13 +55,13 @@ data class LegacyLoadFailure(
    companion object {
        fun fromException(
            exception: Exception,
            storeType: StoreType? = null,
            authStore: AuthStore? = null,
            authError: AuthError? = null,
            loginMode: PlayStoreLoginMode? = (exception as? GPlayLoginException)?.loginMode,
        ): LegacyLoadFailure {
            val kind = resolveKind(
                exception = exception,
                storeType = storeType,
                authStore = authStore,
                authError = authError,
            )
            val dialogSpec = resolveDialogSpec(
@@ -72,7 +72,7 @@ data class LegacyLoadFailure(
            return LegacyLoadFailure(
                exception = exception,
                kind = kind,
                storeType = storeType,
                authStore = authStore,
                authError = authError,
                loginMode = loginMode,
                storesToResetOnRetry = resolveStoresToResetOnRetry(
@@ -94,77 +94,82 @@ enum class LegacyLoadFailureKind {
}

fun ResultSupreme<*>.toLegacyLoadFailure(
    storeType: StoreType,
    authStore: AuthStore,
    fallbackException: (isTimeout: Boolean, message: String) -> Exception,
): LegacyLoadFailure {
    val resolvedMessage = message.ifBlank { "Data load error" }
    val resolvedException = exception ?: fallbackException(isTimeout(), resolvedMessage)
    val resolvedAuthError = when {
        this is ResultSupreme.Timeout || exception != null -> toAuthError(storeType)
        this is ResultSupreme.Timeout || exception != null -> toAuthError(authStore.toStoreType())
        else -> null
    }

    return LegacyLoadFailure.fromException(
        exception = resolvedException,
        storeType = storeType,
        authStore = authStore,
        authError = resolvedAuthError,
    )
}

fun Source.toLegacyStoreType(): StoreType {
fun Source.toLegacyAuthStore(): AuthStore {
    return when (this) {
        Source.PLAY_STORE -> StoreType.PLAY_STORE
        Source.OPEN_SOURCE, Source.PWA, Source.SYSTEM_APP -> StoreType.CLEAN_APK
        Source.PLAY_STORE -> AuthStore.PLAY_STORE
        Source.OPEN_SOURCE, Source.PWA, Source.SYSTEM_APP -> AuthStore.OPEN_SOURCE
    }
}

fun AuthError.toLegacyLoadFailure(
    storeType: StoreType,
    authStore: AuthStore,
    loginMode: PlayStoreLoginMode?,
): LegacyLoadFailure {
    return LegacyLoadFailure.fromException(
        exception = toLegacyException(storeType, loginMode),
        storeType = storeType,
        exception = toLegacyException(authStore, loginMode),
        authStore = authStore,
        authError = this,
        loginMode = loginMode,
    )
}

private fun AuthError.toLegacyException(
    storeType: StoreType,
    authStore: AuthStore,
    loginMode: PlayStoreLoginMode?,
): Exception {
    return when (this) {
        is AuthError.InvalidToken -> toValidationException(loginMode)
        is AuthError.NetworkFailure -> toNetworkException(storeType, loginMode)
        is AuthError.InvalidToken -> toInvalidTokenException(authStore, loginMode)
        is AuthError.NetworkFailure -> toNetworkException(authStore, loginMode)
        is AuthError.LoginRequired -> toLoginRequiredException(loginMode)
        is AuthError.RateLimited -> toStoreException(storeType)
        is AuthError.Unknown -> toStoreException(storeType)
        is AuthError.RateLimited -> toStoreException(authStore)
        is AuthError.Unknown -> toStoreException(authStore)
    }
}

private fun AuthError.toValidationException(
private fun AuthError.InvalidToken.toInvalidTokenException(
    authStore: AuthStore,
    loginMode: PlayStoreLoginMode?,
): GPlayValidationException {
    val message = when (this) {
        is AuthError.InvalidToken -> message.orEmpty()
        else -> ""
): Exception {
    return when (authStore) {
        AuthStore.PLAY_STORE -> toValidationException(loginMode)
        AuthStore.OPEN_SOURCE -> CleanApkException(false, message)
    }
}

private fun AuthError.InvalidToken.toValidationException(
    loginMode: PlayStoreLoginMode?,
): GPlayValidationException {
    return GPlayValidationException(
        message = message,
        message = message.orEmpty(),
        loginMode = loginMode,
        networkCode = HttpURLConnection.HTTP_UNAUTHORIZED,
    )
}

private fun AuthError.NetworkFailure.toNetworkException(
    storeType: StoreType,
    authStore: AuthStore,
    loginMode: PlayStoreLoginMode?,
): Exception {
    return when (storeType) {
        StoreType.PLAY_STORE -> GPlayLoginException(isTimeout, message, loginMode)
        StoreType.CLEAN_APK -> CleanApkException(isTimeout, message)
    return when (authStore) {
        AuthStore.PLAY_STORE -> GPlayLoginException(isTimeout, message, loginMode)
        AuthStore.OPEN_SOURCE -> CleanApkException(isTimeout, message)
    }
}

@@ -177,27 +182,33 @@ private fun AuthError.LoginRequired.toLoginRequiredException(
    }
}

private fun AuthError.toStoreException(storeType: StoreType): Exception {
private fun AuthError.toStoreException(authStore: AuthStore): Exception {
    val message = when (this) {
        is AuthError.RateLimited -> message
        is AuthError.Unknown -> message
        else -> null
    }

    return when (storeType) {
        StoreType.PLAY_STORE -> GPlayException(false, message)
        StoreType.CLEAN_APK -> CleanApkException(false, message)
    return when (authStore) {
        AuthStore.PLAY_STORE -> GPlayException(false, message)
        AuthStore.OPEN_SOURCE -> CleanApkException(false, message)
    }
}

private fun resolveKind(
    exception: Exception,
    storeType: StoreType?,
    authStore: AuthStore?,
    authError: AuthError?,
): LegacyLoadFailureKind {
    return when {
        authError is AuthError.RateLimited -> LegacyLoadFailureKind.RATE_LIMITED
        authError is AuthError.InvalidToken -> LegacyLoadFailureKind.SIGN_IN
        authError is AuthError.InvalidToken -> {
            if (authError.store == AuthStore.PLAY_STORE) {
                LegacyLoadFailureKind.SIGN_IN
            } else {
                LegacyLoadFailureKind.DATA_LOAD
            }
        }
        authError is AuthError.LoginRequired -> {
            if (authError.store == AuthStore.PLAY_STORE) {
                LegacyLoadFailureKind.SIGN_IN
@@ -208,7 +219,7 @@ private fun resolveKind(
        authError is AuthError.NetworkFailure -> {
            if (authError.isTimeout) {
                LegacyLoadFailureKind.TIMEOUT
            } else if (storeType == StoreType.PLAY_STORE) {
            } else if (authStore == AuthStore.PLAY_STORE) {
                LegacyLoadFailureKind.SIGN_IN
            } else {
                LegacyLoadFailureKind.DATA_LOAD
@@ -221,6 +232,13 @@ private fun resolveKind(
    }
}

private fun AuthStore.toStoreType(): StoreType {
    return when (this) {
        AuthStore.PLAY_STORE -> StoreType.PLAY_STORE
        AuthStore.OPEN_SOURCE -> StoreType.CLEAN_APK
    }
}

private fun resolveDialogSpec(
    exception: Exception,
    kind: LegacyLoadFailureKind,
Loading