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