Loading app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt +3 −2 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -65,7 +66,6 @@ class AppManagerImpl @Inject constructor( @ApplicationContext private val context: Context, private val fDroidRepository: FDroidRepository ) : AppManager { @Inject lateinit var contentRatingDao: ContentRatingDao Loading @@ -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 Loading app/src/main/java/foundation/e/apps/data/install/core/InstallationEnqueuer.kt +2 −0 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -37,6 +38,7 @@ class InstallationEnqueuer @Inject constructor( private val playStoreAuthStore: PlayStoreAuthStore, private val appEventDispatcher: AppEventDispatcher, ) { suspend fun enqueue( appInstall: AppInstall, isAnUpdate: Boolean = false, Loading app/src/main/java/foundation/e/apps/data/install/core/helper/PreEnqueueChecker.kt +2 −8 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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) } } app/src/main/java/foundation/e/apps/data/install/updates/ManualUpdateChainSnapshot.kt 0 → 100644 +37 −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 data class ManualUpdateChainSnapshot( val chainId: String, val packages: List<Application>, val cursor: Int = 0, ) fun buildManualUpdateChainSnapshot( chainId: String, applications: List<Application>, ): ManualUpdateChainSnapshot { return ManualUpdateChainSnapshot( chainId = chainId, packages = applications, ) } app/src/main/java/foundation/e/apps/data/install/updates/ManualUpdateChainStore.kt 0 → 100644 +79 −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 com.google.gson.Gson import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.first import javax.inject.Inject import javax.inject.Named 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, @Named("gsonCustomAdapter") private val gson: Gson, ) { 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 { gson.fromJson(it, ManualUpdateChainSnapshot::class.java) } ?.takeIf { it.chainId == chainId } } suspend fun writeSnapshot(snapshot: ManualUpdateChainSnapshot) { context.manualUpdateChainDataStore.edit { it[ACTIVE_CHAIN_SNAPSHOT] = gson.toJson(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 { gson.fromJson(it, ManualUpdateChainSnapshot::class.java) } ?: return@edit if (storedSnapshot.chainId == chainId) { preferences.remove(ACTIVE_CHAIN_SNAPSHOT) } } } } Loading
app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt +3 −2 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -65,7 +66,6 @@ class AppManagerImpl @Inject constructor( @ApplicationContext private val context: Context, private val fDroidRepository: FDroidRepository ) : AppManager { @Inject lateinit var contentRatingDao: ContentRatingDao Loading @@ -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 Loading
app/src/main/java/foundation/e/apps/data/install/core/InstallationEnqueuer.kt +2 −0 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -37,6 +38,7 @@ class InstallationEnqueuer @Inject constructor( private val playStoreAuthStore: PlayStoreAuthStore, private val appEventDispatcher: AppEventDispatcher, ) { suspend fun enqueue( appInstall: AppInstall, isAnUpdate: Boolean = false, Loading
app/src/main/java/foundation/e/apps/data/install/core/helper/PreEnqueueChecker.kt +2 −8 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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) } }
app/src/main/java/foundation/e/apps/data/install/updates/ManualUpdateChainSnapshot.kt 0 → 100644 +37 −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 data class ManualUpdateChainSnapshot( val chainId: String, val packages: List<Application>, val cursor: Int = 0, ) fun buildManualUpdateChainSnapshot( chainId: String, applications: List<Application>, ): ManualUpdateChainSnapshot { return ManualUpdateChainSnapshot( chainId = chainId, packages = applications, ) }
app/src/main/java/foundation/e/apps/data/install/updates/ManualUpdateChainStore.kt 0 → 100644 +79 −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 com.google.gson.Gson import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.first import javax.inject.Inject import javax.inject.Named 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, @Named("gsonCustomAdapter") private val gson: Gson, ) { 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 { gson.fromJson(it, ManualUpdateChainSnapshot::class.java) } ?.takeIf { it.chainId == chainId } } suspend fun writeSnapshot(snapshot: ManualUpdateChainSnapshot) { context.manualUpdateChainDataStore.edit { it[ACTIVE_CHAIN_SNAPSHOT] = gson.toJson(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 { gson.fromJson(it, ManualUpdateChainSnapshot::class.java) } ?: return@edit if (storedSnapshot.chainId == chainId) { preferences.remove(ACTIVE_CHAIN_SNAPSHOT) } } } }