From 704391ca04675dfc7b2fb05b6f46782f338c371b Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Thu, 7 Sep 2023 12:21:17 +0000 Subject: [PATCH 01/16] 1546 authdata dump fix --- app/src/main/java/foundation/e/apps/AppLoungeApplication.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt b/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt index a9044e3fb..368c89a3c 100644 --- a/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt +++ b/app/src/main/java/foundation/e/apps/AppLoungeApplication.kt @@ -26,6 +26,7 @@ import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration import androidx.work.ExistingPeriodicWorkPolicy import dagger.hilt.android.HiltAndroidApp +import foundation.e.apps.data.Constants.TAG_AUTHDATA_DUMP import foundation.e.apps.data.preference.DataStoreModule import foundation.e.apps.data.preference.PreferenceManagerModule import foundation.e.apps.install.pkg.PkgManagerBR @@ -81,7 +82,7 @@ class AppLoungeApplication : Application(), Configuration.Provider { Telemetry.init(BuildConfig.SENTRY_DSN, this) plant(object : Timber.Tree() { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { - if (priority <= Log.WARN) { + if (priority <= Log.WARN && tag != TAG_AUTHDATA_DUMP) { return } Log.println(priority, tag, message) -- GitLab From 757a805b07ed67a442b090f6d083a8df8e0bf461 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Fri, 8 Sep 2023 15:21:16 +0600 Subject: [PATCH 02/16] removed redundant login failed dialog --- .../java/foundation/e/apps/MainActivity.kt | 5 ----- .../data/gplay/GplayStoreRepositoryImpl.kt | 21 +++++++++++-------- .../e/apps/data/login/LoginCommon.kt | 4 ++++ .../apps/data/login/LoginSourceRepository.kt | 7 +++++++ 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index c4d95fcea..b548c9423 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -139,11 +139,6 @@ class MainActivity : AppCompatActivity() { ) } else if (exception != null) { Timber.e(exception, "Login failed! message: ${exception?.localizedMessage}") - ApplicationDialogFragment( - title = getString(R.string.sign_in_failed_title), - message = getString(R.string.sign_in_failed_desc), - positiveButtonText = getString(R.string.ok) - ).show(supportFragmentManager, TAG) } } } diff --git a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt index 19461db72..661844aec 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt @@ -35,13 +35,16 @@ import com.aurora.gplayapi.helpers.SearchHelper import com.aurora.gplayapi.helpers.TopChartsHelper import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R +import foundation.e.apps.data.enums.User import foundation.e.apps.data.fused.utils.CategoryType import foundation.e.apps.data.gplay.utils.GPlayHttpClient import foundation.e.apps.data.login.LoginSourceRepository +import foundation.e.apps.data.login.exceptions.GPlayLoginException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject +import kotlin.math.log class GplayStoreRepositoryImpl @Inject constructor( @ApplicationContext private val context: Context, @@ -52,7 +55,7 @@ class GplayStoreRepositoryImpl @Inject constructor( override suspend fun getHomeScreenData(): Any { val homeScreenData = mutableMapOf>() val homeElements = createTopChartElements() - val authData = loginSourceRepository.gplayAuth ?: return homeScreenData + val authData = loginSourceRepository.gplayAuth!! homeElements.forEach { val chart = it.value.keys.iterator().next() @@ -77,7 +80,7 @@ class GplayStoreRepositoryImpl @Inject constructor( query: String, subBundle: MutableSet? ): Pair, MutableSet> { - var authData = loginSourceRepository.gplayAuth ?: return Pair(emptyList(), mutableSetOf()) + var authData = loginSourceRepository.gplayAuth!! val searchHelper = SearchHelper(authData).using(gPlayHttpClient) @@ -102,7 +105,7 @@ class GplayStoreRepositoryImpl @Inject constructor( } override suspend fun getSearchSuggestions(query: String): List { - val authData = loginSourceRepository.gplayAuth ?: return listOf() + val authData = loginSourceRepository.gplayAuth!! val searchData = mutableListOf() withContext(Dispatchers.IO) { @@ -113,7 +116,7 @@ class GplayStoreRepositoryImpl @Inject constructor( } override suspend fun getAppsByCategory(category: String, pageUrl: String?): StreamCluster { - val authData = loginSourceRepository.gplayAuth ?: return StreamCluster() + val authData = loginSourceRepository.gplayAuth!! val subCategoryHelper = CategoryAppsHelper(authData).using(gPlayHttpClient) @@ -131,7 +134,7 @@ class GplayStoreRepositoryImpl @Inject constructor( return categoryList } - val authData = loginSourceRepository.gplayAuth ?: return categoryList + val authData = loginSourceRepository.gplayAuth!! withContext(Dispatchers.IO) { val categoryHelper = CategoryHelper(authData).using(gPlayHttpClient) @@ -142,7 +145,7 @@ class GplayStoreRepositoryImpl @Inject constructor( override suspend fun getAppDetails(packageNameOrId: String): App? { var appDetails: App? - val authData = loginSourceRepository.gplayAuth ?: return null + val authData = loginSourceRepository.gplayAuth!! withContext(Dispatchers.IO) { val appDetailsHelper = AppDetailsHelper(authData).using(gPlayHttpClient) @@ -153,7 +156,7 @@ class GplayStoreRepositoryImpl @Inject constructor( override suspend fun getAppsDetails(packageNamesOrIds: List): List { val appDetailsList = mutableListOf() - val authData = loginSourceRepository.gplayAuth ?: return appDetailsList + val authData = loginSourceRepository.gplayAuth!! withContext(Dispatchers.IO) { val appDetailsHelper = AppDetailsHelper(authData).using(gPlayHttpClient) @@ -185,7 +188,7 @@ class GplayStoreRepositoryImpl @Inject constructor( offerType: Int ): List { val downloadData = mutableListOf() - val authData = loginSourceRepository.gplayAuth ?: return downloadData + val authData = loginSourceRepository.gplayAuth!! withContext(Dispatchers.IO) { val version = versionCode?.let { it as Int } ?: -1 @@ -202,7 +205,7 @@ class GplayStoreRepositoryImpl @Inject constructor( offerType: Int ): List { val downloadData = mutableListOf() - val authData = loginSourceRepository.gplayAuth ?: return downloadData + val authData = loginSourceRepository.gplayAuth!! withContext(Dispatchers.IO) { val purchaseHelper = PurchaseHelper(authData).using(gPlayHttpClient) diff --git a/app/src/main/java/foundation/e/apps/data/login/LoginCommon.kt b/app/src/main/java/foundation/e/apps/data/login/LoginCommon.kt index 6f3646153..cc38464e2 100644 --- a/app/src/main/java/foundation/e/apps/data/login/LoginCommon.kt +++ b/app/src/main/java/foundation/e/apps/data/login/LoginCommon.kt @@ -36,6 +36,10 @@ class LoginCommon @Inject constructor( loginDataStore.saveUserType(user) } + fun getUserType(): User { + return loginDataStore.getUserType() + } + suspend fun saveGoogleLogin(email: String, oauth: String) { loginDataStore.saveGoogleLogin(email, oauth) } diff --git a/app/src/main/java/foundation/e/apps/data/login/LoginSourceRepository.kt b/app/src/main/java/foundation/e/apps/data/login/LoginSourceRepository.kt index 6cbf99998..7cf920cbb 100644 --- a/app/src/main/java/foundation/e/apps/data/login/LoginSourceRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/login/LoginSourceRepository.kt @@ -20,6 +20,7 @@ package foundation.e.apps.data.login import com.aurora.gplayapi.data.models.AuthData import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.User +import foundation.e.apps.data.login.exceptions.GPlayLoginException import javax.inject.Inject import javax.inject.Singleton @@ -31,6 +32,8 @@ class LoginSourceRepository @Inject constructor( ) { var gplayAuth: AuthData? = null + get() = field ?: throw GPlayLoginException(false, "AuthData is not available!", getUserType()) + suspend fun getAuthObjects(clearAuthTypes: List = listOf()): List { val authObjectsLocal = ArrayList() @@ -71,4 +74,8 @@ class LoginSourceRepository @Inject constructor( this.gplayAuth = validateAuthData.data return validateAuthData } + + private fun getUserType(): User { + return loginCommon.getUserType() + } } -- GitLab From a0095029b3e6c3808f8394e5797a63c4d1aaf1a9 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Fri, 8 Sep 2023 19:12:14 +0600 Subject: [PATCH 03/16] fixed: lint issues --- .../foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt index 661844aec..383f8e7c6 100644 --- a/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt @@ -35,16 +35,13 @@ import com.aurora.gplayapi.helpers.SearchHelper import com.aurora.gplayapi.helpers.TopChartsHelper import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R -import foundation.e.apps.data.enums.User import foundation.e.apps.data.fused.utils.CategoryType import foundation.e.apps.data.gplay.utils.GPlayHttpClient import foundation.e.apps.data.login.LoginSourceRepository -import foundation.e.apps.data.login.exceptions.GPlayLoginException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject -import kotlin.math.log class GplayStoreRepositoryImpl @Inject constructor( @ApplicationContext private val context: Context, -- GitLab From 430bc57cfda2d586157915be348c9914b26ba304 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Fri, 8 Sep 2023 21:51:45 +0530 Subject: [PATCH 04/16] update version to 2.6.2 for 1.15-rc --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 2787d2af3..461d23ecb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ plugins { def versionMajor = 2 def versionMinor = 6 -def versionPatch = 1 +def versionPatch = 2 def getGitHash = { -> def stdOut = new ByteArrayOutputStream() -- GitLab From e6da9db13bc11f7aa81541305351b63bd7d4b79f Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Wed, 30 Aug 2023 14:04:45 +0600 Subject: [PATCH 05/16] error handling updated for search and homepage --- .../foundation/e/apps/data/ResultSupreme.kt | 18 ++++- .../e/apps/data/cleanapk/RetrofitModule.kt | 5 ++ .../e/apps/data/fused/FusedApiImpl.kt | 76 ++++++++++++------- .../apps/data/gplay/utils/GPlayHttpClient.kt | 30 ++------ .../apps/data/login/api/LoginApiRepository.kt | 5 +- 5 files changed, 76 insertions(+), 58 deletions(-) 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 14695702f..80816f2e6 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 43a505137..61a466b4d 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 @@ -34,6 +34,7 @@ import foundation.e.apps.data.ecloud.EcloudApiInterface import foundation.e.apps.data.exodus.ExodusTrackerApi import foundation.e.apps.data.fdroid.FdroidApiInterface import foundation.e.apps.data.fdroid.FdroidWebInterface +import foundation.e.apps.data.gplay.utils.GPlayHttpClient import okhttp3.Cache import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -48,6 +49,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 +57,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 +212,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 cb398a78c..f580124df 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 @@ -73,7 +72,6 @@ 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 @@ -100,7 +98,7 @@ 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_API = "Gplay api has faced error!" private const val ERROR_GPLAY_SOURCE_NOT_SELECTED = "Gplay apps are not selected!" } @@ -167,29 +165,32 @@ class FusedApiImpl @Inject constructor( authData: AuthData, ): ResultSupreme> { - val apiStatus = when (source) { - Source.GPLAY -> runCodeWithTimeout({ + val result = when (source) { + Source.GPLAY -> handleResultFromAppSources> { priorList.addAll(fetchGPlayHome(authData)) - }) + priorList + } - Source.OPEN -> runCodeWithTimeout({ + Source.OPEN -> handleResultFromAppSources { val response = (cleanApkAppsRepository.getHomeScreenData() as Response).body() response?.home?.let { priorList.addAll(generateCleanAPKHome(it, APP_TYPE_OPEN)) } - }) + priorList + } - Source.PWA -> runCodeWithTimeout({ + Source.PWA -> handleResultFromAppSources { 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 +198,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) { @@ -322,16 +323,17 @@ class FusedApiImpl @Inject constructor( searchResult: MutableList, packageSpecificResults: ArrayList ): ResultSupreme, Boolean>> { - val status = runCodeWithTimeout({ + val result = handleResultFromAppSources { cleanApkResults.addAll(getCleanAPKSearchResults(query)) - }) + cleanApkResults + } if (cleanApkResults.isNotEmpty()) { searchResult.addAll(cleanApkResults) } return ResultSupreme.create( - status, + result.getResultStatus(), Pair( filterWithKeywordSearch( searchResult, @@ -957,7 +959,7 @@ class FusedApiImpl @Inject constructor( private fun getCategoryIconName(category: FusedCategory): String { var categoryTitle = if (category.tag.getOperationalTag() - .contentEquals(AppTag.GPlay().getOperationalTag()) + .contentEquals(AppTag.GPlay().getOperationalTag()) ) category.id else category.title if (categoryTitle.contains(CATEGORY_TITLE_REPLACEABLE_CONJUNCTION)) { @@ -1082,12 +1084,12 @@ class FusedApiImpl @Inject constructor( query: String, nextPageSubBundle: Set? ): GplaySearchResult { - try { + return handleResultFromAppSources { val searchResults = gplayRepository.getSearchResult(query, nextPageSubBundle?.toMutableSet()) if (!preferenceManagerModule.isGplaySelected()) { - return ResultSupreme.Error(ERROR_GPLAY_SOURCE_NOT_SELECTED) + return@handleResultFromAppSources Pair(listOf(), setOf()) } val fusedAppList = @@ -1097,21 +1099,39 @@ 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}" + return@handleResultFromAppSources Pair(fusedAppList.toList(), searchResults.second.toSet()) + } + } + private suspend fun handleResultFromAppSources(call: suspend () -> T): ResultSupreme { + return try { + ResultSupreme.Success(call()) + } catch (e: SocketTimeoutException) { + val message = extractErrorMessage(e) + val exception = GPlayException(true, message) + val resultTimeout = ResultSupreme.Timeout(exception = exception) + resultTimeout.message = message + + resultTimeout + } catch (e: GplayHttpRequestException) { + val message = extractErrorMessage(e) val exception = GPlayException(e.status == 408, message) - return ResultSupreme.Error(message, exception) + + ResultSupreme.Error(message, exception) } catch (e: Exception) { - val exception = - GPlayException(e is SocketTimeoutException, e.localizedMessage) + val message = extractErrorMessage(e) + ResultSupreme.Error(message, e) + } + } - return ResultSupreme.Error(e.localizedMessage ?: "", exception) + 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/gplay/utils/GPlayHttpClient.kt b/app/src/main/java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt index b502cd756..692e76f0d 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 @@ -45,7 +45,7 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject class GPlayHttpClient @Inject constructor( - private val cache: Cache, + private val cache: Cache, ) : IHttpClient { private val POST = "POST" @@ -54,10 +54,13 @@ class GPlayHttpClient @Inject constructor( companion object { private const val TAG = "GPlayHttpClient" private const val HTTP_TIMEOUT_IN_SECOND = 10L + <<<<<<< HEAD private const val SEARCH = "search" private const val SEARCH_SUGGEST = "searchSuggest" private const val STATUS_CODE_UNAUTHORIZED = 401 private const val STATUS_CODE_TOO_MANY_REQUESTS = 429 + ======= + >>>>>>> ea7f47e2 (error handling updated for search and homepage) } private val okHttpClient = OkHttpClient().newBuilder() @@ -164,30 +167,12 @@ class GPlayHttpClient @Inject constructor( 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)) { - throw e - } - - when (e) { - is UnknownHostException, - is SocketTimeoutException -> handleExceptionOnGooglePlayRequest(e) - else -> handleExceptionOnGooglePlayRequest(e) - } + throw e } 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 { @@ -221,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 5d97b6487..d9de1fad3 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 @@ -144,7 +144,6 @@ class LoginApiRepository constructor( */ private suspend fun runCodeWithTimeout( block: suspend () -> T, - timeoutBlock: (() -> T?)? = null, exceptionBlock: ((e: Exception) -> T?)? = null, ): ResultSupreme { return try { @@ -152,9 +151,7 @@ class LoginApiRepository constructor( return@withTimeout ResultSupreme.Success(block()) } } catch (e: TimeoutCancellationException) { - ResultSupreme.Timeout(timeoutBlock?.invoke()).apply { - message = e.message ?: "" - } + ResultSupreme.Timeout(exception = e) } catch (e: Exception) { e.printStackTrace() ResultSupreme.Error(exceptionBlock?.invoke(e), message = e.message ?: "") -- GitLab From 2618f3e17c7c1e8ff2a3b33857dc4856db9b48d2 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Wed, 30 Aug 2023 15:32:27 +0600 Subject: [PATCH 06/16] error handling updated for all gplay apis --- .../e/apps/data/fused/FusedApiImpl.kt | 45 ++++++++----------- 1 file changed, 19 insertions(+), 26 deletions(-) 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 f580124df..e86b5c68d 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 @@ -693,7 +693,7 @@ class FusedApiImpl @Inject constructor( appList: List, ): ResultSupreme> { val filteredFusedApps = mutableListOf() - val status = runCodeWithTimeout({ + return handleResultFromAppSources { appList.forEach { val filter = getAppFilterLevel(it, authData) if (filter.isUnFiltered()) { @@ -704,9 +704,8 @@ class FusedApiImpl @Inject constructor( ) } } - }) - - return ResultSupreme.create(status, filteredFusedApps) + filteredFusedApps + } } /** @@ -785,7 +784,7 @@ class FusedApiImpl @Inject constructor( var response: FusedApp? = null - val status = runCodeWithTimeout({ + val result = handleResultFromAppSources { response = if (origin == Origin.CLEANAPK) { (cleanApkAppsRepository.getAppDetails(id) as Response).body()?.app } else { @@ -798,9 +797,10 @@ class FusedApiImpl @Inject constructor( it.updateSource() it.updateFilterLevel(authData) } - }) + response + } - return Pair(response ?: FusedApp(), status) + return Pair(result.data ?: FusedApp(), result.getResultStatus()) } /* @@ -838,9 +838,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) @@ -848,25 +848,18 @@ class FusedApiImpl @Inject constructor( private suspend fun fetchGplayCategories( type: CategoryType, - ): Triple, String> { - var errorApplicationCategory = "" - var apiStatus = ResultStatus.OK + ): ResultSupreme> { val categoryList = mutableListOf() - runCodeWithTimeout({ + + return handleResultFromAppSources { 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( @@ -1130,6 +1123,7 @@ class FusedApiImpl @Inject constructor( is SocketTimeoutException -> "Timeout" else -> "Unknown" } + return (e.localizedMessage?.ifBlank { ERROR_GPLAY_API } ?: ERROR_GPLAY_API) + "Status: $status" } @@ -1436,7 +1430,7 @@ class FusedApiImpl @Inject constructor( var fusedAppList: MutableList = mutableListOf() var nextPageUrl = "" - val status = runCodeWithTimeout({ + return handleResultFromAppSources { val streamCluster = gplayRepository.getAppsByCategory(category, pageUrl) as StreamCluster @@ -1449,8 +1443,7 @@ class FusedApiImpl @Inject constructor( if (!nextPageUrl.isNullOrEmpty()) { fusedAppList.add(FusedApp(isPlaceHolder = true)) } - }) - - return ResultSupreme.create(status, Pair(fusedAppList, nextPageUrl)) + Pair(fusedAppList, nextPageUrl) + } } } -- GitLab From 8776c30e361981b86dbf6bcc194425a65c00be46 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Wed, 6 Sep 2023 18:53:06 +0600 Subject: [PATCH 07/16] error handling updated for cleanApkApps. --- .../e/apps/data/fused/FusedApiImpl.kt | 126 ++++++++---------- .../apps/data/gplay/utils/GPlayHttpClient.kt | 8 +- 2 files changed, 61 insertions(+), 73 deletions(-) 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 e86b5c68d..6a7cbec24 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 @@ -99,7 +99,9 @@ class FusedApiImpl @Inject constructor( private const val CATEGORY_OPEN_GAMES_ID = "game_open_games" private const val CATEGORY_OPEN_GAMES_TITLE = "Open games" private const val ERROR_GPLAY_API = "Gplay api has faced error!" - private const val ERROR_GPLAY_SOURCE_NOT_SELECTED = "Gplay apps are not selected!" + private const val TIMEOUT = "Timeout" + private const val UNKNOWN = "Unknown" + private const val STATUS = "Status: " } /** @@ -166,12 +168,12 @@ class FusedApiImpl @Inject constructor( ): ResultSupreme> { val result = when (source) { - Source.GPLAY -> handleResultFromAppSources> { + Source.GPLAY -> handleNetworkResult> { priorList.addAll(fetchGPlayHome(authData)) priorList } - Source.OPEN -> handleResultFromAppSources { + Source.OPEN -> handleNetworkResult { val response = (cleanApkAppsRepository.getHomeScreenData() as Response).body() response?.home?.let { @@ -180,7 +182,7 @@ class FusedApiImpl @Inject constructor( priorList } - Source.PWA -> handleResultFromAppSources { + Source.PWA -> handleNetworkResult { val response = (cleanApkPWARepository.getHomeScreenData() as Response).body() response?.home?.let { @@ -290,7 +292,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 { @@ -298,14 +300,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, @@ -323,7 +325,7 @@ class FusedApiImpl @Inject constructor( searchResult: MutableList, packageSpecificResults: ArrayList ): ResultSupreme, Boolean>> { - val result = handleResultFromAppSources { + val result = handleNetworkResult { cleanApkResults.addAll(getCleanAPKSearchResults(query)) cleanApkResults } @@ -353,7 +355,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) } @@ -361,7 +363,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. @@ -380,10 +382,10 @@ 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)) } /* @@ -448,7 +450,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" @@ -457,15 +459,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 } @@ -525,7 +527,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() @@ -533,13 +535,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() @@ -547,8 +549,8 @@ class FusedApiImpl @Inject constructor( it.updateFilterLevel(null) list.add(it) } - }) - return ResultSupreme.create(status, Pair(list, "")) + } + return ResultSupreme.create(result.getResultStatus(), Pair(list, "")) } /* @@ -559,7 +561,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" @@ -571,8 +573,8 @@ class FusedApiImpl @Inject constructor( ?: FusedApp() } fusedApp.updateFilterLevel(null) - }) - return Pair(fusedApp, status) + } + return Pair(fusedApp, result.getResultStatus()) } override suspend fun getApplicationDetails( @@ -616,7 +618,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" @@ -629,7 +631,9 @@ class FusedApiImpl @Inject constructor( ) } } - }) + } + + status = result.getResultStatus() /* * If status is not ok, immediately return. @@ -655,7 +659,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". @@ -672,9 +676,9 @@ class FusedApiImpl @Inject constructor( ) } } - }) + } - return Pair(fusedAppList, status) + return Pair(fusedAppList, result.getResultStatus()) } /** @@ -693,7 +697,7 @@ class FusedApiImpl @Inject constructor( appList: List, ): ResultSupreme> { val filteredFusedApps = mutableListOf() - return handleResultFromAppSources { + return handleNetworkResult { appList.forEach { val filter = getAppFilterLevel(it, authData) if (filter.isUnFiltered()) { @@ -784,7 +788,7 @@ class FusedApiImpl @Inject constructor( var response: FusedApp? = null - val result = handleResultFromAppSources { + val result = handleNetworkResult { response = if (origin == Origin.CLEANAPK) { (cleanApkAppsRepository.getAppDetails(id) as Response).body()?.app } else { @@ -851,7 +855,7 @@ class FusedApiImpl @Inject constructor( ): ResultSupreme> { val categoryList = mutableListOf() - return handleResultFromAppSources { + return handleNetworkResult { val playResponse = gplayRepository.getCategories(type).map { app -> val category = app.transformToFusedCategory() updateCategoryDrawable(category) @@ -865,10 +869,8 @@ class FusedApiImpl @Inject constructor( 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( @@ -876,23 +878,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( @@ -902,14 +897,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) } /** @@ -1077,12 +1067,12 @@ class FusedApiImpl @Inject constructor( query: String, nextPageSubBundle: Set? ): GplaySearchResult { - return handleResultFromAppSources { + return handleNetworkResult { val searchResults = gplayRepository.getSearchResult(query, nextPageSubBundle?.toMutableSet()) if (!preferenceManagerModule.isGplaySelected()) { - return@handleResultFromAppSources Pair(listOf(), setOf()) + return@handleNetworkResult Pair(listOf(), setOf()) } val fusedAppList = @@ -1092,19 +1082,17 @@ class FusedApiImpl @Inject constructor( fusedAppList.add(FusedApp(isPlaceHolder = true)) } - return@handleResultFromAppSources Pair(fusedAppList.toList(), searchResults.second.toSet()) + return@handleNetworkResult Pair(fusedAppList.toList(), searchResults.second.toSet()) } } - private suspend fun handleResultFromAppSources(call: suspend () -> T): ResultSupreme { + private suspend fun handleNetworkResult(call: suspend () -> T): ResultSupreme { return try { ResultSupreme.Success(call()) } catch (e: SocketTimeoutException) { val message = extractErrorMessage(e) - val exception = GPlayException(true, message) - val resultTimeout = ResultSupreme.Timeout(exception = exception) + val resultTimeout = ResultSupreme.Timeout(exception = e) resultTimeout.message = message - resultTimeout } catch (e: GplayHttpRequestException) { val message = extractErrorMessage(e) @@ -1120,12 +1108,12 @@ class FusedApiImpl @Inject constructor( private fun extractErrorMessage(e: Exception): String { val status = when (e) { is GplayHttpRequestException -> e.status.toString() - is SocketTimeoutException -> "Timeout" - else -> "Unknown" + is SocketTimeoutException -> TIMEOUT + else -> UNKNOWN } return (e.localizedMessage?.ifBlank { ERROR_GPLAY_API } - ?: ERROR_GPLAY_API) + "Status: $status" + ?: ERROR_GPLAY_API) + "$STATUS$status" } /* @@ -1430,7 +1418,7 @@ class FusedApiImpl @Inject constructor( var fusedAppList: MutableList = mutableListOf() var nextPageUrl = "" - return handleResultFromAppSources { + return handleNetworkResult { val streamCluster = gplayRepository.getAppsByCategory(category, pageUrl) as StreamCluster 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 692e76f0d..a123e685e 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 @@ -39,6 +39,7 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import timber.log.Timber import java.io.IOException +import java.net.Socket import java.net.SocketTimeoutException import java.net.UnknownHostException import java.util.concurrent.TimeUnit @@ -54,13 +55,11 @@ class GPlayHttpClient @Inject constructor( companion object { private const val TAG = "GPlayHttpClient" private const val HTTP_TIMEOUT_IN_SECOND = 10L - <<<<<<< HEAD private const val SEARCH = "search" private const val SEARCH_SUGGEST = "searchSuggest" private const val STATUS_CODE_UNAUTHORIZED = 401 private const val STATUS_CODE_TOO_MANY_REQUESTS = 429 - ======= - >>>>>>> ea7f47e2 (error handling updated for search and homepage) + } private val okHttpClient = OkHttpClient().newBuilder() @@ -167,7 +166,8 @@ class GPlayHttpClient @Inject constructor( response = call.execute() buildPlayResponse(response) } catch (e: Exception) { - throw e + val status = if (e is SocketTimeoutException) 408 else -1 + throw GplayHttpRequestException(status, e.localizedMessage ?: "") } finally { response?.close() } -- GitLab From 093eb1c5a8ec9913b9f0b587c71f2311e288d727 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Thu, 7 Sep 2023 14:34:15 +0600 Subject: [PATCH 08/16] resolved conflict --- .../foundation/e/apps/data/NetworkHandler.kt | 44 +++++++++++++++++++ .../e/apps/data/fused/FusedApiImpl.kt | 40 +++-------------- .../apps/data/gplay/utils/GPlayHttpClient.kt | 7 ++- .../apps/data/login/api/LoginApiRepository.kt | 34 ++++---------- .../ApplicationListViewModel.kt | 26 +++++------ 5 files changed, 75 insertions(+), 76 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/data/NetworkHandler.kt 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 000000000..e99dec8a0 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt @@ -0,0 +1,44 @@ +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) { + val message = extractErrorMessage(e) + val resultTimeout = ResultSupreme.Timeout(exception = e) + resultTimeout.message = message + resultTimeout + } catch (e: GplayHttpRequestException) { + val message = extractErrorMessage(e) + val exception = GPlayException(e.status == GPlayHttpClient.STATUS_CODE_TIMEOUT, message) + + if (exception.isTimeout) { + ResultSupreme.Timeout(exception = exception) + } else { + ResultSupreme.Error(message, exception) + } + } catch (e: Exception) { + val message = extractErrorMessage(e) + 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" +} \ No newline at end of file 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 6a7cbec24..3502f4b74 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 @@ -63,6 +63,7 @@ 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.handleNetworkResult import foundation.e.apps.data.login.exceptions.GPlayException import foundation.e.apps.data.preference.PreferenceManagerModule import foundation.e.apps.install.pkg.PWAManagerModule @@ -98,10 +99,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_API = "Gplay api has faced error!" - private const val TIMEOUT = "Timeout" - private const val UNKNOWN = "Unknown" - private const val STATUS = "Status: " } /** @@ -383,7 +380,10 @@ class FusedApiImpl @Inject constructor( * Also send true in the pair to signal more results being loaded. */ if (result.getResultStatus() != ResultStatus.OK) { - return ResultSupreme.create(result.getResultStatus(), Pair(packageSpecificResults, false)) + return ResultSupreme.create( + result.getResultStatus(), + Pair(packageSpecificResults, false) + ) } return ResultSupreme.create(result.getResultStatus(), Pair(packageSpecificResults, true)) } @@ -1086,36 +1086,6 @@ class FusedApiImpl @Inject constructor( } } - private suspend fun handleNetworkResult(call: suspend () -> T): ResultSupreme { - return try { - ResultSupreme.Success(call()) - } catch (e: SocketTimeoutException) { - val message = extractErrorMessage(e) - val resultTimeout = ResultSupreme.Timeout(exception = e) - resultTimeout.message = message - resultTimeout - } catch (e: GplayHttpRequestException) { - val message = extractErrorMessage(e) - val exception = GPlayException(e.status == 408, message) - - ResultSupreme.Error(message, exception) - } catch (e: Exception) { - val message = extractErrorMessage(e) - 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" - } - /* * This function will replace a GPlay app with F-Droid app if exists, * else will show the GPlay app itself. 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 a123e685e..7e95f06a2 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 @@ -45,6 +45,7 @@ import java.net.UnknownHostException import java.util.concurrent.TimeUnit import javax.inject.Inject + class GPlayHttpClient @Inject constructor( private val cache: Cache, ) : IHttpClient { @@ -59,7 +60,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() @@ -165,8 +166,10 @@ class GPlayHttpClient @Inject constructor( val call = okHttpClient.newCall(request) response = call.execute() buildPlayResponse(response) + } catch (e: GplayHttpRequestException) { + throw e } catch (e: Exception) { - val status = if (e is SocketTimeoutException) 408 else -1 + val status = if (e is SocketTimeoutException) STATUS_CODE_TIMEOUT else -1 throw GplayHttpRequestException(status, e.localizedMessage ?: "") } finally { response?.close() 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 d9de1fad3..acad60241 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 @@ -23,6 +23,7 @@ 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 @@ -52,9 +53,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 +77,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 +110,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 +130,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,23 +139,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, - exceptionBlock: ((e: Exception) -> T?)? = null, - ): ResultSupreme { - return try { - withTimeout(timeoutDurationInMillis) { - return@withTimeout ResultSupreme.Success(block()) - } - } catch (e: TimeoutCancellationException) { - ResultSupreme.Timeout(exception = e) - } 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 79c8cc52d..86be6867d 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) } -- GitLab From 16df7fdb0eb3101d7953ad9cdc71482fb4235d8b Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Thu, 7 Sep 2023 15:45:27 +0600 Subject: [PATCH 09/16] fixed: lint issues --- .../main/java/foundation/e/apps/data/NetworkHandler.kt | 5 ++--- app/src/main/java/foundation/e/apps/data/ResultSupreme.kt | 4 ++-- .../foundation/e/apps/data/cleanapk/RetrofitModule.kt | 1 - .../java/foundation/e/apps/data/fused/FusedApiImpl.kt | 8 ++------ .../foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt | 3 --- .../e/apps/data/login/api/LoginApiRepository.kt | 3 --- 6 files changed, 6 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt index e99dec8a0..29b58d296 100644 --- a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt +++ b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt @@ -39,6 +39,5 @@ private fun extractErrorMessage(e: Exception): String { is SocketTimeoutException -> TIMEOUT else -> UNKNOWN } - return (e.localizedMessage?.ifBlank { ERROR_GPLAY_API } - ?: ERROR_GPLAY_API) + " $STATUS $status" -} \ No newline at end of file + 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 80816f2e6..a7a773f60 100644 --- a/app/src/main/java/foundation/e/apps/data/ResultSupreme.kt +++ b/app/src/main/java/foundation/e/apps/data/ResultSupreme.kt @@ -124,11 +124,11 @@ sealed class ResultSupreme { } fun getResultStatus(): ResultStatus { - return when(this) { + return when (this) { is Success -> ResultStatus.OK is Timeout -> ResultStatus.TIMEOUT else -> ResultStatus.UNKNOWN.apply { - message = this@ResultSupreme.exception?.localizedMessage?: UNKNOWN_ERROR + message = this@ResultSupreme.exception?.localizedMessage ?: UNKNOWN_ERROR } } } 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 61a466b4d..c924f27c7 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 @@ -34,7 +34,6 @@ import foundation.e.apps.data.ecloud.EcloudApiInterface import foundation.e.apps.data.exodus.ExodusTrackerApi import foundation.e.apps.data.fdroid.FdroidApiInterface import foundation.e.apps.data.fdroid.FdroidWebInterface -import foundation.e.apps.data.gplay.utils.GPlayHttpClient import okhttp3.Cache import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaTypeOrNull 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 3502f4b74..e08bb6ded 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 @@ -62,9 +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.handleNetworkResult -import foundation.e.apps.data.login.exceptions.GPlayException import foundation.e.apps.data.preference.PreferenceManagerModule import foundation.e.apps.install.pkg.PWAManagerModule import foundation.e.apps.install.pkg.PkgManagerModule @@ -76,7 +74,6 @@ import kotlinx.coroutines.coroutineScope 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 @@ -941,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") 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 7e95f06a2..38fb0ad22 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 @@ -39,13 +39,10 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import timber.log.Timber import java.io.IOException -import java.net.Socket import java.net.SocketTimeoutException -import java.net.UnknownHostException import java.util.concurrent.TimeUnit import javax.inject.Inject - class GPlayHttpClient @Inject constructor( private val cache: Cache, ) : IHttpClient { 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 acad60241..b2fbd026c 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,14 +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 /** -- GitLab From a58da01b7384c3590b4afa05672e9b699a662a70 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Thu, 7 Sep 2023 20:35:18 +0600 Subject: [PATCH 10/16] minor refactoring --- .../foundation/e/apps/data/NetworkHandler.kt | 58 ++++++++++++++----- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt index 29b58d296..78985de5f 100644 --- a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt +++ b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt @@ -1,3 +1,21 @@ +/* + * 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 @@ -14,25 +32,37 @@ suspend fun handleNetworkResult(call: suspend () -> T): ResultSupreme { return try { ResultSupreme.Success(call()) } catch (e: SocketTimeoutException) { - val message = extractErrorMessage(e) - val resultTimeout = ResultSupreme.Timeout(exception = e) - resultTimeout.message = message - resultTimeout + handleSocketTimeoutException(e) } catch (e: GplayHttpRequestException) { - val message = extractErrorMessage(e) - val exception = GPlayException(e.status == GPlayHttpClient.STATUS_CODE_TIMEOUT, message) - - if (exception.isTimeout) { - ResultSupreme.Timeout(exception = exception) - } else { - ResultSupreme.Error(message, exception) - } + resultSupremeGplayHttpRequestException(e) } catch (e: Exception) { - val message = extractErrorMessage(e) - ResultSupreme.Error(message, e) + 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() -- GitLab From 784ae25887461a857f463d977d6936b9d4527135 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Mon, 11 Sep 2023 11:55:00 +0600 Subject: [PATCH 11/16] fixed: ignore error dialog for 401 --- .../java/foundation/e/apps/data/gplay/utils/GPlayHttpClient.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 38fb0ad22..acb44e965 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 @@ -55,6 +55,7 @@ class GPlayHttpClient @Inject constructor( private const val HTTP_TIMEOUT_IN_SECOND = 10L private const val SEARCH = "search" private const val SEARCH_SUGGEST = "searchSuggest" + private const val STATUS_CODE_OK = 200 private const val STATUS_CODE_UNAUTHORIZED = 401 private const val STATUS_CODE_TOO_MANY_REQUESTS = 429 const val STATUS_CODE_TIMEOUT = 408 @@ -206,7 +207,7 @@ class GPlayHttpClient @Inject constructor( } } - if (code != 200) { + if (code !in listOf(STATUS_CODE_OK, STATUS_CODE_UNAUTHORIZED)) { throw GplayHttpRequestException(code, response.message) } -- GitLab From 079791e14fbc6cb00129bdcdce56eef05855609c Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Mon, 11 Sep 2023 20:43:33 +0600 Subject: [PATCH 12/16] fixed: redundant get authobject call --- .../e/apps/data/login/LoginSourceRepository.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/login/LoginSourceRepository.kt b/app/src/main/java/foundation/e/apps/data/login/LoginSourceRepository.kt index 7cf920cbb..fea7939b6 100644 --- a/app/src/main/java/foundation/e/apps/data/login/LoginSourceRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/login/LoginSourceRepository.kt @@ -43,10 +43,13 @@ class LoginSourceRepository @Inject constructor( if (source::class.java.simpleName in clearAuthTypes) { source.clearSavedAuth() } - if (source is LoginSourceGPlay) { - gplayAuth = source.getAuthObject().result.data + + val authObject = source.getAuthObject() + authObjectsLocal.add(authObject) + + if (authObject is AuthObject.GPlayAuth) { + gplayAuth = authObject.result.data } - authObjectsLocal.add(source.getAuthObject()) } return authObjectsLocal -- GitLab From 6c9b0c3884a39113662d38c05ab6716e7f477f14 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Mon, 11 Sep 2023 15:22:29 +0000 Subject: [PATCH 13/16] Issue 1601: Fix install for no google mode --- .../java/foundation/e/apps/data/preference/DataStoreManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/data/preference/DataStoreManager.kt b/app/src/main/java/foundation/e/apps/data/preference/DataStoreManager.kt index 4294505ae..8bd36cbfe 100644 --- a/app/src/main/java/foundation/e/apps/data/preference/DataStoreManager.kt +++ b/app/src/main/java/foundation/e/apps/data/preference/DataStoreManager.kt @@ -33,7 +33,7 @@ class DataStoreManager @Inject constructor() { fun getAuthData(): AuthData { val authDataJson = dataStoreModule.getAuthDataSync() - return gson.fromJson(authDataJson, AuthData::class.java) + return gson.fromJson(authDataJson, AuthData::class.java) ?: AuthData("", "") } fun getUserType(): User { -- GitLab From a2f15f0cd3452663119f6df3571936b7f9dd379c Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Mon, 11 Sep 2023 21:39:37 +0600 Subject: [PATCH 14/16] fixed lint issue --- .../java/foundation/e/apps/ui/search/SearchViewModel.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 90e07f32b..a2bc71080 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 @@ -19,7 +19,6 @@ package foundation.e.apps.ui.search import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.SearchSuggestEntry @@ -42,7 +41,6 @@ import timber.log.Timber import javax.inject.Inject import kotlin.coroutines.coroutineContext - @HiltViewModel class SearchViewModel @Inject constructor( private val fusedAPIRepository: FusedAPIRepository, @@ -159,8 +157,8 @@ class SearchViewModel @Inject constructor( val isFirstFetch = nextSubBundle == null nextSubBundle = gplaySearchResult.data?.second - //first page has less data, then fetch next page data without waiting for users' scroll - if (isFirstFetch) { + // first page has less data, then fetch next page data without waiting for users' scroll + if (isFirstFetch && gplaySearchResult.isSuccess()) { CoroutineScope(coroutineContext).launch { fetchGplayData(query) } -- GitLab From aa651fe032da1277f51af03b59d1749ff8d25ee0 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Tue, 12 Sep 2023 23:33:17 +0600 Subject: [PATCH 15/16] added unit tests for unauthorized response --- .../apps/data/gplay/utils/GPlayHttpClient.kt | 2 +- .../foundation/e/apps/FusedApiImplTest.kt | 2 +- .../foundation/e/apps/GplyHttpClientTest.kt | 132 ++++++++++++++++++ .../foundation/e/apps/NetworkHandlerTest.kt | 9 ++ .../java/foundation/e/apps/util/FakeCall.kt | 79 +++++++++++ 5 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 app/src/test/java/foundation/e/apps/GplyHttpClientTest.kt create mode 100644 app/src/test/java/foundation/e/apps/NetworkHandlerTest.kt create mode 100644 app/src/test/java/foundation/e/apps/util/FakeCall.kt 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 acb44e965..6c2929048 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 @@ -61,7 +61,7 @@ class GPlayHttpClient @Inject constructor( const val STATUS_CODE_TIMEOUT = 408 } - private val okHttpClient = OkHttpClient().newBuilder() + var okHttpClient = OkHttpClient().newBuilder() .retryOnConnectionFailure(false) .callTimeout(HTTP_TIMEOUT_IN_SECOND, TimeUnit.SECONDS) .followRedirects(true) diff --git a/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt b/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt index 3096c444c..c8e8e98aa 100644 --- a/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt +++ b/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt @@ -682,7 +682,7 @@ class FusedApiImplTest { Mockito.`when`( gPlayAPIRepository.getCategories(CategoryType.APPLICATION) - ).thenThrow(RuntimeException()) + ).thenThrow() val categoryListResponse = fusedAPIImpl.getCategoriesList(CategoryType.APPLICATION) diff --git a/app/src/test/java/foundation/e/apps/GplyHttpClientTest.kt b/app/src/test/java/foundation/e/apps/GplyHttpClientTest.kt new file mode 100644 index 000000000..8593cffde --- /dev/null +++ b/app/src/test/java/foundation/e/apps/GplyHttpClientTest.kt @@ -0,0 +1,132 @@ +/* + * 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 + +import com.aurora.gplayapi.data.models.PlayResponse +import foundation.e.apps.data.gplay.utils.GPlayHttpClient +import foundation.e.apps.util.FakeCall +import foundation.e.apps.util.MainCoroutineRule +import foundation.e.apps.utils.SystemInfoProvider +import foundation.e.apps.utils.eventBus.AppEvent +import foundation.e.apps.utils.eventBus.EventBus +import io.mockk.every +import io.mockk.mockkObject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import okhttp3.Cache +import okhttp3.OkHttpClient +import okhttp3.RequestBody.Companion.toRequestBody +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue + +@OptIn(ExperimentalCoroutinesApi::class) +class GplyHttpClientTest { + + @Mock + private lateinit var cache: Cache + + @Mock + private lateinit var okHttpClient: OkHttpClient + + private lateinit var call: FakeCall + + private lateinit var gPlayHttpClient: GPlayHttpClient + + @OptIn(ExperimentalCoroutinesApi::class) + @get:Rule + val coroutineTestRule = MainCoroutineRule() + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + gPlayHttpClient = GPlayHttpClient(cache) + gPlayHttpClient.okHttpClient = this.okHttpClient + call = FakeCall() + } + + @Test + fun testPostMapFailWhenStatus401() = runTest { + initMocks() + val response = gPlayHttpClient.post("http://abc.abc", mapOf(), mapOf()) + assertResponse(response) + } + + @Test + fun testPostRequestBodyFailWhenStatus401() = runTest { + initMocks() + val response = gPlayHttpClient.post("http://abc.abc", mapOf(), "".toRequestBody()) + assertResponse(response) + } + + @Test + fun testPostByteArrayRequestBodyFailWhenStatus401() = runTest { + initMocks() + val response = gPlayHttpClient.post("http://abc.abc", mapOf(), "".toByteArray()) + assertResponse(response) + } + + @Test + fun testGetFailWhenStatus401() = runTest { + initMocks() + val response = gPlayHttpClient.get(FakeCall.FAKE_URL, mapOf()) + assertResponse(response) + } + + @Test + fun testGetParamStringFailWhenStatus401() = runTest { + initMocks() + val response = gPlayHttpClient.get(FakeCall.FAKE_URL, mapOf(), "") + assertResponse(response) + } + + @Test + fun testGetMapFailWhenStatus401() = runTest { + initMocks() + val response = gPlayHttpClient.get(FakeCall.FAKE_URL, mapOf(), mapOf()) + assertResponse(response) + } + + @Test + fun testPostAuthFailWhenStatus401() = runTest { + initMocks() + val response = gPlayHttpClient.postAuth("http://abc.abc", "".toByteArray()) + assertResponse(response) + } + + private fun initMocks() { + call.willThrow401 = true + mockkObject(SystemInfoProvider) + every { SystemInfoProvider.getAppBuildInfo() } returns "" + Mockito.`when`(okHttpClient.newCall(any())).thenReturn(call) + } + private suspend fun assertResponse(response: PlayResponse) { + assertFalse(response.isSuccessful) + assertTrue(response.code == 401) + assertTrue(EventBus.events.first() is AppEvent.InvalidAuthEvent) + } + +} diff --git a/app/src/test/java/foundation/e/apps/NetworkHandlerTest.kt b/app/src/test/java/foundation/e/apps/NetworkHandlerTest.kt new file mode 100644 index 000000000..9912cb11a --- /dev/null +++ b/app/src/test/java/foundation/e/apps/NetworkHandlerTest.kt @@ -0,0 +1,9 @@ +package foundation.e.apps + +import foundation.e.apps.data.handleNetworkResult + +class NetworkHandlerTest { + + fun testHandleNetworkResultWhenStatus401() { + } +} \ No newline at end of file diff --git a/app/src/test/java/foundation/e/apps/util/FakeCall.kt b/app/src/test/java/foundation/e/apps/util/FakeCall.kt new file mode 100644 index 000000000..1c6aa71d8 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/util/FakeCall.kt @@ -0,0 +1,79 @@ +/* + * 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.util + +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody +import okhttp3.ResponseBody.Companion.toResponseBody +import okio.Timeout + +class FakeCall : Call { + + var willThrow401 = false + + companion object { + const val FAKE_URL = "https://abc.abc" + } + + private val fakeRequest = Request.Builder().url(FAKE_URL).build() + override fun cancel() { + TODO("Not yet implemented") + } + + override fun clone(): Call { + TODO("Not yet implemented") + } + + override fun enqueue(responseCallback: Callback) { + TODO("Not yet implemented") + } + + override fun execute(): Response { + if (willThrow401) { + return Response.Builder() + .request(fakeRequest) + .protocol(Protocol.HTTP_2) + .message("") + .code(401) + .body("".toResponseBody()) + .build() + } + return Response.Builder().build() + } + + override fun isCanceled(): Boolean { + TODO("Not yet implemented") + } + + override fun isExecuted(): Boolean { + TODO("Not yet implemented") + } + + override fun request(): Request { + TODO("Not yet implemented") + } + + override fun timeout(): Timeout { + TODO("Not yet implemented") + } +} \ No newline at end of file -- GitLab From 144ba93d6edde70a085f9bef4b4d3d38a66640a5 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Wed, 13 Sep 2023 14:52:14 +0600 Subject: [PATCH 16/16] added unit test for loginviewmodel --- .../apps/data/gplay/utils/GPlayHttpClient.kt | 2 + .../foundation/e/apps/NetworkHandlerTest.kt | 9 --- .../e/apps/{ => fused}/FusedApiImplTest.kt | 4 +- .../{ => fused}/FusedApiRepositoryTest.kt | 2 +- .../e/apps/{ => gplay}/GplyHttpClientTest.kt | 23 +++--- .../e/apps/login/LoginViewModelTest.kt | 70 +++++++++++++++++++ 6 files changed, 89 insertions(+), 21 deletions(-) delete mode 100644 app/src/test/java/foundation/e/apps/NetworkHandlerTest.kt rename app/src/test/java/foundation/e/apps/{ => fused}/FusedApiImplTest.kt (99%) rename app/src/test/java/foundation/e/apps/{ => fused}/FusedApiRepositoryTest.kt (98%) rename app/src/test/java/foundation/e/apps/{ => gplay}/GplyHttpClientTest.kt (83%) create mode 100644 app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt 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 6c2929048..2bfd5578a 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 @@ -19,6 +19,7 @@ package foundation.e.apps.data.gplay.utils +import androidx.annotation.VisibleForTesting import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.network.IHttpClient import foundation.e.apps.data.login.AuthObject @@ -61,6 +62,7 @@ class GPlayHttpClient @Inject constructor( const val STATUS_CODE_TIMEOUT = 408 } + @VisibleForTesting var okHttpClient = OkHttpClient().newBuilder() .retryOnConnectionFailure(false) .callTimeout(HTTP_TIMEOUT_IN_SECOND, TimeUnit.SECONDS) diff --git a/app/src/test/java/foundation/e/apps/NetworkHandlerTest.kt b/app/src/test/java/foundation/e/apps/NetworkHandlerTest.kt deleted file mode 100644 index 9912cb11a..000000000 --- a/app/src/test/java/foundation/e/apps/NetworkHandlerTest.kt +++ /dev/null @@ -1,9 +0,0 @@ -package foundation.e.apps - -import foundation.e.apps.data.handleNetworkResult - -class NetworkHandlerTest { - - fun testHandleNetworkResultWhenStatus401() { - } -} \ No newline at end of file diff --git a/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt b/app/src/test/java/foundation/e/apps/fused/FusedApiImplTest.kt similarity index 99% rename from app/src/test/java/foundation/e/apps/FusedApiImplTest.kt rename to app/src/test/java/foundation/e/apps/fused/FusedApiImplTest.kt index c8e8e98aa..816394481 100644 --- a/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt +++ b/app/src/test/java/foundation/e/apps/fused/FusedApiImplTest.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package foundation.e.apps +package foundation.e.apps.fused import android.content.Context import android.text.format.Formatter @@ -25,6 +25,8 @@ import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category import com.aurora.gplayapi.data.models.SearchBundle +import foundation.e.apps.FakePreferenceModule +import foundation.e.apps.R import foundation.e.apps.data.cleanapk.data.categories.Categories import foundation.e.apps.data.cleanapk.data.search.Search import foundation.e.apps.data.cleanapk.repositories.CleanApkRepository diff --git a/app/src/test/java/foundation/e/apps/FusedApiRepositoryTest.kt b/app/src/test/java/foundation/e/apps/fused/FusedApiRepositoryTest.kt similarity index 98% rename from app/src/test/java/foundation/e/apps/FusedApiRepositoryTest.kt rename to app/src/test/java/foundation/e/apps/fused/FusedApiRepositoryTest.kt index 8e04ce98e..801ae4437 100644 --- a/app/src/test/java/foundation/e/apps/FusedApiRepositoryTest.kt +++ b/app/src/test/java/foundation/e/apps/fused/FusedApiRepositoryTest.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package foundation.e.apps +package foundation.e.apps.fused import foundation.e.apps.data.fused.FusedAPIRepository import foundation.e.apps.data.fused.FusedApiImpl diff --git a/app/src/test/java/foundation/e/apps/GplyHttpClientTest.kt b/app/src/test/java/foundation/e/apps/gplay/GplyHttpClientTest.kt similarity index 83% rename from app/src/test/java/foundation/e/apps/GplyHttpClientTest.kt rename to app/src/test/java/foundation/e/apps/gplay/GplyHttpClientTest.kt index 8593cffde..f906f6fb9 100644 --- a/app/src/test/java/foundation/e/apps/GplyHttpClientTest.kt +++ b/app/src/test/java/foundation/e/apps/gplay/GplyHttpClientTest.kt @@ -16,10 +16,11 @@ * along with this program. If not, see . */ -package foundation.e.apps +package foundation.e.apps.gplay import com.aurora.gplayapi.data.models.PlayResponse import foundation.e.apps.data.gplay.utils.GPlayHttpClient +import foundation.e.apps.data.login.AuthObject import foundation.e.apps.util.FakeCall import foundation.e.apps.util.MainCoroutineRule import foundation.e.apps.utils.SystemInfoProvider @@ -69,49 +70,49 @@ class GplyHttpClientTest { } @Test - fun testPostMapFailWhenStatus401() = runTest { + fun testPostWithMapFailedWhenStatus401() = runTest { initMocks() val response = gPlayHttpClient.post("http://abc.abc", mapOf(), mapOf()) assertResponse(response) } @Test - fun testPostRequestBodyFailWhenStatus401() = runTest { + fun testPostWithRequestBodyFailedWhenStatus401() = runTest { initMocks() val response = gPlayHttpClient.post("http://abc.abc", mapOf(), "".toRequestBody()) assertResponse(response) } @Test - fun testPostByteArrayRequestBodyFailWhenStatus401() = runTest { + fun testPostWithByteArrayFailedWhenStatus401() = runTest { initMocks() val response = gPlayHttpClient.post("http://abc.abc", mapOf(), "".toByteArray()) assertResponse(response) } @Test - fun testGetFailWhenStatus401() = runTest { + fun testGetWithoutParamsFailedWhenStatus401() = runTest { initMocks() val response = gPlayHttpClient.get(FakeCall.FAKE_URL, mapOf()) assertResponse(response) } @Test - fun testGetParamStringFailWhenStatus401() = runTest { + fun testGetWithStringParamsFailedWhenStatus401() = runTest { initMocks() val response = gPlayHttpClient.get(FakeCall.FAKE_URL, mapOf(), "") assertResponse(response) } @Test - fun testGetMapFailWhenStatus401() = runTest { + fun testGetWithMapParamsFailedWhenStatus401() = runTest { initMocks() val response = gPlayHttpClient.get(FakeCall.FAKE_URL, mapOf(), mapOf()) assertResponse(response) } @Test - fun testPostAuthFailWhenStatus401() = runTest { + fun testPostAuthFailedWhenStatus401() = runTest { initMocks() val response = gPlayHttpClient.postAuth("http://abc.abc", "".toByteArray()) assertResponse(response) @@ -126,7 +127,9 @@ class GplyHttpClientTest { private suspend fun assertResponse(response: PlayResponse) { assertFalse(response.isSuccessful) assertTrue(response.code == 401) - assertTrue(EventBus.events.first() is AppEvent.InvalidAuthEvent) + val event = EventBus.events.first() + assertTrue(event is AppEvent.InvalidAuthEvent) + assertTrue(event.data is String) + assertTrue(event.data == AuthObject.GPlayAuth::class.java.simpleName) } - } diff --git a/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt b/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt new file mode 100644 index 000000000..01521a1ff --- /dev/null +++ b/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt @@ -0,0 +1,70 @@ +/* + * 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.login + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.enums.User +import foundation.e.apps.data.login.AuthObject +import foundation.e.apps.data.login.LoginSourceRepository +import foundation.e.apps.data.login.LoginViewModel +import okhttp3.Cache +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +class LoginViewModelTest { + + @Mock + private lateinit var loginSourceRepository: LoginSourceRepository + @Mock + private lateinit var cache: Cache + + private lateinit var loginViewModel: LoginViewModel + + @Suppress("unused") + @get:Rule + val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule() + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + loginViewModel = LoginViewModel(loginSourceRepository, cache) + } + + @Test + fun testMarkInvalidAuthObject() { + val authObjectList = mutableListOf( + AuthObject.GPlayAuth( + ResultSupreme.Success(AuthData("aa@aa.com", "feri4234")), User.GOOGLE + ) + ) + loginViewModel.authObjects.value = authObjectList + + loginViewModel.markInvalidAuthObject(AuthObject.GPlayAuth::class.java.simpleName) + val currentAuthObjectList = loginViewModel.authObjects.value as List + val invalidGplayAuth = currentAuthObjectList.find { it is AuthObject.GPlayAuth } + + assert(invalidGplayAuth != null) + assert((invalidGplayAuth as AuthObject.GPlayAuth).result.isUnknownError()) + } +} \ No newline at end of file -- GitLab