Loading app/src/main/java/foundation/e/apps/data/login/AuthObject.kt +0 −1 Original line number Diff line number Diff line Loading @@ -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" Loading app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt +5 −12 Original line number Diff line number Diff line Loading @@ -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 . Loading @@ -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, Loading @@ -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 Loading app/src/main/java/foundation/e/apps/data/playstore/utils/GPlayHttpClient.kt +78 −53 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) Loading Loading @@ -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 Loading @@ -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, Loading @@ -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() } Loading @@ -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( Loading @@ -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 } } } Loading app/src/main/java/foundation/e/apps/domain/usecases/ReportFaultyGPlayUser.kt +16 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) { Loading app/src/test/java/foundation/e/apps/data/playstore/PlayStoreRepositoryTest.kt +10 −3 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -64,6 +64,7 @@ class PlayStoreRepositoryTest { private lateinit var repository: PlayStoreRepository private val expectedLocale = Locale.FRANCE private val ioCoroutineScope = TestScope() @Before fun setup() { Loading @@ -81,7 +82,8 @@ class PlayStoreRepositoryTest { tokenDispenserDataSource, googleLoginDataSource, nativeDeviceProperty, json json, ioCoroutineScope ) } Loading Loading @@ -158,4 +160,9 @@ class PlayStoreRepositoryTest { coVerify(exactly = 1) { appLoungeDataStore.saveAasToken(newAasToken) } coVerify(exactly = 1) { googleLoginDataSource.googleLogin(email, newAasToken, nativeDeviceProperty) } } @Test fun doAuthenticatedRequest_should } Loading
app/src/main/java/foundation/e/apps/data/login/AuthObject.kt +0 −1 Original line number Diff line number Diff line Loading @@ -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" Loading
app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt +5 −12 Original line number Diff line number Diff line Loading @@ -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 . Loading @@ -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, Loading @@ -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 Loading
app/src/main/java/foundation/e/apps/data/playstore/utils/GPlayHttpClient.kt +78 −53 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) Loading Loading @@ -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 Loading @@ -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, Loading @@ -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() } Loading @@ -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( Loading @@ -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 } } } Loading
app/src/main/java/foundation/e/apps/domain/usecases/ReportFaultyGPlayUser.kt +16 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) { Loading
app/src/test/java/foundation/e/apps/data/playstore/PlayStoreRepositoryTest.kt +10 −3 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -64,6 +64,7 @@ class PlayStoreRepositoryTest { private lateinit var repository: PlayStoreRepository private val expectedLocale = Locale.FRANCE private val ioCoroutineScope = TestScope() @Before fun setup() { Loading @@ -81,7 +82,8 @@ class PlayStoreRepositoryTest { tokenDispenserDataSource, googleLoginDataSource, nativeDeviceProperty, json json, ioCoroutineScope ) } Loading Loading @@ -158,4 +160,9 @@ class PlayStoreRepositoryTest { coVerify(exactly = 1) { appLoungeDataStore.saveAasToken(newAasToken) } coVerify(exactly = 1) { googleLoginDataSource.googleLogin(email, newAasToken, nativeDeviceProperty) } } @Test fun doAuthenticatedRequest_should }