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

Commit 3104b134 authored by Abhishek Aggarwal's avatar Abhishek Aggarwal
Browse files

Merge branch '0000-main-2-update_sign_issues' into 'main'

0000 main 2 update sign issues

See merge request !816
parents d75b43e3 3f643b11
Loading
Loading
Loading
Loading
Loading
+1 −3
Original line number Diff line number Diff line
@@ -80,10 +80,8 @@ class ApplicationDataManager @Inject constructor(
    }

    fun updateStatus(application: Application) {
        if (application.status != Status.INSTALLATION_ISSUE) {
        application.status = getFusedAppInstallationStatus(application)
    }
    }

    /*
     * Get fused app installation status.
+18 −0
Original line number Diff line number Diff line
@@ -49,6 +49,7 @@ class ApplicationRepository @Inject constructor(
    private val downloadInfoApi: DownloadInfoApi,
    private val enabledStoreRepositoryProvider: EnabledStoreRepositoryProvider,
    private val enabledSourceState: EnabledSourceState,
    private val playStoreOtherStoreStatusResolver: PlayStoreOtherStoreStatusResolver,
) {
    companion object {
        const val APP_TYPE_ANY = "any"
@@ -108,6 +109,7 @@ class ApplicationRepository @Inject constructor(
        val result = handleNetworkResult {
            storeRepository.getHomeScreenData(list)
        }
        hydratePlayHomeUpdateStatus(source, list)

        prefixHomeErrorMessage(result, source)
        list.sortBy {
@@ -126,6 +128,14 @@ class ApplicationRepository @Inject constructor(
        )
    }

    private suspend fun hydratePlayHomeUpdateStatus(source: Source, homes: List<Home>) {
        if (source != Source.PLAY_STORE) return
        playStoreOtherStoreStatusResolver.resolveAndApply(
            applications = homes.flatMap { it.list },
            hydrateMissingDetails = true,
        )
    }

    private fun prefixHomeErrorMessage(result: ResultSupreme<*>, source: Source) {
        if (result.getResultStatus() != ResultStatus.OK) {
            val prefix = when (source) {
@@ -216,6 +226,14 @@ class ApplicationRepository @Inject constructor(
        return appsApi.getFusedAppInstallationStatus(application)
    }

    /**
     * Raw package-manager status, bypassing source-aware filtering. Use this when a UI needs to
     * know whether the underlying APK can update even if the rendered source isn't the owner.
     */
    fun getRawFusedAppInstallationStatus(application: Application): Status {
        return appsApi.getRawFusedAppInstallationStatus(application)
    }

    fun isAnyFusedAppUpdated(
        newApplications: List<Application>,
        oldApplications: List<Application>
+28 −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

import foundation.e.apps.data.application.data.Application
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class PendingUpdatesStore @Inject constructor() {
    fun getPendingUpdates(): List<Application> = UpdatesDao.appsAwaitingForUpdate
}
+96 −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

import foundation.e.apps.OpenForTesting
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.enums.Source
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Session-scoped cache of Play app details. Returns defensive copies so consumers can't mutate
 * cached entries. Invalidated via package install/uninstall broadcasts (see PkgManagerBR).
 */
@Singleton
@OpenForTesting
class PlayStoreAppDetailsCache @Inject constructor() {
    private data class CachedDetails(
        val application: Application,
        val isComplete: Boolean,
    )

    private val detailsByPackage = ConcurrentHashMap<String, CachedDetails>()

    fun get(packageName: String): Application? {
        return detailsByPackage[packageName]?.application?.copy()
    }

    suspend fun getOrFetch(
        packageName: String,
        provider: PlayStoreAppDetailsProvider,
    ): Application? {
        // Only a complete entry (from a single details fetch) can satisfy a detail-screen
        // request. A partial entry from the batch path lacks screenshots/description, so we
        // re-fetch the full details and upgrade the cache instead of returning it.
        detailsByPackage[packageName]?.let { cached ->
            if (cached.isComplete) return cached.application.copy()
        }

        val details = provider.getPlayStoreAppDetails(packageName)
        return if (details.package_name.isBlank()) {
            get(packageName)
        } else {
            put(details, isComplete = true)
            get(packageName)
        }
    }

    suspend fun getOrFetch(
        packageNames: List<String>,
        provider: PlayStoreAppDetailsProvider,
    ): Map<String, Application> {
        val distinct = packageNames.filter { it.isNotBlank() }.distinct()
        val cached = distinct.mapNotNull { name -> get(name)?.let { name to it } }.toMap()
        val missing = distinct - cached.keys

        if (missing.isEmpty()) return cached

        putAll(provider.getPlayStoreAppDetails(missing))
        val freshlyCached = missing.mapNotNull { name -> get(name)?.let { name to it } }.toMap()
        return cached + freshlyCached
    }

    fun putAll(applications: List<Application>) {
        // Batch (bulkDetails) responses omit screenshots/description, so they are partial.
        applications.forEach { put(it, isComplete = false) }
    }

    fun put(application: Application, isComplete: Boolean = true) {
        if (application.package_name.isBlank()) return
        // Pin source to PLAY_STORE in case a provider returns the value pre-normalization.
        detailsByPackage[application.package_name] =
            CachedDetails(application.copy(source = Source.PLAY_STORE), isComplete)
    }

    fun clear() {
        detailsByPackage.clear()
    }
}
+87 −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

import foundation.e.apps.OpenForTesting
import foundation.e.apps.data.EnabledStoreRepositoryProvider
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.install.pkg.AppLoungePackageManager
import kotlinx.coroutines.CancellationException
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton

/**
 * For installed Play apps that were hydrated from a lightweight source (e.g. home feed)
 * without a real latest_version_code, fetches full details so update buttons render correctly.
 */
@Singleton
@OpenForTesting
class PlayStoreAppDetailsHydrator @Inject constructor(
    private val enabledStoreRepositoryProvider: EnabledStoreRepositoryProvider,
    private val appLoungePackageManager: AppLoungePackageManager,
    private val playStoreAppDetailsCache: PlayStoreAppDetailsCache,
) {
    suspend fun hydrateAndApplyMissingDetails(
        applications: List<Application>,
    ): Map<String, Application> {
        val packagesNeedingDetails = applications
            .asSequence()
            .filter(::isMissingPlayDetailsCandidate)
            .map { it.package_name }
            .distinct()
            .toList()

        if (packagesNeedingDetails.isEmpty()) return emptyMap()

        val detailsByPackage = fetchDetails(packagesNeedingDetails)
        applications.forEach { application ->
            val details = detailsByPackage[application.package_name] ?: return@forEach
            if (details.latest_version_code > application.latest_version_code) {
                application.latest_version_code = details.latest_version_code
            }
        }
        return detailsByPackage
    }

    private suspend fun fetchDetails(packageNames: List<String>): Map<String, Application> {
        val provider = getPlayStoreDetailsProvider() ?: return emptyMap()
        return runCatching {
            playStoreAppDetailsCache.getOrFetch(packageNames, provider)
        }.getOrElse { exception ->
            if (exception is CancellationException) throw exception
            Timber.w(exception, "Failed to hydrate installed Play app details")
            emptyMap()
        }
    }

    private fun isMissingPlayDetailsCandidate(application: Application): Boolean {
        return application.source == Source.PLAY_STORE &&
            !application.is_pwa &&
            application.package_name.isNotBlank() &&
            application.latest_version_code <= 0 &&
            appLoungePackageManager.isInstalled(application.package_name)
    }

    private suspend fun getPlayStoreDetailsProvider(): PlayStoreAppDetailsProvider? {
        return enabledStoreRepositoryProvider.awaitStore(Source.PLAY_STORE)
            as? PlayStoreAppDetailsProvider
    }
}
Loading