diff --git a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..78985de5fb13bfdcf071fe6dcce939a5abcb6ebe --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt @@ -0,0 +1,73 @@ +/* + * Copyright MURENA SAS 2023 + * Apps Quickly and easily install Android apps onto your device! + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.data + +import foundation.e.apps.data.gplay.utils.GPlayHttpClient +import foundation.e.apps.data.gplay.utils.GplayHttpRequestException +import foundation.e.apps.data.login.exceptions.GPlayException +import java.net.SocketTimeoutException + +private const val TIMEOUT = "Timeout" +private const val UNKNOWN = "Unknown" +private const val STATUS = "Status:" +private const val ERROR_GPLAY_API = "Gplay api has faced error!" + +suspend fun handleNetworkResult(call: suspend () -> T): ResultSupreme { + return try { + ResultSupreme.Success(call()) + } catch (e: SocketTimeoutException) { + handleSocketTimeoutException(e) + } catch (e: GplayHttpRequestException) { + resultSupremeGplayHttpRequestException(e) + } catch (e: Exception) { + handleOthersException(e) + } +} + +private fun handleSocketTimeoutException(e: SocketTimeoutException): ResultSupreme.Timeout { + val message = extractErrorMessage(e) + val resultTimeout = ResultSupreme.Timeout(exception = e) + resultTimeout.message = message + return resultTimeout +} + +private fun resultSupremeGplayHttpRequestException(e: GplayHttpRequestException): ResultSupreme { + val message = extractErrorMessage(e) + val exception = GPlayException(e.status == GPlayHttpClient.STATUS_CODE_TIMEOUT, message) + + return if (exception.isTimeout) { + ResultSupreme.Timeout(exception = exception) + } else { + ResultSupreme.Error(message, exception) + } +} + +private fun handleOthersException(e: Exception): ResultSupreme.Error { + val message = extractErrorMessage(e) + return ResultSupreme.Error(message, e) +} + +private fun extractErrorMessage(e: Exception): String { + val status = when (e) { + is GplayHttpRequestException -> e.status.toString() + is SocketTimeoutException -> TIMEOUT + else -> UNKNOWN + } + return (e.localizedMessage?.ifBlank { ERROR_GPLAY_API } ?: ERROR_GPLAY_API) + " $STATUS $status" +} diff --git a/app/src/main/java/foundation/e/apps/data/ResultSupreme.kt b/app/src/main/java/foundation/e/apps/data/ResultSupreme.kt index 14695702f15924b607ee449c8f66958bbe95ddad..a7a773f60ae33131d12c9ec6f68cae8adda48356 100644 --- a/app/src/main/java/foundation/e/apps/data/ResultSupreme.kt +++ b/app/src/main/java/foundation/e/apps/data/ResultSupreme.kt @@ -20,6 +20,8 @@ package foundation.e.apps.data import foundation.e.apps.data.enums.ResultStatus import java.util.concurrent.TimeoutException +private const val UNKNOWN_ERROR = "Unknown error!" + /** * Another implementation of Result class. * This removes the use of [ResultStatus] class for different status. @@ -52,10 +54,12 @@ sealed class ResultSupreme { * Example can be an empty list. * @param exception Optional exception from try-catch block. */ - class Timeout(data: T, exception: Exception = TimeoutException()) : + class Timeout(data: T? = null, exception: Exception = TimeoutException()) : ResultSupreme() { init { - setData(data) + data?.let { + setData(it) + } this.exception = exception } } @@ -119,6 +123,16 @@ sealed class ResultSupreme { this.data = data } + fun getResultStatus(): ResultStatus { + return when (this) { + is Success -> ResultStatus.OK + is Timeout -> ResultStatus.TIMEOUT + else -> ResultStatus.UNKNOWN.apply { + message = this@ResultSupreme.exception?.localizedMessage ?: UNKNOWN_ERROR + } + } + } + companion object { /** diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt index 43a505137c32b44c1c88c655cf7a8c55faf04e19..c924f27c7ad5412351e3f3f40ce9b58b530ddb12 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt @@ -48,6 +48,7 @@ import retrofit2.converter.moshi.MoshiConverterFactory import timber.log.Timber import java.net.ConnectException import java.util.Locale +import java.util.concurrent.TimeUnit import javax.inject.Named import javax.inject.Singleton @@ -55,6 +56,8 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object RetrofitModule { + private const val HTTP_TIMEOUT_IN_SECOND = 10L + /** * Provides an instance of Retrofit to work with CleanAPK API * @return instance of [CleanApkRetrofit] @@ -208,6 +211,7 @@ object RetrofitModule { fun provideOkHttpClient(cache: Cache, interceptor: Interceptor): OkHttpClient { return OkHttpClient.Builder() .addInterceptor(interceptor) + .callTimeout(HTTP_TIMEOUT_IN_SECOND, TimeUnit.SECONDS) .cache(cache) .build() } diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt index cb398a78c124c0e8ff6e03156570ac61a1ac0aae..e08bb6deda41e6b4cf580bedfabd29f15180e6e7 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt @@ -22,7 +22,6 @@ import android.content.Context import android.text.format.Formatter import androidx.lifecycle.LiveData import androidx.lifecycle.liveData -import androidx.lifecycle.map import com.aurora.gplayapi.Constants import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App @@ -63,8 +62,7 @@ import foundation.e.apps.data.fused.utils.CategoryType import foundation.e.apps.data.fused.utils.CategoryUtils import foundation.e.apps.data.fusedDownload.models.FusedDownload import foundation.e.apps.data.gplay.GplayStoreRepository -import foundation.e.apps.data.gplay.utils.GplayHttpRequestException -import foundation.e.apps.data.login.exceptions.GPlayException +import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.data.preference.PreferenceManagerModule import foundation.e.apps.install.pkg.PWAManagerModule import foundation.e.apps.install.pkg.PkgManagerModule @@ -73,11 +71,9 @@ import kotlinx.coroutines.Deferred import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.map import kotlinx.coroutines.withTimeout import retrofit2.Response import timber.log.Timber -import java.net.SocketTimeoutException import javax.inject.Inject import javax.inject.Named import javax.inject.Singleton @@ -100,8 +96,6 @@ class FusedApiImpl @Inject constructor( private const val CATEGORY_TITLE_REPLACEABLE_CONJUNCTION = "&" private const val CATEGORY_OPEN_GAMES_ID = "game_open_games" private const val CATEGORY_OPEN_GAMES_TITLE = "Open games" - private const val ERROR_GPLAY_SEARCH = "Gplay search has failed!" - private const val ERROR_GPLAY_SOURCE_NOT_SELECTED = "Gplay apps are not selected!" } /** @@ -167,29 +161,32 @@ class FusedApiImpl @Inject constructor( authData: AuthData, ): ResultSupreme> { - val apiStatus = when (source) { - Source.GPLAY -> runCodeWithTimeout({ + val result = when (source) { + Source.GPLAY -> handleNetworkResult> { priorList.addAll(fetchGPlayHome(authData)) - }) + priorList + } - Source.OPEN -> runCodeWithTimeout({ + Source.OPEN -> handleNetworkResult { val response = (cleanApkAppsRepository.getHomeScreenData() as Response).body() response?.home?.let { priorList.addAll(generateCleanAPKHome(it, APP_TYPE_OPEN)) } - }) + priorList + } - Source.PWA -> runCodeWithTimeout({ + Source.PWA -> handleNetworkResult { val response = (cleanApkPWARepository.getHomeScreenData() as Response).body() response?.home?.let { priorList.addAll(generateCleanAPKHome(it, APP_TYPE_PWA)) } - }) + priorList + } } - setHomeErrorMessage(apiStatus, source) + setHomeErrorMessage(result.getResultStatus(), source) priorList.sortByDescending { when (it.source) { APP_TYPE_OPEN -> 2 @@ -197,7 +194,7 @@ class FusedApiImpl @Inject constructor( else -> 3 } } - return ResultSupreme.create(apiStatus, priorList) + return ResultSupreme.create(result.getResultStatus(), priorList) } private fun setHomeErrorMessage(apiStatus: ResultStatus, source: Source) { @@ -289,7 +286,7 @@ class FusedApiImpl @Inject constructor( packageSpecificResults: ArrayList ): ResultSupreme, Boolean>> { val pwaApps: MutableList = mutableListOf() - val status = runCodeWithTimeout({ + val result = handleNetworkResult { val apps = cleanApkPWARepository.getSearchResult(query).body()?.apps apps?.apply { @@ -297,14 +294,14 @@ class FusedApiImpl @Inject constructor( pwaApps.addAll(this) } } - }) + } - if (pwaApps.isNotEmpty() || status != ResultStatus.OK) { + if (pwaApps.isNotEmpty() || result.getResultStatus() != ResultStatus.OK) { searchResult.addAll(pwaApps) } return ResultSupreme.create( - status, + result.getResultStatus(), Pair( filterWithKeywordSearch( searchResult, @@ -322,16 +319,17 @@ class FusedApiImpl @Inject constructor( searchResult: MutableList, packageSpecificResults: ArrayList ): ResultSupreme, Boolean>> { - val status = runCodeWithTimeout({ + val result = handleNetworkResult { cleanApkResults.addAll(getCleanAPKSearchResults(query)) - }) + cleanApkResults + } if (cleanApkResults.isNotEmpty()) { searchResult.addAll(cleanApkResults) } return ResultSupreme.create( - status, + result.getResultStatus(), Pair( filterWithKeywordSearch( searchResult, @@ -351,7 +349,7 @@ class FusedApiImpl @Inject constructor( var gplayPackageResult: FusedApp? = null var cleanapkPackageResult: FusedApp? = null - val status = runCodeWithTimeout({ + val result = handleNetworkResult { if (preferenceManagerModule.isGplaySelected()) { gplayPackageResult = getGplayPackagResult(query, authData) } @@ -359,7 +357,7 @@ class FusedApiImpl @Inject constructor( if (preferenceManagerModule.isOpenSourceSelected()) { cleanapkPackageResult = getCleanApkPackageResult(query) } - }) + } /* * Currently only show open source package result if exists in both fdroid and gplay. @@ -378,10 +376,13 @@ class FusedApiImpl @Inject constructor( * If there was a timeout, return it and don't try to fetch anything else. * Also send true in the pair to signal more results being loaded. */ - if (status != ResultStatus.OK) { - return ResultSupreme.create(status, Pair(packageSpecificResults, false)) + if (result.getResultStatus() != ResultStatus.OK) { + return ResultSupreme.create( + result.getResultStatus(), + Pair(packageSpecificResults, false) + ) } - return ResultSupreme.create(status, Pair(packageSpecificResults, true)) + return ResultSupreme.create(result.getResultStatus(), Pair(packageSpecificResults, true)) } /* @@ -446,7 +447,7 @@ class FusedApiImpl @Inject constructor( */ private suspend fun getCleanapkSearchResult(packageName: String): ResultSupreme { var fusedApp = FusedApp() - val status = runCodeWithTimeout({ + val result = handleNetworkResult { val result = cleanApkAppsRepository.getSearchResult( packageName, "package_name" @@ -455,15 +456,15 @@ class FusedApiImpl @Inject constructor( if (result?.apps?.isNotEmpty() == true && result.numberOfResults == 1) { fusedApp = result.apps[0] } - }) - return ResultSupreme.create(status, fusedApp) + } + return ResultSupreme.create(result.getResultStatus(), fusedApp) } override suspend fun getSearchSuggestions(query: String): List { var searchSuggesions = listOf() - runCodeWithTimeout({ + handleNetworkResult { searchSuggesions = gplayRepository.getSearchSuggestions(query) - }) + } return searchSuggesions } @@ -523,7 +524,7 @@ class FusedApiImpl @Inject constructor( override suspend fun getPWAApps(category: String): ResultSupreme, String>> { val list = mutableListOf() - val status = runCodeWithTimeout({ + val result = handleNetworkResult { val response = getPWAAppsResponse(category) response?.apps?.forEach { it.updateStatus() @@ -531,13 +532,13 @@ class FusedApiImpl @Inject constructor( it.updateFilterLevel(null) list.add(it) } - }) - return ResultSupreme.create(status, Pair(list, "")) + } + return ResultSupreme.create(result.getResultStatus(), Pair(list, "")) } override suspend fun getOpenSourceApps(category: String): ResultSupreme, String>> { val list = mutableListOf() - val status = runCodeWithTimeout({ + val result = handleNetworkResult { val response = getOpenSourceAppsResponse(category) response?.apps?.forEach { it.updateStatus() @@ -545,8 +546,8 @@ class FusedApiImpl @Inject constructor( it.updateFilterLevel(null) list.add(it) } - }) - return ResultSupreme.create(status, Pair(list, "")) + } + return ResultSupreme.create(result.getResultStatus(), Pair(list, "")) } /* @@ -557,7 +558,7 @@ class FusedApiImpl @Inject constructor( */ override suspend fun getCleanapkAppDetails(packageName: String): Pair { var fusedApp = FusedApp() - val status = runCodeWithTimeout({ + val result = handleNetworkResult { val result = cleanApkAppsRepository.getSearchResult( packageName, "package_name" @@ -569,8 +570,8 @@ class FusedApiImpl @Inject constructor( ?: FusedApp() } fusedApp.updateFilterLevel(null) - }) - return Pair(fusedApp, status) + } + return Pair(fusedApp, result.getResultStatus()) } override suspend fun getApplicationDetails( @@ -614,7 +615,7 @@ class FusedApiImpl @Inject constructor( * i.e. check timeout for individual package query. */ for (packageName in packageNameList) { - status = runCodeWithTimeout({ + val result = handleNetworkResult { cleanApkAppsRepository.getSearchResult( packageName, "package_name" @@ -627,7 +628,9 @@ class FusedApiImpl @Inject constructor( ) } } - }) + } + + status = result.getResultStatus() /* * If status is not ok, immediately return. @@ -653,7 +656,7 @@ class FusedApiImpl @Inject constructor( /* * Old code moved from getApplicationDetails() */ - val status = runCodeWithTimeout({ + val result = handleNetworkResult { gplayRepository.getAppsDetails(packageNameList).forEach { app -> /* * Some apps are restricted to locations. Example "com.skype.m2". @@ -670,9 +673,9 @@ class FusedApiImpl @Inject constructor( ) } } - }) + } - return Pair(fusedAppList, status) + return Pair(fusedAppList, result.getResultStatus()) } /** @@ -691,7 +694,7 @@ class FusedApiImpl @Inject constructor( appList: List, ): ResultSupreme> { val filteredFusedApps = mutableListOf() - val status = runCodeWithTimeout({ + return handleNetworkResult { appList.forEach { val filter = getAppFilterLevel(it, authData) if (filter.isUnFiltered()) { @@ -702,9 +705,8 @@ class FusedApiImpl @Inject constructor( ) } } - }) - - return ResultSupreme.create(status, filteredFusedApps) + filteredFusedApps + } } /** @@ -783,7 +785,7 @@ class FusedApiImpl @Inject constructor( var response: FusedApp? = null - val status = runCodeWithTimeout({ + val result = handleNetworkResult { response = if (origin == Origin.CLEANAPK) { (cleanApkAppsRepository.getAppDetails(id) as Response).body()?.app } else { @@ -796,9 +798,10 @@ class FusedApiImpl @Inject constructor( it.updateSource() it.updateFilterLevel(authData) } - }) + response + } - return Pair(response ?: FusedApp(), status) + return Pair(result.data ?: FusedApp(), result.getResultStatus()) } /* @@ -836,9 +839,9 @@ class FusedApiImpl @Inject constructor( val gplayCategoryResult = fetchGplayCategories( type, ) - categoriesList.addAll(gplayCategoryResult.second) - apiStatus = gplayCategoryResult.first - errorApplicationCategory = gplayCategoryResult.third + categoriesList.addAll(gplayCategoryResult.data ?: listOf()) + apiStatus = gplayCategoryResult.getResultStatus() + errorApplicationCategory = APP_TYPE_ANY } return Pair(apiStatus, errorApplicationCategory) @@ -846,34 +849,25 @@ class FusedApiImpl @Inject constructor( private suspend fun fetchGplayCategories( type: CategoryType, - ): Triple, String> { - var errorApplicationCategory = "" - var apiStatus = ResultStatus.OK + ): ResultSupreme> { val categoryList = mutableListOf() - runCodeWithTimeout({ + + return handleNetworkResult { val playResponse = gplayRepository.getCategories(type).map { app -> val category = app.transformToFusedCategory() updateCategoryDrawable(category) category } categoryList.addAll(playResponse) - }, { - errorApplicationCategory = APP_TYPE_ANY - apiStatus = ResultStatus.TIMEOUT - }, { - errorApplicationCategory = APP_TYPE_ANY - apiStatus = ResultStatus.UNKNOWN - }) - return Triple(apiStatus, categoryList, errorApplicationCategory) + categoryList + } } private suspend fun fetchPWACategories( type: CategoryType, ): Triple, String> { - var errorApplicationCategory = "" - var apiStatus: ResultStatus = ResultStatus.OK val fusedCategoriesList = mutableListOf() - runCodeWithTimeout({ + val result = handleNetworkResult { getPWAsCategories()?.let { fusedCategoriesList.addAll( getFusedCategoryBasedOnCategoryType( @@ -881,23 +875,16 @@ class FusedApiImpl @Inject constructor( ) ) } - }, { - errorApplicationCategory = APP_TYPE_PWA - apiStatus = ResultStatus.TIMEOUT - }, { - errorApplicationCategory = APP_TYPE_PWA - apiStatus = ResultStatus.UNKNOWN - }) - return Triple(apiStatus, fusedCategoriesList, errorApplicationCategory) + } + + return Triple(result.getResultStatus(), fusedCategoriesList, APP_TYPE_PWA) } private suspend fun fetchOpenSourceCategories( type: CategoryType, ): Triple, String> { - var errorApplicationCategory = "" - var apiStatus: ResultStatus = ResultStatus.OK val fusedCategoryList = mutableListOf() - runCodeWithTimeout({ + val result = handleNetworkResult { getOpenSourceCategories()?.let { fusedCategoryList.addAll( getFusedCategoryBasedOnCategoryType( @@ -907,14 +894,9 @@ class FusedApiImpl @Inject constructor( ) ) } - }, { - errorApplicationCategory = APP_TYPE_OPEN - apiStatus = ResultStatus.TIMEOUT - }, { - errorApplicationCategory = APP_TYPE_OPEN - apiStatus = ResultStatus.UNKNOWN - }) - return Triple(apiStatus, fusedCategoryList, errorApplicationCategory) + } + + return Triple(result.getResultStatus(), fusedCategoryList, APP_TYPE_OPEN) } /** @@ -956,9 +938,8 @@ class FusedApiImpl @Inject constructor( } private fun getCategoryIconName(category: FusedCategory): String { - var categoryTitle = if (category.tag.getOperationalTag() - .contentEquals(AppTag.GPlay().getOperationalTag()) - ) category.id else category.title + var categoryTitle = if (category.tag.getOperationalTag().contentEquals(AppTag.GPlay().getOperationalTag())) + category.id else category.title if (categoryTitle.contains(CATEGORY_TITLE_REPLACEABLE_CONJUNCTION)) { categoryTitle = categoryTitle.replace(CATEGORY_TITLE_REPLACEABLE_CONJUNCTION, "and") @@ -1082,12 +1063,12 @@ class FusedApiImpl @Inject constructor( query: String, nextPageSubBundle: Set? ): GplaySearchResult { - try { + return handleNetworkResult { val searchResults = gplayRepository.getSearchResult(query, nextPageSubBundle?.toMutableSet()) if (!preferenceManagerModule.isGplaySelected()) { - return ResultSupreme.Error(ERROR_GPLAY_SOURCE_NOT_SELECTED) + return@handleNetworkResult Pair(listOf(), setOf()) } val fusedAppList = @@ -1097,20 +1078,7 @@ class FusedApiImpl @Inject constructor( fusedAppList.add(FusedApp(isPlaceHolder = true)) } - return ResultSupreme.Success(Pair(fusedAppList.toList(), searchResults.second.toSet())) - } catch (e: GplayHttpRequestException) { - val message = ( - e.localizedMessage?.ifBlank { ERROR_GPLAY_SEARCH } - ?: ERROR_GPLAY_SEARCH - ) + "Status: ${e.status}" - - val exception = GPlayException(e.status == 408, message) - return ResultSupreme.Error(message, exception) - } catch (e: Exception) { - val exception = - GPlayException(e is SocketTimeoutException, e.localizedMessage) - - return ResultSupreme.Error(e.localizedMessage ?: "", exception) + return@handleNetworkResult Pair(fusedAppList.toList(), searchResults.second.toSet()) } } @@ -1416,7 +1384,7 @@ class FusedApiImpl @Inject constructor( var fusedAppList: MutableList = mutableListOf() var nextPageUrl = "" - val status = runCodeWithTimeout({ + return handleNetworkResult { val streamCluster = gplayRepository.getAppsByCategory(category, pageUrl) as StreamCluster @@ -1429,8 +1397,7 @@ class FusedApiImpl @Inject constructor( if (!nextPageUrl.isNullOrEmpty()) { fusedAppList.add(FusedApp(isPlaceHolder = true)) } - }) - - return ResultSupreme.create(status, Pair(fusedAppList, nextPageUrl)) + Pair(fusedAppList, nextPageUrl) + } } } diff --git a/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt b/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt index 839d7c168229231301718c71f0d6289a27dd0abf..38fb0ad2277b2460d6531b276f3f8b1705781cf6 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt @@ -40,7 +40,6 @@ import okhttp3.Response import timber.log.Timber import java.io.IOException import java.net.SocketTimeoutException -import java.net.UnknownHostException import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -58,6 +57,7 @@ class GPlayHttpClient @Inject constructor( private const val SEARCH_SUGGEST = "searchSuggest" private const val STATUS_CODE_UNAUTHORIZED = 401 private const val STATUS_CODE_TOO_MANY_REQUESTS = 429 + const val STATUS_CODE_TIMEOUT = 408 } private val okHttpClient = OkHttpClient().newBuilder() @@ -163,32 +163,16 @@ class GPlayHttpClient @Inject constructor( val call = okHttpClient.newCall(request) response = call.execute() buildPlayResponse(response) + } catch (e: GplayHttpRequestException) { + throw e } catch (e: Exception) { - // TODO: exception will be thrown for all apis when all gplay api implementation - // will handle the exceptions. this will be done in following issue. - // Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/1483 - if (request.url.toString().contains(SEARCH)) { - throw e - } - - when (e) { - is UnknownHostException, - is SocketTimeoutException -> handleExceptionOnGooglePlayRequest(e) - - else -> handleExceptionOnGooglePlayRequest(e) - } + val status = if (e is SocketTimeoutException) STATUS_CODE_TIMEOUT else -1 + throw GplayHttpRequestException(status, e.localizedMessage ?: "") } finally { response?.close() } } - private fun handleExceptionOnGooglePlayRequest(e: Exception): PlayResponse { - Timber.e("processRequest: ${e.localizedMessage}") - return PlayResponse().apply { - errorString = "${this@GPlayHttpClient::class.java.simpleName}: ${e.localizedMessage}" - } - } - private fun buildUrl(url: String, params: Map): HttpUrl { val urlBuilder = url.toHttpUrl().newBuilder() params.forEach { @@ -222,10 +206,7 @@ class GPlayHttpClient @Inject constructor( } } - // TODO: exception will be thrown for all apis when all gplay api implementation - // will handle the exceptions. this will be done in following issue. - // Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/1483 - if (response.request.url.toString().contains(SEARCH) && code != 200) { + if (code != 200) { throw GplayHttpRequestException(code, response.message) } diff --git a/app/src/main/java/foundation/e/apps/data/login/api/LoginApiRepository.kt b/app/src/main/java/foundation/e/apps/data/login/api/LoginApiRepository.kt index 5d97b64870aa92b261af2e29cf8470a2bc47d110..b2fbd026c9919c4b62a0e97c0736fa367b786048 100644 --- a/app/src/main/java/foundation/e/apps/data/login/api/LoginApiRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/login/api/LoginApiRepository.kt @@ -19,13 +19,11 @@ package foundation.e.apps.data.login.api import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.PlayResponse -import foundation.e.apps.data.Constants.timeoutDurationInMillis import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.User import foundation.e.apps.data.gplay.utils.AC2DMUtil +import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.data.login.exceptions.GPlayLoginException -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.withTimeout import java.util.Locale /** @@ -52,9 +50,9 @@ class LoginApiRepository constructor( * else blank for Anonymous login. */ suspend fun fetchAuthData(email: String, aasToken: String, locale: Locale): ResultSupreme { - val result = runCodeWithTimeout({ + val result = handleNetworkResult { gPlayLoginInterface.fetchAuthData(email, aasToken) - }) + } return result.apply { this.data?.locale = locale this.exception = when (result) { @@ -76,13 +74,13 @@ class LoginApiRepository constructor( */ suspend fun login(authData: AuthData): ResultSupreme { var response = PlayResponse() - val result = runCodeWithTimeout({ + val result = handleNetworkResult { response = gPlayLoginInterface.login(authData) if (response.code != 200) { throw Exception("Validation network code: ${response.code}") } response - }) + } return ResultSupreme.replicate(result, response).apply { this.exception = when (result) { is ResultSupreme.Timeout -> GPlayLoginException(true, "GPlay API timeout", user) @@ -109,8 +107,8 @@ class LoginApiRepository constructor( googleLoginApi: GoogleLoginApi, email: String, oauthToken: String - ): ResultSupreme { - val result = runCodeWithTimeout({ + ): ResultSupreme { + val result = handleNetworkResult { var aasToken = "" val response = googleLoginApi.getAC2DMResponse(email, oauthToken) var error = response.errorString @@ -129,7 +127,7 @@ class LoginApiRepository constructor( throw Exception(error) } aasToken - }) + } return result.apply { this.exception = when (result) { is ResultSupreme.Timeout -> GPlayLoginException(true, "GPlay API timeout", User.GOOGLE) @@ -138,26 +136,4 @@ class LoginApiRepository constructor( } } } - - /** - * Utility method to run a specified code block in a fixed amount of time. - */ - private suspend fun runCodeWithTimeout( - block: suspend () -> T, - timeoutBlock: (() -> T?)? = null, - exceptionBlock: ((e: Exception) -> T?)? = null, - ): ResultSupreme { - return try { - withTimeout(timeoutDurationInMillis) { - return@withTimeout ResultSupreme.Success(block()) - } - } catch (e: TimeoutCancellationException) { - ResultSupreme.Timeout(timeoutBlock?.invoke()).apply { - message = e.message ?: "" - } - } catch (e: Exception) { - e.printStackTrace() - ResultSupreme.Error(exceptionBlock?.invoke(e), message = e.message ?: "") - } - } } diff --git a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListViewModel.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListViewModel.kt index 79c8cc52d37bbb50296571b448cd0093d120a758..86be6867d85fb6e9970d9bbdec2faf078de10a32 100644 --- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListViewModel.kt @@ -42,9 +42,11 @@ class ApplicationListViewModel @Inject constructor( val appListLiveData: MutableLiveData>?> = MutableLiveData() - var isLoading = false + private var isLoading = false - var nextPageUrl: String? = null + private var nextPageUrl: String? = null + + private var currentAuthListObject: List? = null fun loadData( category: String, @@ -54,11 +56,18 @@ class ApplicationListViewModel @Inject constructor( ) { super.onLoadData(authObjectList, { successAuthList, _ -> - if (appListLiveData.value?.data?.isNotEmpty() == true) { + // if token is refreshed, then reset all data + if (currentAuthListObject != null && currentAuthListObject != authObjectList) { + appListLiveData.postValue(ResultSupreme.Success(emptyList())) + nextPageUrl = null + } + + if (appListLiveData.value?.data?.isNotEmpty() == true && currentAuthListObject == authObjectList) { appListLiveData.postValue(appListLiveData.value) return@onLoadData } + this.currentAuthListObject = authObjectList successAuthList.find { it is AuthObject.GPlayAuth }?.run { getList(category, result.data!! as AuthData, source) return@onLoadData @@ -163,18 +172,9 @@ class ApplicationListViewModel @Inject constructor( private fun appendAppList(it: Pair, String>): List? { val currentAppList = appListLiveData.value?.data?.toMutableList() currentAppList?.removeIf { item -> item.isPlaceHolder } - val appList = currentAppList?.plus(it.first) - return appList + return currentAppList?.plus(it.first) } - /** - * @return returns true if there is changes in data, otherwise false - */ - fun isAnyAppUpdated( - newFusedApps: List, - oldFusedApps: List - ) = fusedAPIRepository.isAnyFusedAppUpdated(newFusedApps, oldFusedApps) - fun hasAnyAppInstallStatusChanged(currentList: List) = fusedAPIRepository.isAnyAppInstallStatusChanged(currentList) } diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 6a2e807747c86fcebf1f983a67549c602ace0207..a2bc710807f7e60f1d9d0e8e69d45bbf02b466b8 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -158,7 +158,7 @@ class SearchViewModel @Inject constructor( nextSubBundle = gplaySearchResult.data?.second // first page has less data, then fetch next page data without waiting for users' scroll - if (isFirstFetch) { + if (isFirstFetch && gplaySearchResult.isSuccess()) { CoroutineScope(coroutineContext).launch { fetchGplayData(query) }