Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8d90f495 authored by Jonathan Klee's avatar Jonathan Klee
Browse files

Merge branch '0000-a16-domain-home-page' into 'main'

Improve Home page performance

See merge request !686
parents 15e762a2 55146b70
Loading
Loading
Loading
Loading
Loading
+90 −10
Original line number Diff line number Diff line
@@ -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
@@ -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,
@@ -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> {
+4 −0
Original line number Diff line number Diff line
@@ -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(),
@@ -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
            }
+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()
            }
        }
    }
}
+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,
)
+22 −13
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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