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

Commit b1a66518 authored by Guillaume Jacquart's avatar Guillaume Jacquart
Browse files

feat:3871: Separate new and old error handling ways. Add mutex.

parent 52b02786
Loading
Loading
Loading
Loading
Loading
+0 −1
Original line number Diff line number Diff line
@@ -42,7 +42,6 @@ sealed class AuthObject {
    abstract fun createInvalidAuthObject(): AuthObject

    class GPlayAuth(override val result: ResultSupreme<AuthData?>, override val user: User) : AuthObject() {
        // Seule création de GPlayValidationException
        override fun createInvalidAuthObject(): AuthObject {
            val message = "Validating AuthData failed.\nNetwork code: 401"

+5 −12
Original line number Diff line number Diff line
@@ -407,17 +407,12 @@ class PlayStoreRepository @Inject constructor(

    private suspend fun <T> doAuthenticatedRequest(request: suspend (AuthData) -> T): T {
        val authData = appLoungeDataStore.getAuthData()
        return try {
            gPlayRequestWithUnswallowedException { request(authData) }
        } catch (e: HttpUnauthorizedException) {
            _faultyEmail.emit(authData.email)
            throw e
        }
        return request(authData)
    }

    // Prepared for next refactoring task.
    @Suppress("UnusedPrivateMember")
    private suspend fun <T> doAuthenticatedRequestAutoRefreshToken(request: suspend (AuthData) -> T): T {
    private suspend fun <T> doAuthenticatedRequestAutoRefreshToken(request: (AuthData) -> T): T {
        return exponentialBackoffRetry(
            initialDelay = 100.milliseconds,
            maxDelay = 15.seconds, // exp: 0,2, 0,4, 0,8, 1,6, 3,2, 6,4 ; 12,8 .
@@ -437,7 +432,7 @@ class PlayStoreRepository @Inject constructor(
    }

    @Suppress("ThrowsCount", "TooGenericExceptionCaught")
    private suspend fun <T> gPlayRequestWithUnswallowedException(request: suspend () -> T): T {
    private suspend fun <T> gPlayRequestWithUnswallowedException(request: () -> T): T {
        val lastErrorFlow = gPlayHttpClient.requestException.shareIn(
            scope = ioCoroutineScope,
            started = SharingStarted.Eagerly,
@@ -446,16 +441,14 @@ class PlayStoreRepository @Inject constructor(

        @Suppress("TooGenericExceptionCaught")
        return try {
            request()
            gPlayHttpClient.withMutex(swallowExceptions = false, request)
        } catch (e: Exception) {
            lastErrorFlow.timeout(gPlayHttpClientPukedExceptionWaitDuration)
                .catch {
                    Timber.d("No error emitted from GPlayHttpClient after timeout.")
                    throw e
                }.collect { pukedException ->
                    Timber.d(pukedException, "Error in GPlay request, Reswallow it for now.")
                    // Re-swallow the exception, until upper layer ar eready o receive new exceptions.
                    throw e
                    throw pukedException
                }
            Timber.w("Un-swallow exception failed, reach un-reachable code")
            throw e
+78 −53
Original line number Diff line number Diff line
@@ -38,6 +38,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.Headers.Companion.toHeaders
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
@@ -84,6 +86,11 @@ class GPlayHttpClient @Inject constructor(
    private val _requestException = MutableSharedFlow<Exception>()
    val requestException: Flow<Exception> = _requestException

    private val mutex = Mutex()

    // Legacy flag, to remove after https://gitlab.e.foundation/groups/e/os/-/epics/227
    private var swallowExceptions: Boolean = true

    @VisibleForTesting
    var okHttpClient = OkHttpClient().newBuilder()
        .retryOnConnectionFailure(false)
@@ -178,6 +185,15 @@ class GPlayHttpClient @Inject constructor(
        return processRequest(request)
    }

    suspend fun <T> withMutex(swallowExceptions: Boolean, action: () -> T): T {
        this.swallowExceptions = swallowExceptions
        return try {
            mutex.withLock(action = action)
        } finally {
            this.swallowExceptions = true
        }
    }

    private fun headersWithLocale(headers: Map<String, String>): Map<String, String> {
        val headersWithLocale = headers.toMutableMap()
        headersWithLocale["Accept-Language"] = Locale.getDefault().language
@@ -185,15 +201,31 @@ class GPlayHttpClient @Inject constructor(
    }

    private fun processRequest(request: Request): PlayResponse {
        if (swallowExceptions) {
            return processRequestSwallowExceptions(request)
        }

        // Reset response code as flow doesn't sends the same value twice
        _responseCode.value = 0
        var response: Response? = null
        return try {
            val call = okHttpClient.newCall(request)
            response = call.execute()
            buildPlayResponse(response).apply {
                _responseCode.value = code

            if (!response.isSuccessful) {
                throw when (response.code) {
                    STATUS_CODE_UNAUTHORIZED -> HttpUnauthorizedException(message = response.message)
                    STATUS_CODE_TOO_MANY_REQUESTS -> HttpTooManyRequestException(message = response.message)
                    else -> HttpFailedCodeException(statusCode = response.code, message = response.message)
                }
            }

            _responseCode.value = response.code
            PlayResponse(
                isSuccessful = true,
                code = response.code,
                responseBytes = response.body.bytes()
            )
        } catch (e: Exception) {
            val parsedException = when (e) {
                is SocketTimeoutException,
@@ -207,21 +239,25 @@ class GPlayHttpClient @Inject constructor(
            ioCoroutineScope.launch {
                _requestException.emit(parsedException)
            }

            // Swallow the exception, until upper layer are upgraded.
            throw when (parsedException) {
                is HttpFailedCodeException ->
                    GplayHttpRequestException(
                        parsedException.statusCode,
                        parsedException.message ?: "HttpFailed exception ${parsedException.statusCode}"
                    )
                is ConnectivityException ->
                    GplayHttpRequestException(
                        if (e is SocketTimeoutException) STATUS_CODE_TIMEOUT else -1,
                        parsedException.message ?: ""
                    )
                else -> GplayHttpRequestException(-1, e.localizedMessage ?: "")
            throw parsedException
        } finally {
            response?.close()
        }
    }

    private fun processRequestSwallowExceptions(request: Request): PlayResponse {
        // Reset response code as flow doesn't sends the same value twice
        _responseCode.value = 0
        var response: Response? = null
        return try {
            val call = okHttpClient.newCall(request)
            response = call.execute()
            buildPlayResponse(response)
        } catch (e: GplayHttpRequestException) {
            throw e
        } catch (e: Exception) {
            val status = if (e is SocketTimeoutException) STATUS_CODE_TIMEOUT else -1
            throw GplayHttpRequestException(status, e.localizedMessage ?: "")
        } finally {
            response?.close()
        }
@@ -239,17 +275,9 @@ class GPlayHttpClient @Inject constructor(
        val url = response.request.url
        val code = response.code
        val isSuccessful = response.isSuccessful
        val errorMessage = if (!isSuccessful) response.message else null
        val responseBytes = response.body?.bytes() ?: byteArrayOf()

        if (!isSuccessful) {
            val httpException = when (code) {
                STATUS_CODE_UNAUTHORIZED -> HttpUnauthorizedException(message = response.message)
                STATUS_CODE_TOO_MANY_REQUESTS -> HttpTooManyRequestException(message = response.message)
                else -> HttpFailedCodeException(statusCode = response.code, message = response.message)
            }


            // TODO: remove that
        when (code) {
            STATUS_CODE_UNAUTHORIZED -> MainScope().launch {
                EventBus.invokeEvent(
@@ -268,28 +296,25 @@ class GPlayHttpClient @Inject constructor(
            }
        }

            // TODO 20251211: limit this hack to purchase procedure.
            if (url.toString().contains(URL_SUBSTRING_PURCHASE) && code in listOf(
        if (!url.toString().contains(URL_SUBSTRING_PURCHASE) && code !in listOf(
                STATUS_CODE_OK,
                STATUS_CODE_UNAUTHORIZED
            )
        ) {
                return PlayResponse(
                    isSuccessful = false,
                    code = code,
                    responseBytes = responseBytes,
                    errorString = response.message
                )
            throw GplayHttpRequestException(code, response.message)
        }

            throw httpException
        fun PlayResponse.withErrorString(error: String?): PlayResponse {
            return if (error == null) return this else copy(errorString = error)
        }

        return PlayResponse(
            isSuccessful = isSuccessful,
            code = code,
            responseBytes = responseBytes
        )
            responseBytes = responseBytes,
        ).withErrorString(errorMessage).apply {
            _responseCode.value = response.code
        }
    }
}

+16 −1
Original line number Diff line number Diff line
@@ -6,9 +6,13 @@ import foundation.e.apps.data.playstore.PlayStoreRepository
import foundation.e.apps.data.preference.AppLoungeDataStore
import foundation.e.apps.di.qualifiers.IoCoroutineScope
import foundation.e.apps.utils.SystemInfoProvider
import foundation.e.apps.utils.eventBus.AppEvent
import foundation.e.apps.utils.eventBus.EventBus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@@ -21,7 +25,18 @@ class ReportFaultyGPlayUser @Inject constructor(
    @IoCoroutineScope private val ioCoroutineScope: CoroutineScope
) {
    fun listen() {
        playStoreRepository.faultyEmail.map(::report).launchIn(ioCoroutineScope)
        merge(
            playStoreRepository.faultyEmail,
            // TODO: to remove when finalizing:
            //  https://gitlab.e.foundation/groups/e/os/-/epics/227
            EventBus.events.mapNotNull { appEvent ->
                if (appEvent is AppEvent.InvalidAuthEvent) {
                    appLoungeDataStore.getAuthData().email
                } else {
                    null
                }
            }
        ).map(::report).launchIn(ioCoroutineScope)
    }

    private suspend fun report(email: String) {
+10 −3
Original line number Diff line number Diff line
@@ -26,15 +26,15 @@ import android.os.LocaleList
import com.aurora.gplayapi.data.models.AuthData
import foundation.e.apps.data.application.ApplicationDataManager
import foundation.e.apps.data.enums.User
import foundation.e.apps.data.preference.AppLoungeDataStore
import foundation.e.apps.data.playstore.utils.GPlayHttpClient
import foundation.e.apps.data.preference.AppLoungeDataStore
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json
import org.junit.Before
@@ -64,6 +64,7 @@ class PlayStoreRepositoryTest {

    private lateinit var repository: PlayStoreRepository
    private val expectedLocale = Locale.FRANCE
    private val ioCoroutineScope = TestScope()

    @Before
    fun setup() {
@@ -81,7 +82,8 @@ class PlayStoreRepositoryTest {
            tokenDispenserDataSource,
            googleLoginDataSource,
            nativeDeviceProperty,
            json
            json,
            ioCoroutineScope
        )
    }

@@ -158,4 +160,9 @@ class PlayStoreRepositoryTest {
        coVerify(exactly = 1) { appLoungeDataStore.saveAasToken(newAasToken) }
        coVerify(exactly = 1) { googleLoginDataSource.googleLogin(email, newAasToken, nativeDeviceProperty) }
    }

    @Test
    fun doAuthenticatedRequest_should


}