Loading app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt +90 −10 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package foundation.e.apps.data.application import androidx.lifecycle.LiveData import androidx.lifecycle.liveData import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.Stores import foundation.e.apps.data.application.apps.AppsApi Loading @@ -27,19 +28,20 @@ import foundation.e.apps.data.application.category.CategoryApi import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Home import foundation.e.apps.data.application.downloadInfo.DownloadInfoApi import foundation.e.apps.data.application.home.HomeApi import foundation.e.apps.data.application.utils.CategoryType import foundation.e.apps.data.enums.FilterLevel 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.handleNetworkResult import foundation.e.apps.data.install.models.AppInstall import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import javax.inject.Inject import javax.inject.Singleton @Singleton class ApplicationRepository @Inject constructor( private val homeApi: HomeApi, private val categoryApi: CategoryApi, private val appsApi: AppsApi, private val downloadInfoApi: DownloadInfoApi, Loading @@ -51,8 +53,86 @@ class ApplicationRepository @Inject constructor( const val APP_TYPE_PWA = "pwa" } suspend fun getHomeScreenData(): LiveData<ResultSupreme<List<Home>>> { return homeApi.fetchHomeScreenData() private enum class AppSourceWeight { GPLAY, OPEN_SOURCE, PWA } fun getHomeScreenData(): LiveData<ResultSupreme<List<Home>>> { return liveData { suspend fun emitResult( result: ResultSupreme<List<Home>>, resultsBySource: Map<Source, ResultSupreme<List<Home>>> ) { val merged = mergeResults(resultsBySource.values) emit( ResultSupreme.create( result.getResultStatus(), merged, result.message, result.exception, ) ) } coroutineScope { val resultsBySource = mutableMapOf<Source, ResultSupreme<List<Home>>>() val sources = stores.getStores().keys val deferredResults = sources.associateWith { source -> async { loadHomeData(source) } } sources.forEach { source -> val result = deferredResults.getValue(source).await() resultsBySource[source] = result emitResult(result, resultsBySource) } } } } private suspend fun loadHomeData(source: Source): ResultSupreme<List<Home>> { val list = mutableListOf<Home>() val result = handleNetworkResult { val homeDataBuilder = stores.getStore(source) checkNotNull(homeDataBuilder) { "Could not find store for $source" } homeDataBuilder.getHomeScreenData(list) } setHomeErrorMessage(result.getResultStatus(), source) list.sortBy { when (it.source) { APP_TYPE_OPEN -> AppSourceWeight.OPEN_SOURCE.ordinal APP_TYPE_PWA -> AppSourceWeight.PWA.ordinal else -> AppSourceWeight.GPLAY.ordinal } } return ResultSupreme.create(result.getResultStatus(), list) } private fun setHomeErrorMessage(apiStatus: ResultStatus, source: Source) { if (apiStatus != ResultStatus.OK) { apiStatus.message = when (source) { Source.PLAY_STORE -> ("GPlay home loading error\n" + apiStatus.message).trim() Source.SYSTEM_APP -> ("Gitlab home not allowed\n" + apiStatus.message).trim() Source.OPEN_SOURCE -> ("Open Source home loading error\n" + apiStatus.message).trim() Source.PWA -> ("PWA home loading error\n" + apiStatus.message).trim() } } } private fun mergeResults(results: Collection<ResultSupreme<List<Home>>>): List<Home> { val merged = results.flatMap { it.data.orEmpty() }.toMutableList() merged.sortBy { when (it.source) { APP_TYPE_OPEN -> AppSourceWeight.OPEN_SOURCE.ordinal APP_TYPE_PWA -> AppSourceWeight.PWA.ordinal else -> AppSourceWeight.GPLAY.ordinal } } return merged } fun getSelectedAppTypes(): List<String> { Loading app/src/main/java/foundation/e/apps/data/application/data/Application.kt +4 −0 Original line number Diff line number Diff line Loading @@ -39,6 +39,7 @@ data class Application( var perms: List<String> = emptyList(), var reportId: Long = -1L, val icon_image_path: String = String(), val icon_url: String = String(), val last_modified: String = String(), var latest_version_code: Long = -1, val latest_version_number: String = String(), Loading Loading @@ -102,6 +103,9 @@ data class Application( ) { val iconUrl: String? get() { if (icon_url.isNotBlank()) { return icon_url } if (icon_image_path.isBlank()) { return null } Loading app/src/main/java/foundation/e/apps/data/application/home/HomeApiImpl.ktdeleted 100644 → 0 +0 −98 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2023 * Apps Quickly and easily install Android apps onto your device! * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.apps.data.application.home import androidx.lifecycle.LiveData import androidx.lifecycle.liveData import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.Stores import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Home import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Source import foundation.e.apps.data.handleNetworkResult import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import javax.inject.Inject class HomeApiImpl @Inject constructor( private val stores: Stores ) : HomeApi { private enum class AppSourceWeight { GPLAY, OPEN_SOURCE, PWA } override suspend fun fetchHomeScreenData(): LiveData<ResultSupreme<List<Home>>> { val list = mutableListOf<Home>() return liveData { coroutineScope { if (Source.PLAY_STORE in stores.getStores()) { val result = async { loadHomeData(list, Source.PLAY_STORE) } emit(result.await()) } val otherStores = stores.getStores().filter { it.key != Source.PLAY_STORE } otherStores.forEach { (source, _) -> val result = async { loadHomeData(list, source) } emit(result.await()) } } } } private suspend fun loadHomeData( priorList: MutableList<Home>, source: Source ): ResultSupreme<List<Home>> { val result = handleNetworkResult { val homeDataBuilder = stores.getStore(source) homeDataBuilder?.getHomeScreenData(priorList) ?: throw IllegalStateException("Could not find store for $source") } setHomeErrorMessage(result.getResultStatus(), source) priorList.sortBy { when (it.source) { ApplicationRepository.APP_TYPE_OPEN -> AppSourceWeight.OPEN_SOURCE.ordinal ApplicationRepository.APP_TYPE_PWA -> AppSourceWeight.PWA.ordinal else -> AppSourceWeight.GPLAY.ordinal } } return ResultSupreme.create(result.getResultStatus(), priorList) } private fun setHomeErrorMessage(apiStatus: ResultStatus, source: Source) { if (apiStatus != ResultStatus.OK) { apiStatus.message = when (source) { Source.PLAY_STORE -> ("GPlay home loading error\n" + apiStatus.message).trim() Source.SYSTEM_APP -> ("Gitlab home not allowed\n" + apiStatus.message).trim() Source.OPEN_SOURCE -> ("Open Source home loading error\n" + apiStatus.message).trim() Source.PWA -> ("PWA home loading error\n" + apiStatus.message).trim() } } } } app/src/main/java/foundation/e/apps/data/application/mapper/ApplicationDomainMapper.kt 0 → 100644 +67 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.data.application.mapper import foundation.e.apps.data.application.data.Application import foundation.e.apps.domain.application.ApplicationDomain fun ApplicationDomain.toApplication() = Application( _id = id, author = author, category = category, description = description, perms = perms, reportId = reportId, icon_image_path = iconImagePath, icon_url = iconUrl.orEmpty(), last_modified = lastModified, latest_version_code = latestVersionCode, latest_version_number = latestVersionNumber, latest_downloaded_version = latestDownloadedVersion, licence = licence, name = name, other_images_path = otherImagesPath, package_name = packageName, offer_type = offerType, status = status, shareUrl = shareUrl, originalSize = originalSize, appSize = appSize, source = source, price = price, isFree = isFree, is_pwa = isPwa, pwaPlayerDbId = pwaPlayerDbId, url = url, type = type, privacyScore = privacyScore, isPurchased = isPurchased, updatedOn = updatedOn, numberOfPermission = numberOfPermission, numberOfTracker = numberOfTracker, filterLevel = filterLevel, isGplayReplaced = isGplayReplaced, isFDroidApp = isFDroidApp, contentRating = contentRating, restriction = restriction, antiFeatures = antiFeatures, isPlaceHolder = isPlaceHolder, ratings = ratings, isSystemApp = isSystemApp, ) app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt +22 −13 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R import foundation.e.apps.data.StoreRepository import foundation.e.apps.data.application.ApplicationDataManager import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Home import foundation.e.apps.data.application.search.SearchSuggestion Loading @@ -51,6 +52,9 @@ import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.utils.SystemInfoProvider import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext import timber.log.Timber import java.net.HttpURLConnection Loading @@ -66,31 +70,36 @@ class PlayStoreRepository @Inject constructor( private val playStoreSearchHelper: PlayStoreSearchHelper ) : StoreRepository { override suspend fun getHomeScreenData(list: MutableList<Home>): List<Home> { val homeScreenData = mutableMapOf<String, List<Application>>() override suspend fun getHomeScreenData(list: MutableList<Home>) = coroutineScope { val homeElements = createTopChartElements() homeElements.forEach { if (it.value.isEmpty()) return@forEach val results = homeElements.map { (title, chartMap) -> async { if (chartMap.isEmpty()) { return@async null } val chart = chartMap.keys.iterator().next() val type = chartMap.values.iterator().next() title to getTopApps(type, chart) } }.awaitAll().filterNotNull() val chart = it.value.keys.iterator().next() val type = it.value.values.iterator().next() val result = getTopApps(type, chart) homeScreenData[it.key] = result results.forEach { (title, apps) -> if (apps.isEmpty()) { return@forEach } homeScreenData.map { val fusedApps = it.value.map { app -> val fusedApps = apps.map { app -> app.apply { applicationDataManager.updateStatus(this) applicationDataManager.updateFilterLevel(this) source = Source.PLAY_STORE } } list.add(Home(it.key, fusedApps)) list.add(Home(title, fusedApps, ApplicationRepository.APP_TYPE_ANY)) } return list list } private fun createTopChartElements() = mutableMapOf( Loading Loading
app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt +90 −10 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package foundation.e.apps.data.application import androidx.lifecycle.LiveData import androidx.lifecycle.liveData import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.Stores import foundation.e.apps.data.application.apps.AppsApi Loading @@ -27,19 +28,20 @@ import foundation.e.apps.data.application.category.CategoryApi import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Home import foundation.e.apps.data.application.downloadInfo.DownloadInfoApi import foundation.e.apps.data.application.home.HomeApi import foundation.e.apps.data.application.utils.CategoryType import foundation.e.apps.data.enums.FilterLevel 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.handleNetworkResult import foundation.e.apps.data.install.models.AppInstall import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import javax.inject.Inject import javax.inject.Singleton @Singleton class ApplicationRepository @Inject constructor( private val homeApi: HomeApi, private val categoryApi: CategoryApi, private val appsApi: AppsApi, private val downloadInfoApi: DownloadInfoApi, Loading @@ -51,8 +53,86 @@ class ApplicationRepository @Inject constructor( const val APP_TYPE_PWA = "pwa" } suspend fun getHomeScreenData(): LiveData<ResultSupreme<List<Home>>> { return homeApi.fetchHomeScreenData() private enum class AppSourceWeight { GPLAY, OPEN_SOURCE, PWA } fun getHomeScreenData(): LiveData<ResultSupreme<List<Home>>> { return liveData { suspend fun emitResult( result: ResultSupreme<List<Home>>, resultsBySource: Map<Source, ResultSupreme<List<Home>>> ) { val merged = mergeResults(resultsBySource.values) emit( ResultSupreme.create( result.getResultStatus(), merged, result.message, result.exception, ) ) } coroutineScope { val resultsBySource = mutableMapOf<Source, ResultSupreme<List<Home>>>() val sources = stores.getStores().keys val deferredResults = sources.associateWith { source -> async { loadHomeData(source) } } sources.forEach { source -> val result = deferredResults.getValue(source).await() resultsBySource[source] = result emitResult(result, resultsBySource) } } } } private suspend fun loadHomeData(source: Source): ResultSupreme<List<Home>> { val list = mutableListOf<Home>() val result = handleNetworkResult { val homeDataBuilder = stores.getStore(source) checkNotNull(homeDataBuilder) { "Could not find store for $source" } homeDataBuilder.getHomeScreenData(list) } setHomeErrorMessage(result.getResultStatus(), source) list.sortBy { when (it.source) { APP_TYPE_OPEN -> AppSourceWeight.OPEN_SOURCE.ordinal APP_TYPE_PWA -> AppSourceWeight.PWA.ordinal else -> AppSourceWeight.GPLAY.ordinal } } return ResultSupreme.create(result.getResultStatus(), list) } private fun setHomeErrorMessage(apiStatus: ResultStatus, source: Source) { if (apiStatus != ResultStatus.OK) { apiStatus.message = when (source) { Source.PLAY_STORE -> ("GPlay home loading error\n" + apiStatus.message).trim() Source.SYSTEM_APP -> ("Gitlab home not allowed\n" + apiStatus.message).trim() Source.OPEN_SOURCE -> ("Open Source home loading error\n" + apiStatus.message).trim() Source.PWA -> ("PWA home loading error\n" + apiStatus.message).trim() } } } private fun mergeResults(results: Collection<ResultSupreme<List<Home>>>): List<Home> { val merged = results.flatMap { it.data.orEmpty() }.toMutableList() merged.sortBy { when (it.source) { APP_TYPE_OPEN -> AppSourceWeight.OPEN_SOURCE.ordinal APP_TYPE_PWA -> AppSourceWeight.PWA.ordinal else -> AppSourceWeight.GPLAY.ordinal } } return merged } fun getSelectedAppTypes(): List<String> { Loading
app/src/main/java/foundation/e/apps/data/application/data/Application.kt +4 −0 Original line number Diff line number Diff line Loading @@ -39,6 +39,7 @@ data class Application( var perms: List<String> = emptyList(), var reportId: Long = -1L, val icon_image_path: String = String(), val icon_url: String = String(), val last_modified: String = String(), var latest_version_code: Long = -1, val latest_version_number: String = String(), Loading Loading @@ -102,6 +103,9 @@ data class Application( ) { val iconUrl: String? get() { if (icon_url.isNotBlank()) { return icon_url } if (icon_image_path.isBlank()) { return null } Loading
app/src/main/java/foundation/e/apps/data/application/home/HomeApiImpl.ktdeleted 100644 → 0 +0 −98 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2023 * Apps Quickly and easily install Android apps onto your device! * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.apps.data.application.home import androidx.lifecycle.LiveData import androidx.lifecycle.liveData import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.Stores import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Home import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Source import foundation.e.apps.data.handleNetworkResult import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import javax.inject.Inject class HomeApiImpl @Inject constructor( private val stores: Stores ) : HomeApi { private enum class AppSourceWeight { GPLAY, OPEN_SOURCE, PWA } override suspend fun fetchHomeScreenData(): LiveData<ResultSupreme<List<Home>>> { val list = mutableListOf<Home>() return liveData { coroutineScope { if (Source.PLAY_STORE in stores.getStores()) { val result = async { loadHomeData(list, Source.PLAY_STORE) } emit(result.await()) } val otherStores = stores.getStores().filter { it.key != Source.PLAY_STORE } otherStores.forEach { (source, _) -> val result = async { loadHomeData(list, source) } emit(result.await()) } } } } private suspend fun loadHomeData( priorList: MutableList<Home>, source: Source ): ResultSupreme<List<Home>> { val result = handleNetworkResult { val homeDataBuilder = stores.getStore(source) homeDataBuilder?.getHomeScreenData(priorList) ?: throw IllegalStateException("Could not find store for $source") } setHomeErrorMessage(result.getResultStatus(), source) priorList.sortBy { when (it.source) { ApplicationRepository.APP_TYPE_OPEN -> AppSourceWeight.OPEN_SOURCE.ordinal ApplicationRepository.APP_TYPE_PWA -> AppSourceWeight.PWA.ordinal else -> AppSourceWeight.GPLAY.ordinal } } return ResultSupreme.create(result.getResultStatus(), priorList) } private fun setHomeErrorMessage(apiStatus: ResultStatus, source: Source) { if (apiStatus != ResultStatus.OK) { apiStatus.message = when (source) { Source.PLAY_STORE -> ("GPlay home loading error\n" + apiStatus.message).trim() Source.SYSTEM_APP -> ("Gitlab home not allowed\n" + apiStatus.message).trim() Source.OPEN_SOURCE -> ("Open Source home loading error\n" + apiStatus.message).trim() Source.PWA -> ("PWA home loading error\n" + apiStatus.message).trim() } } } }
app/src/main/java/foundation/e/apps/data/application/mapper/ApplicationDomainMapper.kt 0 → 100644 +67 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.data.application.mapper import foundation.e.apps.data.application.data.Application import foundation.e.apps.domain.application.ApplicationDomain fun ApplicationDomain.toApplication() = Application( _id = id, author = author, category = category, description = description, perms = perms, reportId = reportId, icon_image_path = iconImagePath, icon_url = iconUrl.orEmpty(), last_modified = lastModified, latest_version_code = latestVersionCode, latest_version_number = latestVersionNumber, latest_downloaded_version = latestDownloadedVersion, licence = licence, name = name, other_images_path = otherImagesPath, package_name = packageName, offer_type = offerType, status = status, shareUrl = shareUrl, originalSize = originalSize, appSize = appSize, source = source, price = price, isFree = isFree, is_pwa = isPwa, pwaPlayerDbId = pwaPlayerDbId, url = url, type = type, privacyScore = privacyScore, isPurchased = isPurchased, updatedOn = updatedOn, numberOfPermission = numberOfPermission, numberOfTracker = numberOfTracker, filterLevel = filterLevel, isGplayReplaced = isGplayReplaced, isFDroidApp = isFDroidApp, contentRating = contentRating, restriction = restriction, antiFeatures = antiFeatures, isPlaceHolder = isPlaceHolder, ratings = ratings, isSystemApp = isSystemApp, )
app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt +22 −13 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R import foundation.e.apps.data.StoreRepository import foundation.e.apps.data.application.ApplicationDataManager import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Home import foundation.e.apps.data.application.search.SearchSuggestion Loading @@ -51,6 +52,9 @@ import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.utils.SystemInfoProvider import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext import timber.log.Timber import java.net.HttpURLConnection Loading @@ -66,31 +70,36 @@ class PlayStoreRepository @Inject constructor( private val playStoreSearchHelper: PlayStoreSearchHelper ) : StoreRepository { override suspend fun getHomeScreenData(list: MutableList<Home>): List<Home> { val homeScreenData = mutableMapOf<String, List<Application>>() override suspend fun getHomeScreenData(list: MutableList<Home>) = coroutineScope { val homeElements = createTopChartElements() homeElements.forEach { if (it.value.isEmpty()) return@forEach val results = homeElements.map { (title, chartMap) -> async { if (chartMap.isEmpty()) { return@async null } val chart = chartMap.keys.iterator().next() val type = chartMap.values.iterator().next() title to getTopApps(type, chart) } }.awaitAll().filterNotNull() val chart = it.value.keys.iterator().next() val type = it.value.values.iterator().next() val result = getTopApps(type, chart) homeScreenData[it.key] = result results.forEach { (title, apps) -> if (apps.isEmpty()) { return@forEach } homeScreenData.map { val fusedApps = it.value.map { app -> val fusedApps = apps.map { app -> app.apply { applicationDataManager.updateStatus(this) applicationDataManager.updateFilterLevel(this) source = Source.PLAY_STORE } } list.add(Home(it.key, fusedApps)) list.add(Home(title, fusedApps, ApplicationRepository.APP_TYPE_ANY)) } return list list } private fun createTopChartElements() = mutableMapOf( Loading