diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml
index c9feddb687cb0c3752b75b2fa2bccbfc7c0d016b..7813ffbf56cdf7620ed388b92bb03a51518819cc 100644
--- a/app/detekt-baseline.xml
+++ b/app/detekt-baseline.xml
@@ -28,7 +28,6 @@
MagicNumber:ApkSignatureManager.kt$ApkSignatureManager$1024
MagicNumber:AppDatabase.kt$AppDatabase.Companion.<no name provided>$3
MagicNumber:AppDatabase.kt$AppDatabase.Companion.<no name provided>$4
- MagicNumber:ApplicationApiImpl.kt$ApplicationApiImpl$3
MagicNumber:ApplicationFragment.kt$ApplicationFragment$100f
MagicNumber:ApplicationFragment.kt$ApplicationFragment$15
MagicNumber:ApplicationFragment.kt$ApplicationFragment$3
@@ -74,8 +73,6 @@
MaxLineLength:AppPrivacyInfo.kt$AppPrivacyInfo
MaxLineLength:ApplicationApi.kt$ApplicationApi$suspend fun getGplayAppsByCategory(authData: AuthData, category: String, pageUrl: String?): ResultSupreme<Pair<List<Application>, String>>
MaxLineLength:ApplicationApiImpl.kt$ApplicationApiImpl$(cleanApkAppsRepository.getAppDetails(result.apps[0]._id) as Response<CleanApkApplication>).body()?.app
- MaxLineLength:ApplicationApiImpl.kt$ApplicationApiImpl$val hasGplayLimitedResult = gplayHomes.any { fusedHome -> fusedHome.list.size < THRESHOLD_LIMITED_RESULT_HOME_PAGE }
- MaxLineLength:ApplicationDiffUtil.kt$ApplicationDiffUtil$((oldItem.trackers == LIST_OF_NULL && newItem.trackers.isEmpty()) || oldItem.trackers == newItem.trackers)
MaxLineLength:ApplicationFragment.kt$ApplicationFragment.Companion$"https://gitlab.e.foundation/e/os/apps/-/blob/main/app/src/main/java/foundation/e/apps/data/exodus/repositories/PrivacyScoreRepositoryImpl.kt"
MaxLineLength:CommonUtilsModule.kt$CommonUtilsModule$*
MaxLineLength:DownloadManager.kt$DownloadManager$Timber.e("Download Issue: $downloadId : DownloadManager returns status: $status but the failed because: reason: $reason")
@@ -83,9 +80,6 @@
MaxLineLength:DownloadManagerUtils.kt$DownloadManagerUtils$Timber.d("===> updateDownloadStatus: ${fusedDownload.name}: $downloadId: $numberOfDownloadedItems/${fusedDownload.downloadIdMap.size}")
MaxLineLength:DownloadManagerUtils.kt$DownloadManagerUtils$if
MaxLineLength:DownloadManagerUtils.kt$DownloadManagerUtils$numberOfDownloadedItems == fusedDownload.downloadIdMap.size && numberOfDownloadedItems == fusedDownload.downloadURLList.size
- MaxLineLength:DownloadProgressLD.kt$DownloadProgressLD$cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
- MaxLineLength:DownloadProgressLD.kt$DownloadProgressLD$cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
- MaxLineLength:DownloadProgressLD.kt$DownloadProgressLD$status == DownloadManager.STATUS_SUCCESSFUL || status == DownloadManager.STATUS_FAILED
MaxLineLength:FdroidRepository.kt$FdroidRepository$override suspend
MaxLineLength:FusedManagerImpl.kt$FusedManagerImpl$.
MaxLineLength:FusedManagerImpl.kt$FusedManagerImpl$return
@@ -120,8 +114,6 @@
ReturnCount:AppPrivacyInfoRepositoryImpl.kt$AppPrivacyInfoRepositoryImpl$override suspend fun getAppPrivacyInfo( application: Application, appHandle: String ): Result<AppPrivacyInfo>
ReturnCount:AppPrivacyInfoRepositoryImpl.kt$AppPrivacyInfoRepositoryImpl$private fun getAppPrivacyInfo( application: Application, appTrackerData: List<Report>, ): AppPrivacyInfo
ReturnCount:ApplicationApiImpl.kt$ApplicationApiImpl$override fun isAnyFusedAppUpdated( newApplications: List<Application>, oldApplications: List<Application> ): Boolean
- ReturnCount:ApplicationApiImpl.kt$ApplicationApiImpl$override fun isHomeDataUpdated( newHomeData: List<Home>, oldHomeData: List<Home> ): Boolean
- ReturnCount:ApplicationApiImpl.kt$ApplicationApiImpl$private fun areFusedAppsUpdated( oldHome: Home, newHome: Home, ): Boolean
ReturnCount:DownloadManager.kt$DownloadManager$fun getSizeRequired(downloadId: Long): Long
ReturnCount:DownloadManager.kt$DownloadManager$private fun sanitizeStatus(downloadId: Long, status: Int, reason: Int): Int
ReturnCount:Extensions.kt$fun Context.isNetworkAvailable(): Boolean
diff --git a/app/src/main/java/foundation/e/apps/data/application/ApplicationApi.kt b/app/src/main/java/foundation/e/apps/data/application/ApplicationApi.kt
index 7b81cc960fa4b5885125dc41e63ecb7fc8843502..468ba9190cc27c8b983d18d3dea30cf1f7e8ad1c 100644
--- a/app/src/main/java/foundation/e/apps/data/application/ApplicationApi.kt
+++ b/app/src/main/java/foundation/e/apps/data/application/ApplicationApi.kt
@@ -29,10 +29,6 @@ interface ApplicationApi {
fun getApplicationCategoryPreference(): List
- suspend fun getHomeScreenData(
- authData: AuthData,
- ): LiveData>>
-
/*
* Return three elements from the function.
* - List : List of categories.
diff --git a/app/src/main/java/foundation/e/apps/data/application/ApplicationApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/ApplicationApiImpl.kt
index bcba66eeca0b737169ad4b140c97841fdfb95b6d..6db8ae0678add4809d7285d6519ad5c7ef46e746 100644
--- a/app/src/main/java/foundation/e/apps/data/application/ApplicationApiImpl.kt
+++ b/app/src/main/java/foundation/e/apps/data/application/ApplicationApiImpl.kt
@@ -20,8 +20,6 @@ package foundation.e.apps.data.application
import android.content.Context
import android.text.format.Formatter
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.liveData
import com.aurora.gplayapi.Constants
import com.aurora.gplayapi.SearchSuggestEntry
import com.aurora.gplayapi.data.models.App
@@ -32,28 +30,26 @@ import com.aurora.gplayapi.data.models.StreamCluster
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.R
import foundation.e.apps.data.ResultSupreme
+import foundation.e.apps.data.application.ApplicationApi.Companion.APP_TYPE_ANY
+import foundation.e.apps.data.application.ApplicationApi.Companion.APP_TYPE_OPEN
+import foundation.e.apps.data.application.ApplicationApi.Companion.APP_TYPE_PWA
+import foundation.e.apps.data.application.data.Application
+import foundation.e.apps.data.application.data.Category
+import foundation.e.apps.data.application.data.Home
+import foundation.e.apps.data.application.data.Ratings
+import foundation.e.apps.data.application.utils.CategoryType
+import foundation.e.apps.data.application.utils.CategoryUtils
import foundation.e.apps.data.cleanapk.CleanApkDownloadInfoFetcher
import foundation.e.apps.data.cleanapk.data.categories.Categories
-import foundation.e.apps.data.cleanapk.data.home.HomeScreen
import foundation.e.apps.data.cleanapk.data.search.Search
import foundation.e.apps.data.cleanapk.repositories.CleanApkRepository
import foundation.e.apps.data.enums.AppTag
import foundation.e.apps.data.enums.FilterLevel
import foundation.e.apps.data.enums.Origin
import foundation.e.apps.data.enums.ResultStatus
-import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.enums.Status
import foundation.e.apps.data.enums.Type
import foundation.e.apps.data.enums.isUnFiltered
-import foundation.e.apps.data.application.ApplicationApi.Companion.APP_TYPE_ANY
-import foundation.e.apps.data.application.ApplicationApi.Companion.APP_TYPE_OPEN
-import foundation.e.apps.data.application.ApplicationApi.Companion.APP_TYPE_PWA
-import foundation.e.apps.data.application.data.Application
-import foundation.e.apps.data.application.data.Category
-import foundation.e.apps.data.application.data.Home
-import foundation.e.apps.data.application.data.Ratings
-import foundation.e.apps.data.application.utils.CategoryType
-import foundation.e.apps.data.application.utils.CategoryUtils
import foundation.e.apps.data.fusedDownload.models.FusedDownload
import foundation.e.apps.data.handleNetworkResult
import foundation.e.apps.data.login.AuthObject
@@ -66,8 +62,6 @@ import foundation.e.apps.utils.eventBus.AppEvent
import foundation.e.apps.utils.eventBus.EventBus
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.MainScope
-import kotlinx.coroutines.async
-import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import retrofit2.Response
import timber.log.Timber
@@ -76,7 +70,6 @@ import javax.inject.Named
import javax.inject.Singleton
import com.aurora.gplayapi.data.models.Category as GplayapiCategory
import foundation.e.apps.data.cleanapk.data.app.Application as CleanApkApplication
-import foundation.e.apps.data.cleanapk.data.home.Home as CleanApkHome
typealias FusedHomeDeferred = Deferred>>
@@ -95,7 +88,6 @@ class ApplicationApiImpl @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 THRESHOLD_LIMITED_RESULT_HOME_PAGE = 4
private const val KEYWORD_TEST_SEARCH = "facebook"
}
@@ -107,97 +99,6 @@ class ApplicationApiImpl @Inject constructor(
return prefs
}
- override suspend fun getHomeScreenData(
- authData: AuthData,
- ): LiveData>> {
-
- val list = mutableListOf()
- var resultGplay: FusedHomeDeferred? = null
- var resultOpenSource: FusedHomeDeferred? = null
- var resultPWA: FusedHomeDeferred? = null
-
- return liveData {
- coroutineScope {
-
- if (preferenceManagerModule.isGplaySelected()) {
- resultGplay = async { loadHomeData(list, Source.GPLAY, authData) }
- }
-
- if (preferenceManagerModule.isOpenSourceSelected()) {
- resultOpenSource = async { loadHomeData(list, Source.OPEN, authData) }
- }
-
- if (preferenceManagerModule.isPWASelected()) {
- resultPWA = async { loadHomeData(list, Source.PWA, authData) }
- }
-
- resultGplay?.await()?.let {
- emit(it)
- }
-
- resultOpenSource?.await()?.let {
- emit(it)
- }
-
- resultPWA?.await()?.let {
- emit(it)
- }
- }
- }
- }
-
- private suspend fun loadHomeData(
- priorList: MutableList,
- source: Source,
- authData: AuthData,
- ): ResultSupreme> {
-
- val result = when (source) {
- Source.GPLAY -> handleNetworkResult> {
- priorList.addAll(fetchGPlayHome(authData))
- priorList
- }
-
- Source.OPEN -> handleNetworkResult {
- val response =
- (cleanApkAppsRepository.getHomeScreenData() as Response).body()
- response?.home?.let {
- priorList.addAll(generateCleanAPKHome(it, APP_TYPE_OPEN))
- }
- priorList
- }
-
- Source.PWA -> handleNetworkResult {
- val response =
- (cleanApkPWARepository.getHomeScreenData() as Response).body()
- response?.home?.let {
- priorList.addAll(generateCleanAPKHome(it, APP_TYPE_PWA))
- }
- priorList
- }
- }
-
- setHomeErrorMessage(result.getResultStatus(), source)
- priorList.sortByDescending {
- when (it.source) {
- APP_TYPE_OPEN -> 2
- APP_TYPE_PWA -> 1
- else -> 3
- }
- }
- return ResultSupreme.create(result.getResultStatus(), priorList)
- }
-
- private fun setHomeErrorMessage(apiStatus: ResultStatus, source: Source) {
- if (apiStatus != ResultStatus.OK) {
- apiStatus.message = when (source) {
- Source.GPLAY -> ("GPlay home loading error\n" + apiStatus.message).trim()
- Source.OPEN -> ("Open Source home loading error\n" + apiStatus.message).trim()
- Source.PWA -> ("PWA home loading error\n" + apiStatus.message).trim()
- }
- }
- }
-
/*
* Return three elements from the function.
* - List : List of categories.
@@ -756,7 +657,7 @@ class ApplicationApiImpl @Inject constructor(
/*
* Handy method to run on an instance of FusedApp to update its filter level.
*/
- private suspend fun Application.updateFilterLevel(authData: AuthData?) {
+ suspend fun Application.updateFilterLevel(authData: AuthData?) {
this.filterLevel = getAppFilterLevel(this, authData)
}
@@ -1065,111 +966,6 @@ class ApplicationApiImpl @Inject constructor(
return gPlayFusedApp
}
- /*
- * Home screen-related internal functions
- */
-
- private suspend fun generateCleanAPKHome(home: CleanApkHome, appType: String): List {
- val list = mutableListOf()
- val headings = if (appType == APP_TYPE_OPEN) {
- getOpenSourceHomeCategories()
- } else {
- getPWAHomeCategories()
- }
- headings.forEach { (key, value) ->
- when (key) {
- "top_updated_apps" -> {
- prepareApps(home.top_updated_apps, list, value)
- }
-
- "top_updated_games" -> {
- prepareApps(home.top_updated_games, list, value)
- }
-
- "popular_apps" -> {
- prepareApps(home.popular_apps, list, value)
- }
-
- "popular_games" -> {
- prepareApps(home.popular_games, list, value)
- }
-
- "popular_apps_in_last_24_hours" -> {
- prepareApps(home.popular_apps_in_last_24_hours, list, value)
- }
-
- "popular_games_in_last_24_hours" -> {
- prepareApps(home.popular_games_in_last_24_hours, list, value)
- }
-
- "discover" -> {
- prepareApps(home.discover, list, value)
- }
- }
- }
-
- return list.map {
- it.source = appType
- it
- }
- }
-
- private suspend fun prepareApps(
- appList: List,
- list: MutableList,
- value: String
- ) {
- if (appList.isNotEmpty()) {
- appList.forEach {
- it.updateStatus()
- it.updateType()
- it.updateFilterLevel(null)
- }
- list.add(Home(value, appList))
- }
- }
-
- private fun getPWAHomeCategories() = mapOf(
- "popular_apps" to context.getString(R.string.popular_apps),
- "popular_games" to context.getString(R.string.popular_games),
- "discover" to context.getString(R.string.discover_pwa)
- )
-
- private fun getOpenSourceHomeCategories() = mapOf(
- "top_updated_apps" to context.getString(R.string.top_updated_apps),
- "top_updated_games" to context.getString(R.string.top_updated_games),
- "popular_apps_in_last_24_hours" to context.getString(R.string.popular_apps_in_last_24_hours),
- "popular_games_in_last_24_hours" to context.getString(R.string.popular_games_in_last_24_hours),
- "discover" to context.getString(R.string.discover)
- )
-
- private suspend fun fetchGPlayHome(authData: AuthData): List {
- val list = mutableListOf()
- val gplayHomeData = gplayRepository.getHomeScreenData() as Map>
- gplayHomeData.map {
- val fusedApps = it.value.map { app ->
- app.transformToFusedApp().apply {
- updateFilterLevel(authData)
- }
- }
- list.add(Home(it.key, fusedApps))
- }
-
- handleLimitedResult(list)
- Timber.d("HomePageData: $list")
-
- return list
- }
-
- private fun handleLimitedResult(homeList: List) {
- val gplayHomes = homeList.filter { fusedHome -> fusedHome.source.isEmpty() }
- val hasGplayLimitedResult = gplayHomes.any { fusedHome -> fusedHome.list.size < THRESHOLD_LIMITED_RESULT_HOME_PAGE }
- if (hasGplayLimitedResult) {
- Timber.w("Limited result is found for homepage...")
- refreshToken()
- }
- }
-
private fun refreshToken() {
MainScope().launch {
EventBus.invokeEvent(
diff --git a/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt b/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a537d6bf7c50141c233bc309178bb193609ef18d
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.application
+
+import com.aurora.gplayapi.Constants
+import com.aurora.gplayapi.data.models.AuthData
+import foundation.e.apps.data.application.data.Application
+import foundation.e.apps.data.application.data.Home
+import foundation.e.apps.data.enums.FilterLevel
+import foundation.e.apps.data.enums.Origin
+import foundation.e.apps.data.enums.Status
+import foundation.e.apps.data.playstore.PlayStoreRepository
+import foundation.e.apps.install.pkg.PWAManagerModule
+import foundation.e.apps.install.pkg.PkgManagerModule
+import javax.inject.Inject
+import javax.inject.Named
+import javax.inject.Singleton
+
+@Singleton
+class ApplicationDataManager @Inject constructor(
+ @Named("gplayRepository") private val gplayRepository: PlayStoreRepository,
+ private val pkgManagerModule: PkgManagerModule,
+ private val pwaManagerModule: PWAManagerModule
+) {
+ suspend fun updateFilterLevel(authData: AuthData?, application: Application) {
+ application.filterLevel = getAppFilterLevel(application, authData)
+ }
+
+ suspend fun prepareApps(
+ appList: List,
+ list: MutableList,
+ value: String
+ ) {
+ if (appList.isNotEmpty()) {
+ appList.forEach {
+ it.updateType()
+ updateStatus(it)
+ updateFilterLevel(null, it)
+ }
+ list.add(Home(value, appList))
+ }
+ }
+
+ private suspend fun getAppFilterLevel(application: Application, authData: AuthData?): FilterLevel {
+ return when {
+ application.package_name.isBlank() -> FilterLevel.UNKNOWN
+ !application.isFree && application.price.isBlank() -> FilterLevel.UI
+ application.origin == Origin.CLEANAPK -> FilterLevel.NONE
+ !isRestricted(application) -> FilterLevel.NONE
+ authData == null -> FilterLevel.UNKNOWN // cannot determine for gplay app
+ !isApplicationVisible(application) -> FilterLevel.DATA
+ application.originalSize == 0L -> FilterLevel.UI
+ !isDownloadable(application) -> FilterLevel.UI
+ else -> FilterLevel.NONE
+ }
+ }
+
+ private fun isRestricted(application: Application): Boolean {
+ return application.restriction != Constants.Restriction.NOT_RESTRICTED
+ }
+
+ /*
+ * Some apps are simply not visible.
+ * Example: com.skype.m2
+ */
+ private suspend fun isApplicationVisible(application: Application): Boolean {
+ return kotlin.runCatching { gplayRepository.getAppDetails(application.package_name) }.isSuccess
+ }
+
+ /*
+ * Some apps are visible but not downloadable.
+ * Example: com.riotgames.league.wildrift
+ */
+ private suspend fun isDownloadable(application: Application): Boolean {
+ return kotlin.runCatching {
+ gplayRepository.getDownloadInfo(
+ application.package_name,
+ application.latest_version_code,
+ application.offer_type,
+ )
+ }.isSuccess
+ }
+
+ fun updateStatus(application: Application) {
+ if (application.status != Status.INSTALLATION_ISSUE) {
+ application.status = getFusedAppInstallationStatus(application)
+ }
+ }
+
+ /*
+ * Get fused app installation status.
+ * Applicable for both native apps and PWAs.
+ *
+ * Recommended to use this instead of [PkgManagerModule.getPackageStatus].
+ */
+ fun getFusedAppInstallationStatus(application: Application): Status {
+ return if (application.is_pwa) {
+ pwaManagerModule.getPwaStatus(application)
+ } else {
+ pkgManagerModule.getPackageStatus(application.package_name, application.latest_version_code)
+ }
+ }
+
+}
diff --git a/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt b/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt
index ab7d541a8f612a7e1961455ccebfd7069b932c32..a7526bb12c27316953cb2fb444a6b18ec3558bc1 100644
--- a/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt
+++ b/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt
@@ -37,10 +37,13 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class ApplicationRepository @Inject constructor(private val applicationAPIImpl: ApplicationApi) {
+class ApplicationRepository @Inject constructor(
+ private val applicationAPIImpl: ApplicationApi,
+ private val homeApi: HomeApi
+) {
suspend fun getHomeScreenData(authData: AuthData): LiveData>> {
- return applicationAPIImpl.getHomeScreenData(authData)
+ return homeApi.fetchHomeScreenData(authData)
}
fun getApplicationCategoryPreference(): List {
diff --git a/app/src/main/java/foundation/e/apps/data/application/HomeApi.kt b/app/src/main/java/foundation/e/apps/data/application/HomeApi.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7e677be457146de4c67dafb31cf277a797433a8e
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/data/application/HomeApi.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.application
+
+import androidx.lifecycle.LiveData
+import com.aurora.gplayapi.data.models.AuthData
+import foundation.e.apps.data.ResultSupreme
+import foundation.e.apps.data.application.data.Home
+
+interface HomeApi {
+ suspend fun fetchHomeScreenData(
+ authData: AuthData,
+ ): LiveData>>
+}
diff --git a/app/src/main/java/foundation/e/apps/data/application/HomeApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/HomeApiImpl.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9bc92f99da8e3a404fe2cf002736c2d0e286166c
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/data/application/HomeApiImpl.kt
@@ -0,0 +1,268 @@
+/*
+ * 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.application
+
+import android.content.Context
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.liveData
+import com.aurora.gplayapi.data.models.App
+import com.aurora.gplayapi.data.models.AuthData
+import dagger.hilt.android.qualifiers.ApplicationContext
+import foundation.e.apps.R
+import foundation.e.apps.data.ResultSupreme
+import foundation.e.apps.data.application.data.Home
+import foundation.e.apps.data.application.utils.toApplication
+import foundation.e.apps.data.cleanapk.data.home.HomeScreen
+import foundation.e.apps.data.cleanapk.repositories.CleanApkRepository
+import foundation.e.apps.data.enums.ResultStatus
+import foundation.e.apps.data.enums.Source
+import foundation.e.apps.data.handleNetworkResult
+import foundation.e.apps.data.login.AuthObject
+import foundation.e.apps.data.playstore.PlayStoreRepository
+import foundation.e.apps.data.preference.PreferenceManagerModule
+import foundation.e.apps.utils.eventBus.AppEvent
+import foundation.e.apps.utils.eventBus.EventBus
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import retrofit2.Response
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Named
+import foundation.e.apps.data.cleanapk.data.home.Home as CleanApkHome
+
+class HomeApiImpl @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val preferenceManagerModule: PreferenceManagerModule,
+ @Named("gplayRepository") private val gplayRepository: PlayStoreRepository,
+ @Named("cleanApkAppsRepository") private val cleanApkAppsRepository: CleanApkRepository,
+ @Named("cleanApkPWARepository") private val cleanApkPWARepository: CleanApkRepository,
+ private val applicationDataManager: ApplicationDataManager
+) : HomeApi {
+
+ companion object {
+ private const val THRESHOLD_LIMITED_RESULT_HOME_PAGE = 4
+ }
+
+ private enum class AppSourceWeight {
+ GPLAY,
+ OPEN_SOURCE,
+ PWA
+ }
+
+ override suspend fun fetchHomeScreenData(authData: AuthData): LiveData>> {
+ val list = mutableListOf()
+ var resultGplay: FusedHomeDeferred? = null
+ var resultOpenSource: FusedHomeDeferred? = null
+ var resultPWA: FusedHomeDeferred? = null
+
+ return liveData {
+ coroutineScope {
+
+ if (preferenceManagerModule.isGplaySelected()) {
+ resultGplay = async { loadHomeData(list, Source.GPLAY, authData) }
+ }
+
+ if (preferenceManagerModule.isOpenSourceSelected()) {
+ resultOpenSource = async { loadHomeData(list, Source.OPEN, authData) }
+ }
+
+ if (preferenceManagerModule.isPWASelected()) {
+ resultPWA = async { loadHomeData(list, Source.PWA, authData) }
+ }
+
+ resultGplay?.await()?.let {
+ emit(it)
+ }
+
+ resultOpenSource?.await()?.let {
+ emit(it)
+ }
+
+ resultPWA?.await()?.let {
+ emit(it)
+ }
+ }
+ }
+ }
+
+ private suspend fun loadHomeData(
+ priorList: MutableList,
+ source: Source,
+ authData: AuthData,
+ ): ResultSupreme> {
+
+ val result = when (source) {
+ Source.GPLAY -> handleNetworkResult {
+ fetchGPlayHome(authData, priorList)
+ }
+
+ Source.OPEN -> handleNetworkResult {
+ handleCleanApkHomes(priorList, ApplicationApi.APP_TYPE_OPEN)
+ }
+
+ Source.PWA -> handleNetworkResult {
+ handleCleanApkHomes(priorList, ApplicationApi.APP_TYPE_PWA)
+ }
+ }
+
+ setHomeErrorMessage(result.getResultStatus(), source)
+ priorList.sortBy {
+ when (it.source) {
+ ApplicationApi.APP_TYPE_OPEN -> AppSourceWeight.OPEN_SOURCE.ordinal
+ ApplicationApi.APP_TYPE_PWA -> AppSourceWeight.PWA.ordinal
+ else -> AppSourceWeight.GPLAY.ordinal
+ }
+ }
+
+ return ResultSupreme.create(result.getResultStatus(), priorList)
+ }
+
+ private suspend fun handleCleanApkHomes(
+ priorList: MutableList,
+ appType: String
+ ): MutableList {
+ val response = if (appType == ApplicationApi.APP_TYPE_OPEN) {
+ (cleanApkAppsRepository.getHomeScreenData() as Response).body()
+ } else {
+ (cleanApkPWARepository.getHomeScreenData() as Response).body()
+ }
+
+ response?.home?.let {
+ priorList.addAll(generateCleanAPKHome(it, appType))
+ }
+
+ return priorList
+ }
+
+ private suspend fun generateCleanAPKHome(home: CleanApkHome, appType: String): List {
+ val list = mutableListOf()
+ val headings = if (appType == ApplicationApi.APP_TYPE_OPEN) {
+ getOpenSourceHomeCategories()
+ } else {
+ getPWAHomeCategories()
+ }
+
+ headings.forEach { (key, value) ->
+ when (key) {
+ "top_updated_apps" -> {
+ applicationDataManager.prepareApps(home.top_updated_apps, list, value)
+ }
+
+ "top_updated_games" -> {
+ applicationDataManager.prepareApps(home.top_updated_games, list, value)
+ }
+
+ "popular_apps" -> {
+ applicationDataManager.prepareApps(home.popular_apps, list, value)
+ }
+
+ "popular_games" -> {
+ applicationDataManager.prepareApps(home.popular_games, list, value)
+ }
+
+ "popular_apps_in_last_24_hours" -> {
+ applicationDataManager.prepareApps(home.popular_apps_in_last_24_hours, list, value)
+ }
+
+ "popular_games_in_last_24_hours" -> {
+ applicationDataManager.prepareApps(home.popular_games_in_last_24_hours, list, value)
+ }
+
+ "discover" -> {
+ applicationDataManager.prepareApps(home.discover, list, value)
+ }
+ }
+ }
+
+ return list.map {
+ it.source = appType
+ it
+ }
+ }
+
+ private fun getPWAHomeCategories() = mapOf(
+ "popular_apps" to context.getString(R.string.popular_apps),
+ "popular_games" to context.getString(R.string.popular_games),
+ "discover" to context.getString(R.string.discover_pwa)
+ )
+
+ private fun getOpenSourceHomeCategories() = mapOf(
+ "top_updated_apps" to context.getString(R.string.top_updated_apps),
+ "top_updated_games" to context.getString(R.string.top_updated_games),
+ "popular_apps_in_last_24_hours" to context.getString(R.string.popular_apps_in_last_24_hours),
+ "popular_games_in_last_24_hours" to context.getString(R.string.popular_games_in_last_24_hours),
+ "discover" to context.getString(R.string.discover)
+ )
+
+ private fun setHomeErrorMessage(apiStatus: ResultStatus, source: Source) {
+ if (apiStatus != ResultStatus.OK) {
+ apiStatus.message = when (source) {
+ Source.GPLAY -> ("GPlay home loading error\n" + apiStatus.message).trim()
+ Source.OPEN -> ("Open Source home loading error\n" + apiStatus.message).trim()
+ Source.PWA -> ("PWA home loading error\n" + apiStatus.message).trim()
+ }
+ }
+ }
+
+ private suspend fun fetchGPlayHome(
+ authData: AuthData,
+ priorList: MutableList
+ ): List {
+ val list = mutableListOf()
+ val gplayHomeData = gplayRepository.getHomeScreenData() as Map>
+ gplayHomeData.map {
+ val fusedApps = it.value.map { app ->
+ app.toApplication(context).apply {
+ applicationDataManager.updateStatus(this)
+ applicationDataManager.updateFilterLevel(authData, this)
+ }
+ }
+ list.add(Home(it.key, fusedApps))
+ }
+
+ handleLimitedResult(list)
+ Timber.d("HomePageData: $list")
+
+ priorList.addAll(list)
+ return priorList
+ }
+
+ private fun handleLimitedResult(homeList: List) {
+ val gplayHomes = homeList.filter { fusedHome -> fusedHome.source.isEmpty() }
+ val hasGplayLimitedResult = gplayHomes.any { fusedHome ->
+ fusedHome.list.size < THRESHOLD_LIMITED_RESULT_HOME_PAGE
+ }
+
+ if (hasGplayLimitedResult) {
+ Timber.w("Limited result is found for homepage...")
+ refreshToken()
+ }
+ }
+
+ private fun refreshToken() {
+ MainScope().launch {
+ EventBus.invokeEvent(
+ AppEvent.InvalidAuthEvent(AuthObject.GPlayAuth::class.java.simpleName)
+ )
+ }
+ }
+
+}
diff --git a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt
index d8a544060daf85e3f5feef3b898a5c3755f545c2..7b42d7e71ae69723d3e0c22d362c60dc54286d5b 100644
--- a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt
+++ b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt
@@ -93,4 +93,8 @@ data class Application(
*/
var filterLevel: FilterLevel = FilterLevel.UNKNOWN,
var isGplayReplaced: Boolean = false
-)
+) {
+ fun updateType() {
+ this.type = if (this.is_pwa) Type.PWA else Type.NATIVE
+ }
+}
diff --git a/app/src/main/java/foundation/e/apps/data/application/utils/GplayApiExtensions.kt b/app/src/main/java/foundation/e/apps/data/application/utils/GplayApiExtensions.kt
new file mode 100644
index 0000000000000000000000000000000000000000..88cdb6451ebf0bfa5e9fd35538bbb04d779151ec
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/data/application/utils/GplayApiExtensions.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.application.utils
+
+import android.content.Context
+import android.text.format.Formatter
+import com.aurora.gplayapi.data.models.App
+import com.aurora.gplayapi.data.models.Artwork
+import foundation.e.apps.data.application.data.Application
+import foundation.e.apps.data.application.data.Ratings
+import foundation.e.apps.data.enums.Origin
+
+fun App.toApplication(context: Context): Application {
+ val app = Application(
+ _id = this.id.toString(),
+ author = this.developerName,
+ category = this.categoryName,
+ description = this.description,
+ perms = this.permissions,
+ icon_image_path = this.iconArtwork.url,
+ last_modified = this.updatedOn,
+ latest_version_code = this.versionCode,
+ latest_version_number = this.versionName,
+ name = this.displayName,
+ other_images_path = this.screenshots.toList(),
+ package_name = this.packageName,
+ ratings = Ratings(
+ usageQualityScore =
+ this.labeledRating.run {
+ if (isNotEmpty()) {
+ this.replace(",", ".").toDoubleOrNull() ?: -1.0
+ } else -1.0
+ }
+ ),
+ offer_type = this.offerType,
+ origin = Origin.GPLAY,
+ shareUrl = this.shareUrl,
+ originalSize = this.size,
+ appSize = Formatter.formatFileSize(context, this.size),
+ isFree = this.isFree,
+ price = this.price,
+ restriction = this.restriction,
+ )
+ return app
+}
+
+private fun MutableList.toList(): List {
+ val list = mutableListOf()
+ this.forEach {
+ list.add(it.url)
+ }
+ return list
+}
diff --git a/app/src/main/java/foundation/e/apps/di/DataModule.kt b/app/src/main/java/foundation/e/apps/di/DataModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f464ef454257050adf9f99d1b1903f6c8d7ba017
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/di/DataModule.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.di
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import foundation.e.apps.data.application.HomeApi
+import foundation.e.apps.data.application.HomeApiImpl
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+interface DataModule {
+
+ @Singleton
+ @Binds
+ fun getHomeApi(homeApiImpl: HomeApiImpl): HomeApi
+}
diff --git a/app/src/test/java/foundation/e/apps/fused/ApplicationApiImplTest.kt b/app/src/test/java/foundation/e/apps/fused/ApplicationApiImplTest.kt
index 165af4f725a76f4ed16cd00df98d55ce49d92b47..869e8851eb2a5ce24b5efd42e1fabaaa5c9fdca4 100644
--- a/app/src/test/java/foundation/e/apps/fused/ApplicationApiImplTest.kt
+++ b/app/src/test/java/foundation/e/apps/fused/ApplicationApiImplTest.kt
@@ -756,37 +756,6 @@ class ApplicationApiImplTest {
assertEquals("getSearchResult", 4, size)
}
- @Test
- fun testHomeScreenDataWhenDataIsLimited() = runTest {
- val newAppList = mutableListOf(
- App("foundation.e.demoone"),
- App("foundation.e.demotwo"),
- App("foundation.e.demothree"),
- )
-
- var newHomeData = mapOf>(Pair("Top Free Apps", newAppList))
- preferenceManagerModule.isGplaySelectedFake = true
-
- formatterMocked.`when` { Formatter.formatFileSize(any(), any()) }.thenReturn("15MB")
- Mockito.`when`(gPlayAPIRepository.getHomeScreenData()).thenReturn(newHomeData)
- Mockito.`when`(gPlayAPIRepository.getAppDetails(anyString())).thenReturn(App("foundation.e.demothree"))
- Mockito.`when`(gPlayAPIRepository.getDownloadInfo(anyString(), any(), any())).thenReturn(listOf())
- Mockito.`when`(pkgManagerModule.getPackageStatus(any(), any())).thenReturn(Status.UNAVAILABLE)
-
- var hasLimitedDataFound = false
- val job = launch {
- EventBus.events.collect {
- hasLimitedDataFound = true
- }
- }
-
- fusedAPIImpl.getHomeScreenData(AUTH_DATA).getOrAwaitValue()
- delay(500)
- job.cancel()
-
- assert(hasLimitedDataFound)
- }
-
@Test
fun testSearchResultWhenDataIsLimited() = runTest {
preferenceManagerModule.isGplaySelectedFake = true
diff --git a/app/src/test/java/foundation/e/apps/fused/ApplicationApiRepositoryTest.kt b/app/src/test/java/foundation/e/apps/fused/ApplicationApiRepositoryTest.kt
index bde38b21767e3d73815c9ce1fc293026f92fcc81..88e5ff2a97e632fd9626f5c96822699293763108 100644
--- a/app/src/test/java/foundation/e/apps/fused/ApplicationApiRepositoryTest.kt
+++ b/app/src/test/java/foundation/e/apps/fused/ApplicationApiRepositoryTest.kt
@@ -19,6 +19,7 @@ package foundation.e.apps.fused
import foundation.e.apps.data.application.ApplicationRepository
import foundation.e.apps.data.application.ApplicationApiImpl
+import foundation.e.apps.data.application.HomeApi
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
@@ -31,11 +32,13 @@ class ApplicationApiRepositoryTest {
private lateinit var applicationRepository: ApplicationRepository
@Mock
private lateinit var fusedAPIImpl: ApplicationApiImpl
+ @Mock
+ private lateinit var homeApi: HomeApi
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
- applicationRepository = ApplicationRepository(fusedAPIImpl)
+ applicationRepository = ApplicationRepository(fusedAPIImpl, homeApi)
}
@Test
diff --git a/app/src/test/java/foundation/e/apps/home/HomeApiTest.kt b/app/src/test/java/foundation/e/apps/home/HomeApiTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b32e0f941d00d2d49d76b93ebb2241dac84bf6ad
--- /dev/null
+++ b/app/src/test/java/foundation/e/apps/home/HomeApiTest.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.home
+
+import android.content.Context
+import android.text.format.Formatter
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.aurora.gplayapi.data.models.App
+import com.aurora.gplayapi.data.models.AuthData
+import foundation.e.apps.FakePreferenceModule
+import foundation.e.apps.data.application.ApplicationDataManager
+import foundation.e.apps.data.application.HomeApi
+import foundation.e.apps.data.application.HomeApiImpl
+import foundation.e.apps.data.cleanapk.repositories.CleanApkRepository
+import foundation.e.apps.data.enums.Status
+import foundation.e.apps.data.playstore.PlayStoreRepository
+import foundation.e.apps.install.pkg.PWAManagerModule
+import foundation.e.apps.install.pkg.PkgManagerModule
+import foundation.e.apps.util.MainCoroutineRule
+import foundation.e.apps.util.getOrAwaitValue
+import foundation.e.apps.utils.eventBus.EventBus
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.Mock
+import org.mockito.MockedStatic
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+
+class HomeApiTest {
+
+ // Run tasks synchronously
+ @Rule
+ @JvmField
+ val instantExecutorRule = InstantTaskExecutorRule()
+
+ // Sets the main coroutines dispatcher to a TestCoroutineScope for unit testing.
+ @ExperimentalCoroutinesApi
+ @get:Rule
+ var mainCoroutineRule = MainCoroutineRule()
+
+ private lateinit var homeApi: HomeApi
+
+ private lateinit var applicationDataManager: ApplicationDataManager
+
+ @Mock
+ private lateinit var pwaManagerModule: PWAManagerModule
+
+ @Mock
+ private lateinit var pkgManagerModule: PkgManagerModule
+
+ @Mock
+ private lateinit var context: Context
+
+ @Mock
+ private lateinit var cleanApkAppsRepository: CleanApkRepository
+
+ @Mock
+ private lateinit var cleanApkPWARepository: CleanApkRepository
+
+ @Mock
+ private lateinit var gPlayAPIRepository: PlayStoreRepository
+
+ private lateinit var preferenceManagerModule: FakePreferenceModule
+
+ private lateinit var formatterMocked: MockedStatic
+
+ companion object {
+ private val AUTH_DATA = AuthData("e@e.email", "AtadyMsIAtadyM")
+ }
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.openMocks(this)
+ formatterMocked = Mockito.mockStatic(Formatter::class.java)
+ preferenceManagerModule = FakePreferenceModule(context)
+ applicationDataManager =
+ ApplicationDataManager(gPlayAPIRepository, pkgManagerModule, pwaManagerModule)
+ homeApi = HomeApiImpl(
+ context,
+ preferenceManagerModule,
+ gPlayAPIRepository,
+ cleanApkAppsRepository,
+ cleanApkPWARepository,
+ applicationDataManager
+ )
+ }
+
+ @Test
+ fun testHomeScreenDataWhenDataIsLimited() = runTest {
+ val newAppList = mutableListOf(
+ App("foundation.e.demoone"),
+ App("foundation.e.demotwo"),
+ App("foundation.e.demothree"),
+ )
+
+ var newHomeData = mapOf>(Pair("Top Free Apps", newAppList))
+ preferenceManagerModule.isGplaySelectedFake = true
+
+ formatterMocked.`when` { Formatter.formatFileSize(any(), any()) }.thenReturn("15MB")
+ Mockito.`when`(gPlayAPIRepository.getHomeScreenData()).thenReturn(newHomeData)
+ Mockito.`when`(gPlayAPIRepository.getAppDetails(ArgumentMatchers.anyString())).thenReturn(
+ App("foundation.e.demothree")
+ )
+ Mockito.`when`(
+ gPlayAPIRepository.getDownloadInfo(
+ ArgumentMatchers.anyString(),
+ any(),
+ any()
+ )
+ ).thenReturn(listOf())
+ Mockito.`when`(pkgManagerModule.getPackageStatus(any(), any()))
+ .thenReturn(Status.UNAVAILABLE)
+
+ var hasLimitedDataFound = false
+ val job = launch {
+ EventBus.events.collect {
+ hasLimitedDataFound = true
+ }
+ }
+
+ homeApi.fetchHomeScreenData(AUTH_DATA).getOrAwaitValue()
+ delay(500)
+ job.cancel()
+
+ assert(hasLimitedDataFound)
+ }
+}
\ No newline at end of file