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

Commit 7fbf831e authored by Abhishek Aggarwal's avatar Abhishek Aggarwal
Browse files

fix(source): rollback failed source selection updates

parent 27468a60
Loading
Loading
Loading
Loading
+17 −24
Original line number Diff line number Diff line
@@ -208,16 +208,6 @@ class PlayStoreRepository @Inject constructor(
        }
    }

    private suspend fun refreshPlayStoreToken() {
        Timber.i("Refreshing authentication.")
        when (val refreshResult = tokenRefreshHandler.refreshPlayStoreToken()) {
            is AuthResult.Success -> Unit
            is AuthResult.Failure -> {
                Timber.w("Token refresh failed: %s", refreshResult.error.describe())
            }
        }
    }

    private suspend fun <T> executeWithPlayAuthRecovery(
        operationName: String,
        request: suspend () -> T,
@@ -282,21 +272,24 @@ class PlayStoreRepository @Inject constructor(
        }
    }

    private suspend fun <T> retryAfterPlayAuthRefresh(
    private suspend fun <T> retryAfterSuccessfulPlayAuthRefreshOrNull(
        operationName: String,
        reason: String,
        invalidateBootstrapTokens: Boolean = false,
        request: suspend () -> T,
    ): T {
    ): T? {
        Timber.i("Retrying %s after refreshing Play auth because %s", operationName, reason)
        if (invalidateBootstrapTokens) {
            // A 401 means cached Play auth is definitively rejected. Evict the stored AAS token
            // too, so Google-mode refresh does not silently recreate auth from the same stale
            // bootstrap credential.
            playStoreAuthStore.saveAasToken("")

        return when (val refreshResult = tokenRefreshHandler.refreshPlayStoreToken()) {
            is AuthResult.Success -> request()
            is AuthResult.Failure -> {
                Timber.w(
                    "Skipping %s retry because Play auth refresh failed: %s",
                    operationName,
                    refreshResult.error.describe(),
                )
                null
            }
        }
        refreshPlayStoreToken()
        return request()
    }

    private suspend fun <T> retryAfterSuccessfulPlayAuthRefreshOrThrow(
@@ -353,11 +346,11 @@ class PlayStoreRepository @Inject constructor(
        }

        Timber.i("Version code is 0 for all app details.")
        val refreshedAppDetails = retryAfterPlayAuthRefresh(
        val refreshedAppDetails = retryAfterSuccessfulPlayAuthRefreshOrNull(
            operationName = "app details list",
            reason = "all version codes are 0",
            request = request,
        )
        ) ?: return appDetails

        if (refreshedAppDetails.all { it.versionCode == 0L }) {
            Timber.w("After refreshing auth, version code is still 0 for all app details.")
@@ -375,11 +368,11 @@ class PlayStoreRepository @Inject constructor(
        }

        Timber.i("Version code is 0 for %s.", appDetails.packageName)
        val refreshedAppDetails = retryAfterPlayAuthRefresh(
        val refreshedAppDetails = retryAfterSuccessfulPlayAuthRefreshOrNull(
            operationName = "app details",
            reason = "version code is 0 for ${appDetails.packageName}",
            request = request,
        )
        ) ?: appDetails

        if (refreshedAppDetails.versionCode == 0L) {
            Timber.w("After refreshing auth, version code is still 0. Giving up installation.")
+84 −1
Original line number Diff line number Diff line
@@ -22,9 +22,19 @@ import androidx.work.ExistingPeriodicWorkPolicy
import foundation.e.apps.domain.source.SourceSelection
import foundation.e.apps.domain.source.SourceSelectionRepository
import foundation.e.apps.domain.updates.PendingUpdatesRepository
import foundation.e.apps.feature.auth.session.SessionRefreshException
import foundation.e.apps.feature.auth.session.SessionStateController
import foundation.e.apps.updates.PeriodicUpdatesScheduler
import timber.log.Timber
import java.io.IOException
import javax.inject.Inject
import kotlin.coroutines.cancellation.CancellationException

class SourceSelectionUpdateException(
    val requestedSelection: SourceSelection,
    val restoredSelection: SourceSelection,
    cause: Throwable,
) : IllegalStateException("Failed to update source selection", cause)

class UpdateSourceSelectionCoordinator @Inject constructor(
    private val pendingUpdatesRepository: PendingUpdatesRepository,
@@ -33,7 +43,20 @@ class UpdateSourceSelectionCoordinator @Inject constructor(
    private val sourceSelectionRepository: SourceSelectionRepository,
) {
    suspend operator fun invoke(sourceSelection: SourceSelection) {
        val previousSelection = sourceSelectionRepository.currentSourceSelection()
        try {
            sourceSelectionRepository.saveSourceSelection(sourceSelection)
            applySourceSelectionSideEffects()
        } catch (exception: CancellationException) {
            throw exception
        } catch (exception: IOException) {
            failAfterRollback(sourceSelection, previousSelection, exception)
        } catch (exception: SessionRefreshException) {
            failAfterRollback(sourceSelection, previousSelection, exception)
        }
    }

    private suspend fun applySourceSelectionSideEffects() {
        pendingUpdatesRepository.clearPendingUpdates()
        periodicUpdatesScheduler.syncPeriodicUpdates(
            existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
@@ -41,4 +64,64 @@ class UpdateSourceSelectionCoordinator @Inject constructor(
        sessionStateController.clearLoadedSessions()
        sessionStateController.refreshSessions()
    }

    private suspend fun failAfterRollback(
        requestedSelection: SourceSelection,
        previousSelection: SourceSelection,
        originalFailure: Exception,
    ): Nothing {
        rollbackSourceSelection(previousSelection, originalFailure)
        throw SourceSelectionUpdateException(
            requestedSelection = requestedSelection,
            restoredSelection = previousSelection,
            cause = originalFailure,
        )
    }

    private suspend fun rollbackSourceSelection(
        previousSelection: SourceSelection,
        originalFailure: Exception,
    ) {
        try {
            sourceSelectionRepository.saveSourceSelection(previousSelection)
        } catch (exception: IllegalStateException) {
            originalFailure.addSuppressed(exception)
            Timber.w(exception, "Failed to restore previous source selection")
            return
        }

        runRollbackStep(
            description = "reschedule updates for restored source selection",
            originalFailure = originalFailure,
        ) {
            periodicUpdatesScheduler.syncPeriodicUpdates(
                existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
            )
        }
        runRollbackStep(
            description = "refresh sessions for restored source selection",
            originalFailure = originalFailure,
        ) {
            sessionStateController.clearLoadedSessions()
            sessionStateController.refreshSessions()
        }
    }

    private suspend fun runRollbackStep(
        description: String,
        originalFailure: Exception,
        block: suspend () -> Unit,
    ) {
        try {
            block()
        } catch (exception: CancellationException) {
            throw exception
        } catch (exception: IOException) {
            originalFailure.addSuppressed(exception)
            Timber.w(exception, "Failed to %s", description)
        } catch (exception: SessionRefreshException) {
            originalFailure.addSuppressed(exception)
            Timber.w(exception, "Failed to %s", description)
        }
    }
}
+9 −0
Original line number Diff line number Diff line
@@ -190,6 +190,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
                    when (event) {
                        SettingsUiEvent.PlayStoreLoginRequired -> showPlayStoreLoginRequiredMessage()
                        SettingsUiEvent.SelectAtLeastOneSource -> showOnlyRemainingSourceWarning()
                        SettingsUiEvent.SourceSelectionUpdateFailed -> showSourceSelectionUpdateFailed()
                    }
                }
            }
@@ -314,6 +315,14 @@ class SettingsFragment : PreferenceFragmentCompat() {
        ).show()
    }

    private fun showSourceSelectionUpdateFailed() {
        Toast.makeText(
            requireActivity(),
            R.string.source_selection_update_failed,
            Toast.LENGTH_SHORT
        ).show()
    }

    private fun disableDependentCheckbox(
        checkBox: CheckBoxPreference?,
        parentCheckBox: CheckBoxPreference?
+1 −0
Original line number Diff line number Diff line
@@ -21,4 +21,5 @@ package foundation.e.apps.ui.settings
sealed interface SettingsUiEvent {
    data object PlayStoreLoginRequired : SettingsUiEvent
    data object SelectAtLeastOneSource : SettingsUiEvent
    data object SourceSelectionUpdateFailed : SettingsUiEvent
}
+3 −5
Original line number Diff line number Diff line
@@ -30,7 +30,7 @@ import foundation.e.apps.domain.auth.PlayStoreLoginMode
import foundation.e.apps.domain.source.AppSource
import foundation.e.apps.domain.source.SourceSelection
import foundation.e.apps.domain.source.SourceSelectionRepository
import foundation.e.apps.feature.auth.session.SessionRefreshException
import foundation.e.apps.feature.auth.source.SourceSelectionUpdateException
import foundation.e.apps.feature.auth.source.UpdateSourceSelectionCoordinator
import foundation.e.apps.updates.PeriodicUpdatesScheduler
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -42,7 +42,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.IOException
import javax.inject.Inject

@HiltViewModel
@@ -102,10 +101,9 @@ class SettingsViewModel @Inject constructor(
        viewModelScope.launch {
            try {
                updateSourceSelectionCoordinator(sourceSelection)
            } catch (exception: IOException) {
                Timber.e(exception, "Failed to update source selection")
            } catch (exception: SessionRefreshException) {
            } catch (exception: SourceSelectionUpdateException) {
                Timber.e(exception, "Failed to update source selection")
                _uiEvents.emit(SettingsUiEvent.SourceSelectionUpdateFailed)
            }
        }
    }
Loading