Loading app/src/main/java/foundation/e/apps/data/NetworkHandler.kt +22 −0 Original line number Diff line number Diff line Loading @@ -27,9 +27,11 @@ import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import timber.log.Timber import java.io.InterruptedIOException import java.net.SocketTimeoutException private const val TIMEOUT = "Timeout" private val TIMEOUT_MESSAGE_PATTERNS = listOf("timeout", "timed out") private const val UNKNOWN = "Unknown" private const val STATUS = "Status:" private const val ERROR_GPLAY_API = "Gplay api has faced error!" Loading @@ -49,6 +51,8 @@ suspend fun <T> handleNetworkResult(call: suspend () -> T): ResultSupreme<T> { throw e } catch (e: SocketTimeoutException) { handleSocketTimeoutException(e) } catch (e: InterruptedIOException) { handleInterruptedIOException(e) } catch (e: GplayHttpRequestException) { resultSupremeGplayHttpRequestException(e) } catch (e: Exception) { Loading @@ -63,6 +67,16 @@ private fun <T> handleSocketTimeoutException(e: SocketTimeoutException): ResultS return resultTimeout } private fun <T> handleInterruptedIOException(e: InterruptedIOException): ResultSupreme<T> { return if (isTimeoutInterruptedIOException(e)) { val resultTimeout = ResultSupreme.Timeout<T>(exception = e) resultTimeout.message = extractErrorMessage(e) resultTimeout } else { handleOthersException(e) } } private fun <T> resultSupremeGplayHttpRequestException(e: GplayHttpRequestException): ResultSupreme<T> { val message = extractErrorMessage(e) val exception = GPlayException(e.status == GPlayHttpClient.STATUS_CODE_TIMEOUT, message) Loading @@ -83,12 +97,20 @@ private fun extractErrorMessage(e: Exception): String { val status = when (e) { is GplayHttpRequestException -> e.status.toString() is SocketTimeoutException -> TIMEOUT is InterruptedIOException -> if (isTimeoutInterruptedIOException(e)) TIMEOUT else UNKNOWN else -> UNKNOWN } return (e.localizedMessage?.ifBlank { ERROR_GPLAY_API } ?: ERROR_GPLAY_API) + " $STATUS $status" } private fun isTimeoutInterruptedIOException(e: InterruptedIOException): Boolean { val message = e.message ?: return true return TIMEOUT_MESSAGE_PATTERNS.any { pattern -> message.contains(pattern, ignoreCase = true) } } suspend fun <T> retryWithBackoff(retryDelayInSeconds: Int = -1, operation: suspend () -> T): T? { var result: T? = null try { Loading app/src/main/java/foundation/e/apps/data/fdroid/FDroidRepository.kt +20 −4 Original line number Diff line number Diff line Loading @@ -5,7 +5,12 @@ import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.cleanapk.ApkSignatureManager import foundation.e.apps.data.enums.Source import foundation.e.apps.data.fdroid.models.BuildInfo import foundation.e.apps.data.fdroid.models.FdroidApiModel import foundation.e.apps.data.fdroid.models.FdroidEntity import kotlinx.coroutines.CancellationException import retrofit2.Response import timber.log.Timber import java.io.IOException import javax.inject.Inject import javax.inject.Singleton Loading @@ -29,15 +34,15 @@ class FDroidRepository @Inject constructor( */ override suspend fun getFdroidInfo(packageName: String): FdroidEntity? { return fdroidDao.getFdroidEntityFromPackageName(packageName) ?: fdroidApi.getFdroidInfoForPackage(packageName).body()?.let { ?: getFdroidApiResponse(packageName)?.body()?.let { FdroidEntity(packageName, it.authorName).also { fdroidDao.saveFdroidEntity(it) } } } suspend fun getBuildVersionInfo(packageName: String): List<BuildInfo>? { return fdroidApi.getFdroidInfoForPackage(packageName).body()?.builds suspend fun getBuildVersionInfo(packageName: String): List<BuildInfo> { return getFdroidApiResponse(packageName)?.body()?.builds ?: emptyList() } override suspend fun getAuthorName(application: Application): String { Loading Loading @@ -69,6 +74,17 @@ class FDroidRepository @Inject constructor( } override suspend fun isFdroidApplication(packageName: String): Boolean { return fdroidApi.getFdroidInfoForPackage(packageName).isSuccessful return getFdroidApiResponse(packageName)?.isSuccessful == true } private suspend fun getFdroidApiResponse(packageName: String): Response<FdroidApiModel>? { return try { fdroidApi.getFdroidInfoForPackage(packageName) } catch (exception: CancellationException) { throw exception } catch (exception: IOException) { Timber.w(exception, "Failed to fetch F-Droid metadata for %s", packageName) null } } } app/src/main/java/foundation/e/apps/data/fdroid/FdroidApiInterface.kt +1 −1 Original line number Diff line number Diff line Loading @@ -34,5 +34,5 @@ interface FdroidApiInterface { } @GET("{packageName}.yml") suspend fun getFdroidInfoForPackage(@Path("packageName") packageName: String): Response<FdroidApiModel?> suspend fun getFdroidInfoForPackage(@Path("packageName") packageName: String): Response<FdroidApiModel> } app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt +1 −1 Original line number Diff line number Diff line Loading @@ -389,7 +389,7 @@ class UpdatesManagerImpl @Inject constructor( // Received list has build info of the latest version at the bottom. // We want it at the top. val builds = handleNetworkResult { fDroidRepository.getBuildVersionInfo(packageName)?.asReversed() ?: listOf() fDroidRepository.getBuildVersionInfo(packageName).asReversed() }.data val matchingIndex = builds?.find { Loading app/src/test/java/foundation/e/apps/data/NetworkHandlerTest.kt +34 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test import java.io.InterruptedIOException import java.net.SocketTimeoutException @OptIn(ExperimentalCoroutinesApi::class) Loading @@ -33,6 +34,39 @@ class NetworkHandlerTest { assertThat(result.exception).isSameInstanceAs(timeout) } @Test fun handleNetworkResult_convertsInterruptedTimeout() = runTest { val timeout = InterruptedIOException("timeout") val result = handleNetworkResult<String> { throw timeout } assertThat(result.isTimeout()).isTrue() assertThat(result.message).isEqualTo("timeout Status: Timeout") assertThat(result.exception).isSameInstanceAs(timeout) } @Test fun handleNetworkResult_convertsInterruptedTimedOutMessage() = runTest { val timeout = InterruptedIOException("Read timed out") val result = handleNetworkResult<String> { throw timeout } assertThat(result.isTimeout()).isTrue() assertThat(result.message).isEqualTo("Read timed out Status: Timeout") assertThat(result.exception).isSameInstanceAs(timeout) } @Test fun handleNetworkResult_mapsNonTimeoutInterruptedIoExceptionToError() = runTest { val interrupted = InterruptedIOException("stream closed") val result = handleNetworkResult<String> { throw interrupted } assertThat(result.isUnknownError()).isTrue() assertThat(result.message).isEqualTo("stream closed Status: Unknown") assertThat(result.exception).isSameInstanceAs(interrupted) } @Test fun handleNetworkResult_mapsHttp429ToError() = runTest { val exception = GplayHttpRequestException( Loading Loading
app/src/main/java/foundation/e/apps/data/NetworkHandler.kt +22 −0 Original line number Diff line number Diff line Loading @@ -27,9 +27,11 @@ import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import timber.log.Timber import java.io.InterruptedIOException import java.net.SocketTimeoutException private const val TIMEOUT = "Timeout" private val TIMEOUT_MESSAGE_PATTERNS = listOf("timeout", "timed out") private const val UNKNOWN = "Unknown" private const val STATUS = "Status:" private const val ERROR_GPLAY_API = "Gplay api has faced error!" Loading @@ -49,6 +51,8 @@ suspend fun <T> handleNetworkResult(call: suspend () -> T): ResultSupreme<T> { throw e } catch (e: SocketTimeoutException) { handleSocketTimeoutException(e) } catch (e: InterruptedIOException) { handleInterruptedIOException(e) } catch (e: GplayHttpRequestException) { resultSupremeGplayHttpRequestException(e) } catch (e: Exception) { Loading @@ -63,6 +67,16 @@ private fun <T> handleSocketTimeoutException(e: SocketTimeoutException): ResultS return resultTimeout } private fun <T> handleInterruptedIOException(e: InterruptedIOException): ResultSupreme<T> { return if (isTimeoutInterruptedIOException(e)) { val resultTimeout = ResultSupreme.Timeout<T>(exception = e) resultTimeout.message = extractErrorMessage(e) resultTimeout } else { handleOthersException(e) } } private fun <T> resultSupremeGplayHttpRequestException(e: GplayHttpRequestException): ResultSupreme<T> { val message = extractErrorMessage(e) val exception = GPlayException(e.status == GPlayHttpClient.STATUS_CODE_TIMEOUT, message) Loading @@ -83,12 +97,20 @@ private fun extractErrorMessage(e: Exception): String { val status = when (e) { is GplayHttpRequestException -> e.status.toString() is SocketTimeoutException -> TIMEOUT is InterruptedIOException -> if (isTimeoutInterruptedIOException(e)) TIMEOUT else UNKNOWN else -> UNKNOWN } return (e.localizedMessage?.ifBlank { ERROR_GPLAY_API } ?: ERROR_GPLAY_API) + " $STATUS $status" } private fun isTimeoutInterruptedIOException(e: InterruptedIOException): Boolean { val message = e.message ?: return true return TIMEOUT_MESSAGE_PATTERNS.any { pattern -> message.contains(pattern, ignoreCase = true) } } suspend fun <T> retryWithBackoff(retryDelayInSeconds: Int = -1, operation: suspend () -> T): T? { var result: T? = null try { Loading
app/src/main/java/foundation/e/apps/data/fdroid/FDroidRepository.kt +20 −4 Original line number Diff line number Diff line Loading @@ -5,7 +5,12 @@ import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.cleanapk.ApkSignatureManager import foundation.e.apps.data.enums.Source import foundation.e.apps.data.fdroid.models.BuildInfo import foundation.e.apps.data.fdroid.models.FdroidApiModel import foundation.e.apps.data.fdroid.models.FdroidEntity import kotlinx.coroutines.CancellationException import retrofit2.Response import timber.log.Timber import java.io.IOException import javax.inject.Inject import javax.inject.Singleton Loading @@ -29,15 +34,15 @@ class FDroidRepository @Inject constructor( */ override suspend fun getFdroidInfo(packageName: String): FdroidEntity? { return fdroidDao.getFdroidEntityFromPackageName(packageName) ?: fdroidApi.getFdroidInfoForPackage(packageName).body()?.let { ?: getFdroidApiResponse(packageName)?.body()?.let { FdroidEntity(packageName, it.authorName).also { fdroidDao.saveFdroidEntity(it) } } } suspend fun getBuildVersionInfo(packageName: String): List<BuildInfo>? { return fdroidApi.getFdroidInfoForPackage(packageName).body()?.builds suspend fun getBuildVersionInfo(packageName: String): List<BuildInfo> { return getFdroidApiResponse(packageName)?.body()?.builds ?: emptyList() } override suspend fun getAuthorName(application: Application): String { Loading Loading @@ -69,6 +74,17 @@ class FDroidRepository @Inject constructor( } override suspend fun isFdroidApplication(packageName: String): Boolean { return fdroidApi.getFdroidInfoForPackage(packageName).isSuccessful return getFdroidApiResponse(packageName)?.isSuccessful == true } private suspend fun getFdroidApiResponse(packageName: String): Response<FdroidApiModel>? { return try { fdroidApi.getFdroidInfoForPackage(packageName) } catch (exception: CancellationException) { throw exception } catch (exception: IOException) { Timber.w(exception, "Failed to fetch F-Droid metadata for %s", packageName) null } } }
app/src/main/java/foundation/e/apps/data/fdroid/FdroidApiInterface.kt +1 −1 Original line number Diff line number Diff line Loading @@ -34,5 +34,5 @@ interface FdroidApiInterface { } @GET("{packageName}.yml") suspend fun getFdroidInfoForPackage(@Path("packageName") packageName: String): Response<FdroidApiModel?> suspend fun getFdroidInfoForPackage(@Path("packageName") packageName: String): Response<FdroidApiModel> }
app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt +1 −1 Original line number Diff line number Diff line Loading @@ -389,7 +389,7 @@ class UpdatesManagerImpl @Inject constructor( // Received list has build info of the latest version at the bottom. // We want it at the top. val builds = handleNetworkResult { fDroidRepository.getBuildVersionInfo(packageName)?.asReversed() ?: listOf() fDroidRepository.getBuildVersionInfo(packageName).asReversed() }.data val matchingIndex = builds?.find { Loading
app/src/test/java/foundation/e/apps/data/NetworkHandlerTest.kt +34 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test import java.io.InterruptedIOException import java.net.SocketTimeoutException @OptIn(ExperimentalCoroutinesApi::class) Loading @@ -33,6 +34,39 @@ class NetworkHandlerTest { assertThat(result.exception).isSameInstanceAs(timeout) } @Test fun handleNetworkResult_convertsInterruptedTimeout() = runTest { val timeout = InterruptedIOException("timeout") val result = handleNetworkResult<String> { throw timeout } assertThat(result.isTimeout()).isTrue() assertThat(result.message).isEqualTo("timeout Status: Timeout") assertThat(result.exception).isSameInstanceAs(timeout) } @Test fun handleNetworkResult_convertsInterruptedTimedOutMessage() = runTest { val timeout = InterruptedIOException("Read timed out") val result = handleNetworkResult<String> { throw timeout } assertThat(result.isTimeout()).isTrue() assertThat(result.message).isEqualTo("Read timed out Status: Timeout") assertThat(result.exception).isSameInstanceAs(timeout) } @Test fun handleNetworkResult_mapsNonTimeoutInterruptedIoExceptionToError() = runTest { val interrupted = InterruptedIOException("stream closed") val result = handleNetworkResult<String> { throw interrupted } assertThat(result.isUnknownError()).isTrue() assertThat(result.message).isEqualTo("stream closed Status: Unknown") assertThat(result.exception).isSameInstanceAs(interrupted) } @Test fun handleNetworkResult_mapsHttp429ToError() = runTest { val exception = GplayHttpRequestException( Loading