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

Verified Commit cd7630e9 authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

fix(updates): fix update getting skipped for large number of updates

Fixes the buggy behaviour by introducing a chunk-based mechanism so that instead of trying to enqueue all the app updates at once, App Lounge now batches the updates and process them chunk by chunk for manual Update All action.
parent 42aeb0a4
Loading
Loading
Loading
Loading
Loading
+3 −2
Original line number Diff line number Diff line
@@ -51,6 +51,7 @@ import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
import com.aurora.gplayapi.data.models.PlayFile as AuroraFile

@Singleton
class AppManagerImpl @Inject constructor(
    @Named("cacheDir") private val cacheDir: String,
@@ -65,7 +66,6 @@ class AppManagerImpl @Inject constructor(
    @ApplicationContext private val context: Context,
    private val fDroidRepository: FDroidRepository
) : AppManager {

    @Inject
    lateinit var contentRatingDao: ContentRatingDao

@@ -88,8 +88,9 @@ class AppManagerImpl @Inject constructor(

    override suspend fun addDownload(appInstall: AppInstall): Boolean {
        val existingFusedDownload = getDownloadById(appInstall)
        val isInstallWorkRunning = isInstallWorkRunning(existingFusedDownload, appInstall)
        val canAddDownload = when {
            isInstallWorkRunning(existingFusedDownload, appInstall) -> false
            isInstallWorkRunning -> false
            // We don't want to add anything if it already exists without INSTALLATION_ISSUE
            existingFusedDownload != null && !isStatusEligibleToInstall(existingFusedDownload) -> false
            else -> true
+2 −0
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import foundation.e.apps.domain.preferences.SessionRepository
import kotlinx.coroutines.CancellationException
import timber.log.Timber
import javax.inject.Inject

class InstallationEnqueuer @Inject constructor(
    private val preEnqueueChecker: PreEnqueueChecker,
    private val appManager: AppManager,
@@ -37,6 +38,7 @@ class InstallationEnqueuer @Inject constructor(
    private val playStoreAuthStore: PlayStoreAuthStore,
    private val appEventDispatcher: AppEventDispatcher,
) {

    suspend fun enqueue(
        appInstall: AppInstall,
        isAnUpdate: Boolean = false,
+2 −8
Original line number Diff line number Diff line
@@ -21,7 +21,6 @@ package foundation.e.apps.data.install.core.helper
import foundation.e.apps.data.application.AppManager
import foundation.e.apps.data.installation.model.AppInstall
import foundation.e.apps.data.installation.model.InstallationType
import timber.log.Timber
import javax.inject.Inject

class PreEnqueueChecker @Inject constructor(
@@ -30,22 +29,17 @@ class PreEnqueueChecker @Inject constructor(
    private val ageLimiter: AgeLimiter,
    private val devicePreconditions: DevicePreconditions,
) {

    suspend fun canEnqueue(appInstall: AppInstall, isAnUpdate: Boolean = false): Boolean {
        val hasUpdatedDownloadUrls = appInstall.type == InstallationType.PWA ||
            downloadUrlRefresher.updateDownloadUrls(appInstall, isAnUpdate)

        val isDownloadAdded = hasUpdatedDownloadUrls && addDownload(appInstall)
        val isAgeLimitAllowed = isDownloadAdded && ageLimiter.allow(appInstall)

        return isAgeLimitAllowed && devicePreconditions.canProceed(appInstall)
    }

    private suspend fun addDownload(appInstall: AppInstall): Boolean {
        val isDownloadAdded = appManager.addDownload(appInstall)
        if (!isDownloadAdded) {
            Timber.i("Update adding ABORTED! status")
        }

        return isDownloadAdded
        return appManager.addDownload(appInstall)
    }
}
+98 −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.install.updates

import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.enums.Type
import foundation.e.apps.domain.model.install.Status
import kotlinx.serialization.Serializable

@Serializable
data class ManualUpdateChainSnapshot(
    val chainId: String,
    val packages: List<ManualUpdateChainPackageSnapshot>,
    val cursor: Int = 0,
    val createdAtMillis: Long,
)

fun buildManualUpdateChainSnapshot(
    chainId: String,
    applications: List<Application>,
    createdAtMillis: Long,
): ManualUpdateChainSnapshot {
    return ManualUpdateChainSnapshot(
        chainId = chainId,
        packages = applications.map { it.toManualUpdateChainPackageSnapshot() },
        createdAtMillis = createdAtMillis,
    )
}

@Serializable
data class ManualUpdateChainPackageSnapshot(
    val appId: String,
    val name: String,
    val packageName: String,
    val source: String,
    val status: String,
    val type: String,
    val iconImagePath: String,
    val latestVersionCode: Long,
    val offerType: Int,
    val isFree: Boolean,
    val originalSize: Long,
    val url: String,
    val isSystemApp: Boolean,
)

fun Application.toManualUpdateChainPackageSnapshot(): ManualUpdateChainPackageSnapshot {
    return ManualUpdateChainPackageSnapshot(
        appId = _id,
        name = name,
        packageName = package_name,
        source = source.name,
        status = status.name,
        type = type.name,
        iconImagePath = icon_image_path,
        latestVersionCode = latest_version_code,
        offerType = offer_type,
        isFree = isFree,
        originalSize = originalSize,
        url = url,
        isSystemApp = isSystemApp,
    )
}

fun ManualUpdateChainPackageSnapshot.toApplication(): Application {
    return Application(
        _id = appId,
        name = name,
        package_name = packageName,
        source = Source.entries.firstOrNull { it.name == source } ?: Source.PLAY_STORE,
        status = Status.entries.firstOrNull { it.name == status } ?: Status.UNAVAILABLE,
        type = Type.entries.firstOrNull { it.name == type } ?: Type.NATIVE,
        icon_image_path = iconImagePath,
        latest_version_code = latestVersionCode,
        offer_type = offerType,
        isFree = isFree,
        originalSize = originalSize,
        url = url,
        isSystemApp = isSystemApp,
    )
}
+77 −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.install.updates

import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.first
import kotlinx.serialization.json.Json
import javax.inject.Inject
import javax.inject.Singleton

private const val MANUAL_UPDATE_CHAIN_PREFERENCE_DATA_STORE_NAME = "ManualUpdateChains"
val Context.manualUpdateChainDataStore by preferencesDataStore(
    MANUAL_UPDATE_CHAIN_PREFERENCE_DATA_STORE_NAME
)

@Singleton
class ManualUpdateChainStore @Inject constructor(
    @ApplicationContext
    private val context: Context,
    private val json: Json,
) {
    companion object {
        private val ACTIVE_CHAIN_SNAPSHOT = stringPreferencesKey("active_chain_snapshot")
    }

    suspend fun readSnapshot(chainId: String): ManualUpdateChainSnapshot? {
        return context.manualUpdateChainDataStore.data.first()[ACTIVE_CHAIN_SNAPSHOT]
            ?.let { json.decodeFromString<ManualUpdateChainSnapshot>(it) }
            ?.takeIf { it.chainId == chainId }
    }

    suspend fun writeSnapshot(snapshot: ManualUpdateChainSnapshot) {
        context.manualUpdateChainDataStore.edit {
            it[ACTIVE_CHAIN_SNAPSHOT] = json.encodeToString(snapshot)
        }
    }

    suspend fun advanceSnapshot(chainId: String, consumedCount: Int): ManualUpdateChainSnapshot? {
        val snapshot = readSnapshot(chainId) ?: return null
        val advancedSnapshot = snapshot.copy(
            cursor = (snapshot.cursor + consumedCount).coerceAtMost(snapshot.packages.size)
        )
        writeSnapshot(advancedSnapshot)
        return advancedSnapshot
    }

    suspend fun clearSnapshot(chainId: String) {
        context.manualUpdateChainDataStore.edit { preferences ->
            val storedSnapshot = preferences[ACTIVE_CHAIN_SNAPSHOT]
                ?.let { json.decodeFromString<ManualUpdateChainSnapshot>(it) }
                ?: return@edit
            if (storedSnapshot.chainId == chainId) {
                preferences.remove(ACTIVE_CHAIN_SNAPSHOT)
            }
        }
    }
}
Loading