Loading app/src/main/java/foundation/e/apps/data/NetworkHandler.kt 0 → 100644 +73 −0 Original line number Diff line number Diff line /* * 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 <https://www.gnu.org/licenses/>. */ 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 <T> handleNetworkResult(call: suspend () -> T): ResultSupreme<T> { return try { ResultSupreme.Success(call()) } catch (e: SocketTimeoutException) { handleSocketTimeoutException(e) } catch (e: GplayHttpRequestException) { resultSupremeGplayHttpRequestException(e) } catch (e: Exception) { handleOthersException(e) } } private fun <T> handleSocketTimeoutException(e: SocketTimeoutException): ResultSupreme.Timeout<T> { val message = extractErrorMessage(e) val resultTimeout = ResultSupreme.Timeout<T>(exception = e) resultTimeout.message = message return resultTimeout } private fun <T> resultSupremeGplayHttpRequestException(e: GplayHttpRequestException): ResultSupreme<T> { 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 <T> handleOthersException(e: Exception): ResultSupreme.Error<T> { 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" } app/src/main/java/foundation/e/apps/data/ResultSupreme.kt +16 −2 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -52,10 +54,12 @@ sealed class ResultSupreme<T> { * Example can be an empty list. * @param exception Optional exception from try-catch block. */ class Timeout<T>(data: T, exception: Exception = TimeoutException()) : class Timeout<T>(data: T? = null, exception: Exception = TimeoutException()) : ResultSupreme<T>() { init { setData(data) data?.let { setData(it) } this.exception = exception } } Loading Loading @@ -119,6 +123,16 @@ sealed class ResultSupreme<T> { 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 { /** Loading app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt +4 −0 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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] Loading Loading @@ -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() } Loading app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt +81 −114 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading @@ -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 Loading @@ -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!" } /** Loading Loading @@ -167,29 +161,32 @@ class FusedApiImpl @Inject constructor( authData: AuthData, ): ResultSupreme<List<FusedHome>> { val apiStatus = when (source) { Source.GPLAY -> runCodeWithTimeout({ val result = when (source) { Source.GPLAY -> handleNetworkResult<List<FusedHome>> { priorList.addAll(fetchGPlayHome(authData)) }) priorList } Source.OPEN -> runCodeWithTimeout({ Source.OPEN -> handleNetworkResult { val response = (cleanApkAppsRepository.getHomeScreenData() as Response<HomeScreen>).body() response?.home?.let { priorList.addAll(generateCleanAPKHome(it, APP_TYPE_OPEN)) } }) priorList } Source.PWA -> runCodeWithTimeout({ Source.PWA -> handleNetworkResult { val response = (cleanApkPWARepository.getHomeScreenData() as Response<HomeScreen>).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 Loading @@ -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) { Loading Loading @@ -289,7 +286,7 @@ class FusedApiImpl @Inject constructor( packageSpecificResults: ArrayList<FusedApp> ): ResultSupreme<Pair<List<FusedApp>, Boolean>> { val pwaApps: MutableList<FusedApp> = mutableListOf() val status = runCodeWithTimeout({ val result = handleNetworkResult { val apps = cleanApkPWARepository.getSearchResult(query).body()?.apps apps?.apply { Loading @@ -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, Loading @@ -322,16 +319,17 @@ class FusedApiImpl @Inject constructor( searchResult: MutableList<FusedApp>, packageSpecificResults: ArrayList<FusedApp> ): ResultSupreme<Pair<List<FusedApp>, 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, Loading @@ -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) } Loading @@ -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. Loading @@ -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)) } /* Loading Loading @@ -446,7 +447,7 @@ class FusedApiImpl @Inject constructor( */ private suspend fun getCleanapkSearchResult(packageName: String): ResultSupreme<FusedApp> { var fusedApp = FusedApp() val status = runCodeWithTimeout({ val result = handleNetworkResult { val result = cleanApkAppsRepository.getSearchResult( packageName, "package_name" Loading @@ -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<SearchSuggestEntry> { var searchSuggesions = listOf<SearchSuggestEntry>() runCodeWithTimeout({ handleNetworkResult { searchSuggesions = gplayRepository.getSearchSuggestions(query) }) } return searchSuggesions } Loading Loading @@ -523,7 +524,7 @@ class FusedApiImpl @Inject constructor( override suspend fun getPWAApps(category: String): ResultSupreme<Pair<List<FusedApp>, String>> { val list = mutableListOf<FusedApp>() val status = runCodeWithTimeout({ val result = handleNetworkResult { val response = getPWAAppsResponse(category) response?.apps?.forEach { it.updateStatus() Loading @@ -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<Pair<List<FusedApp>, String>> { val list = mutableListOf<FusedApp>() val status = runCodeWithTimeout({ val result = handleNetworkResult { val response = getOpenSourceAppsResponse(category) response?.apps?.forEach { it.updateStatus() Loading @@ -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, "")) } /* Loading @@ -557,7 +558,7 @@ class FusedApiImpl @Inject constructor( */ override suspend fun getCleanapkAppDetails(packageName: String): Pair<FusedApp, ResultStatus> { var fusedApp = FusedApp() val status = runCodeWithTimeout({ val result = handleNetworkResult { val result = cleanApkAppsRepository.getSearchResult( packageName, "package_name" Loading @@ -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( Loading Loading @@ -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" Loading @@ -627,7 +628,9 @@ class FusedApiImpl @Inject constructor( ) } } }) } status = result.getResultStatus() /* * If status is not ok, immediately return. Loading @@ -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". Loading @@ -670,9 +673,9 @@ class FusedApiImpl @Inject constructor( ) } } }) } return Pair(fusedAppList, status) return Pair(fusedAppList, result.getResultStatus()) } /** Loading @@ -691,7 +694,7 @@ class FusedApiImpl @Inject constructor( appList: List<App>, ): ResultSupreme<List<FusedApp>> { val filteredFusedApps = mutableListOf<FusedApp>() val status = runCodeWithTimeout({ return handleNetworkResult { appList.forEach { val filter = getAppFilterLevel(it, authData) if (filter.isUnFiltered()) { Loading @@ -702,9 +705,8 @@ class FusedApiImpl @Inject constructor( ) } } }) return ResultSupreme.create(status, filteredFusedApps) filteredFusedApps } } /** Loading Loading @@ -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<Application>).body()?.app } else { Loading @@ -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()) } /* Loading Loading @@ -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) Loading @@ -846,34 +849,25 @@ class FusedApiImpl @Inject constructor( private suspend fun fetchGplayCategories( type: CategoryType, ): Triple<ResultStatus, List<FusedCategory>, String> { var errorApplicationCategory = "" var apiStatus = ResultStatus.OK ): ResultSupreme<List<FusedCategory>> { val categoryList = mutableListOf<FusedCategory>() 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<ResultStatus, List<FusedCategory>, String> { var errorApplicationCategory = "" var apiStatus: ResultStatus = ResultStatus.OK val fusedCategoriesList = mutableListOf<FusedCategory>() runCodeWithTimeout({ val result = handleNetworkResult { getPWAsCategories()?.let { fusedCategoriesList.addAll( getFusedCategoryBasedOnCategoryType( Loading @@ -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<ResultStatus, List<FusedCategory>, String> { var errorApplicationCategory = "" var apiStatus: ResultStatus = ResultStatus.OK val fusedCategoryList = mutableListOf<FusedCategory>() runCodeWithTimeout({ val result = handleNetworkResult { getOpenSourceCategories()?.let { fusedCategoryList.addAll( getFusedCategoryBasedOnCategoryType( Loading @@ -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) } /** Loading Loading @@ -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") Loading Loading @@ -1082,12 +1063,12 @@ class FusedApiImpl @Inject constructor( query: String, nextPageSubBundle: Set<SearchBundle.SubBundle>? ): 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<FusedApp>(), setOf<SearchBundle.SubBundle>()) } val fusedAppList = Loading @@ -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()) } } Loading Loading @@ -1416,7 +1384,7 @@ class FusedApiImpl @Inject constructor( var fusedAppList: MutableList<FusedApp> = mutableListOf() var nextPageUrl = "" val status = runCodeWithTimeout({ return handleNetworkResult { val streamCluster = gplayRepository.getAppsByCategory(category, pageUrl) as StreamCluster Loading @@ -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) } } } app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt +6 −25 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() Loading Loading @@ -163,32 +163,16 @@ class GPlayHttpClient @Inject constructor( val call = okHttpClient.newCall(request) response = call.execute() buildPlayResponse(response) } 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)) { } catch (e: GplayHttpRequestException) { throw e } when (e) { is UnknownHostException, is SocketTimeoutException -> handleExceptionOnGooglePlayRequest(e) else -> handleExceptionOnGooglePlayRequest(e) } } catch (e: Exception) { 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<String, String>): HttpUrl { val urlBuilder = url.toHttpUrl().newBuilder() params.forEach { Loading Loading @@ -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) } Loading Loading
app/src/main/java/foundation/e/apps/data/NetworkHandler.kt 0 → 100644 +73 −0 Original line number Diff line number Diff line /* * 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 <https://www.gnu.org/licenses/>. */ 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 <T> handleNetworkResult(call: suspend () -> T): ResultSupreme<T> { return try { ResultSupreme.Success(call()) } catch (e: SocketTimeoutException) { handleSocketTimeoutException(e) } catch (e: GplayHttpRequestException) { resultSupremeGplayHttpRequestException(e) } catch (e: Exception) { handleOthersException(e) } } private fun <T> handleSocketTimeoutException(e: SocketTimeoutException): ResultSupreme.Timeout<T> { val message = extractErrorMessage(e) val resultTimeout = ResultSupreme.Timeout<T>(exception = e) resultTimeout.message = message return resultTimeout } private fun <T> resultSupremeGplayHttpRequestException(e: GplayHttpRequestException): ResultSupreme<T> { 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 <T> handleOthersException(e: Exception): ResultSupreme.Error<T> { 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" }
app/src/main/java/foundation/e/apps/data/ResultSupreme.kt +16 −2 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -52,10 +54,12 @@ sealed class ResultSupreme<T> { * Example can be an empty list. * @param exception Optional exception from try-catch block. */ class Timeout<T>(data: T, exception: Exception = TimeoutException()) : class Timeout<T>(data: T? = null, exception: Exception = TimeoutException()) : ResultSupreme<T>() { init { setData(data) data?.let { setData(it) } this.exception = exception } } Loading Loading @@ -119,6 +123,16 @@ sealed class ResultSupreme<T> { 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 { /** Loading
app/src/main/java/foundation/e/apps/data/cleanapk/RetrofitModule.kt +4 −0 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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] Loading Loading @@ -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() } Loading
app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt +81 −114 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading @@ -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 Loading @@ -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!" } /** Loading Loading @@ -167,29 +161,32 @@ class FusedApiImpl @Inject constructor( authData: AuthData, ): ResultSupreme<List<FusedHome>> { val apiStatus = when (source) { Source.GPLAY -> runCodeWithTimeout({ val result = when (source) { Source.GPLAY -> handleNetworkResult<List<FusedHome>> { priorList.addAll(fetchGPlayHome(authData)) }) priorList } Source.OPEN -> runCodeWithTimeout({ Source.OPEN -> handleNetworkResult { val response = (cleanApkAppsRepository.getHomeScreenData() as Response<HomeScreen>).body() response?.home?.let { priorList.addAll(generateCleanAPKHome(it, APP_TYPE_OPEN)) } }) priorList } Source.PWA -> runCodeWithTimeout({ Source.PWA -> handleNetworkResult { val response = (cleanApkPWARepository.getHomeScreenData() as Response<HomeScreen>).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 Loading @@ -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) { Loading Loading @@ -289,7 +286,7 @@ class FusedApiImpl @Inject constructor( packageSpecificResults: ArrayList<FusedApp> ): ResultSupreme<Pair<List<FusedApp>, Boolean>> { val pwaApps: MutableList<FusedApp> = mutableListOf() val status = runCodeWithTimeout({ val result = handleNetworkResult { val apps = cleanApkPWARepository.getSearchResult(query).body()?.apps apps?.apply { Loading @@ -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, Loading @@ -322,16 +319,17 @@ class FusedApiImpl @Inject constructor( searchResult: MutableList<FusedApp>, packageSpecificResults: ArrayList<FusedApp> ): ResultSupreme<Pair<List<FusedApp>, 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, Loading @@ -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) } Loading @@ -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. Loading @@ -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)) } /* Loading Loading @@ -446,7 +447,7 @@ class FusedApiImpl @Inject constructor( */ private suspend fun getCleanapkSearchResult(packageName: String): ResultSupreme<FusedApp> { var fusedApp = FusedApp() val status = runCodeWithTimeout({ val result = handleNetworkResult { val result = cleanApkAppsRepository.getSearchResult( packageName, "package_name" Loading @@ -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<SearchSuggestEntry> { var searchSuggesions = listOf<SearchSuggestEntry>() runCodeWithTimeout({ handleNetworkResult { searchSuggesions = gplayRepository.getSearchSuggestions(query) }) } return searchSuggesions } Loading Loading @@ -523,7 +524,7 @@ class FusedApiImpl @Inject constructor( override suspend fun getPWAApps(category: String): ResultSupreme<Pair<List<FusedApp>, String>> { val list = mutableListOf<FusedApp>() val status = runCodeWithTimeout({ val result = handleNetworkResult { val response = getPWAAppsResponse(category) response?.apps?.forEach { it.updateStatus() Loading @@ -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<Pair<List<FusedApp>, String>> { val list = mutableListOf<FusedApp>() val status = runCodeWithTimeout({ val result = handleNetworkResult { val response = getOpenSourceAppsResponse(category) response?.apps?.forEach { it.updateStatus() Loading @@ -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, "")) } /* Loading @@ -557,7 +558,7 @@ class FusedApiImpl @Inject constructor( */ override suspend fun getCleanapkAppDetails(packageName: String): Pair<FusedApp, ResultStatus> { var fusedApp = FusedApp() val status = runCodeWithTimeout({ val result = handleNetworkResult { val result = cleanApkAppsRepository.getSearchResult( packageName, "package_name" Loading @@ -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( Loading Loading @@ -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" Loading @@ -627,7 +628,9 @@ class FusedApiImpl @Inject constructor( ) } } }) } status = result.getResultStatus() /* * If status is not ok, immediately return. Loading @@ -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". Loading @@ -670,9 +673,9 @@ class FusedApiImpl @Inject constructor( ) } } }) } return Pair(fusedAppList, status) return Pair(fusedAppList, result.getResultStatus()) } /** Loading @@ -691,7 +694,7 @@ class FusedApiImpl @Inject constructor( appList: List<App>, ): ResultSupreme<List<FusedApp>> { val filteredFusedApps = mutableListOf<FusedApp>() val status = runCodeWithTimeout({ return handleNetworkResult { appList.forEach { val filter = getAppFilterLevel(it, authData) if (filter.isUnFiltered()) { Loading @@ -702,9 +705,8 @@ class FusedApiImpl @Inject constructor( ) } } }) return ResultSupreme.create(status, filteredFusedApps) filteredFusedApps } } /** Loading Loading @@ -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<Application>).body()?.app } else { Loading @@ -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()) } /* Loading Loading @@ -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) Loading @@ -846,34 +849,25 @@ class FusedApiImpl @Inject constructor( private suspend fun fetchGplayCategories( type: CategoryType, ): Triple<ResultStatus, List<FusedCategory>, String> { var errorApplicationCategory = "" var apiStatus = ResultStatus.OK ): ResultSupreme<List<FusedCategory>> { val categoryList = mutableListOf<FusedCategory>() 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<ResultStatus, List<FusedCategory>, String> { var errorApplicationCategory = "" var apiStatus: ResultStatus = ResultStatus.OK val fusedCategoriesList = mutableListOf<FusedCategory>() runCodeWithTimeout({ val result = handleNetworkResult { getPWAsCategories()?.let { fusedCategoriesList.addAll( getFusedCategoryBasedOnCategoryType( Loading @@ -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<ResultStatus, List<FusedCategory>, String> { var errorApplicationCategory = "" var apiStatus: ResultStatus = ResultStatus.OK val fusedCategoryList = mutableListOf<FusedCategory>() runCodeWithTimeout({ val result = handleNetworkResult { getOpenSourceCategories()?.let { fusedCategoryList.addAll( getFusedCategoryBasedOnCategoryType( Loading @@ -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) } /** Loading Loading @@ -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") Loading Loading @@ -1082,12 +1063,12 @@ class FusedApiImpl @Inject constructor( query: String, nextPageSubBundle: Set<SearchBundle.SubBundle>? ): 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<FusedApp>(), setOf<SearchBundle.SubBundle>()) } val fusedAppList = Loading @@ -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()) } } Loading Loading @@ -1416,7 +1384,7 @@ class FusedApiImpl @Inject constructor( var fusedAppList: MutableList<FusedApp> = mutableListOf() var nextPageUrl = "" val status = runCodeWithTimeout({ return handleNetworkResult { val streamCluster = gplayRepository.getAppsByCategory(category, pageUrl) as StreamCluster Loading @@ -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) } } }
app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt +6 −25 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() Loading Loading @@ -163,32 +163,16 @@ class GPlayHttpClient @Inject constructor( val call = okHttpClient.newCall(request) response = call.execute() buildPlayResponse(response) } 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)) { } catch (e: GplayHttpRequestException) { throw e } when (e) { is UnknownHostException, is SocketTimeoutException -> handleExceptionOnGooglePlayRequest(e) else -> handleExceptionOnGooglePlayRequest(e) } } catch (e: Exception) { 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<String, String>): HttpUrl { val urlBuilder = url.toHttpUrl().newBuilder() params.forEach { Loading Loading @@ -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) } Loading