Loading app/src/main/java/foundation/e/apps/data/application/UpdatesDao.kt +15 −2 Original line number Diff line number Diff line Loading @@ -23,18 +23,31 @@ import foundation.e.apps.data.installation.model.AppInstall object UpdatesDao { private val _appsAwaitingForUpdate: MutableList<Application> = mutableListOf() val appsAwaitingForUpdate: List<Application> = _appsAwaitingForUpdate var appsAwaitingForUpdateIncludesOtherStores: Boolean = false private set private val _successfulUpdatedApps = mutableListOf<AppInstall>() val successfulUpdatedApps: List<AppInstall> = _successfulUpdatedApps fun addItemsForUpdate(appsNeedUpdate: List<Application>) { fun addItemsForUpdate( appsNeedUpdate: List<Application>, includesOtherStores: Boolean = false ) { _appsAwaitingForUpdate.clear() _appsAwaitingForUpdate.addAll(appsNeedUpdate) appsAwaitingForUpdateIncludesOtherStores = appsNeedUpdate.isNotEmpty() && includesOtherStores } fun hasAnyAppsForUpdate() = _appsAwaitingForUpdate.isNotEmpty() fun removeUpdateIfExists(packageName: String) = _appsAwaitingForUpdate.removeIf { it.package_name == packageName } fun removeUpdateIfExists(packageName: String): Boolean { val removed = _appsAwaitingForUpdate.removeIf { it.package_name == packageName } if (_appsAwaitingForUpdate.isEmpty()) { appsAwaitingForUpdateIncludesOtherStores = false } return removed } fun addSuccessfullyUpdatedApp(appInstall: AppInstall) { _successfulUpdatedApps.add(appInstall) Loading app/src/main/java/foundation/e/apps/data/install/pkg/InstallerService.kt +24 −0 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ package foundation.e.apps.data.install.pkg import android.app.Service import android.content.Intent import android.content.pm.PackageInstaller import android.os.Build import android.os.IBinder import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.data.application.AppManager Loading Loading @@ -59,6 +60,13 @@ class InstallerService : Service() { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, DEFAULT_INSTALL_STATUS) if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) { launchUserConfirmation(intent) stopSelf() return START_NOT_STICKY } var packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) val extra = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) Loading @@ -75,6 +83,22 @@ class InstallerService : Service() { return START_NOT_STICKY } @Suppress("DEPRECATION") private fun launchUserConfirmation(intent: Intent) { val confirmIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) } else { intent.getParcelableExtra(Intent.EXTRA_INTENT) } if (confirmIntent != null) { confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(confirmIntent) Timber.d("Launched user confirmation dialog for install") } else { Timber.e("STATUS_PENDING_USER_ACTION but no confirmation intent found") } } private fun postStatus(status: Int, packageName: String?, extra: String?) { Timber.d("postStatus: $status $packageName $extra") if (status == PackageInstaller.STATUS_SUCCESS) { Loading app/src/main/java/foundation/e/apps/data/updates/InstalledAppUpdateInfoProviderImpl.kt 0 → 100644 +50 −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.updates import foundation.e.apps.data.install.pkg.AppLoungePackageManager import foundation.e.apps.domain.install.InstalledAppUpdateInfoProvider import foundation.e.apps.domain.model.install.Status import javax.inject.Inject import javax.inject.Singleton @Singleton class InstalledAppUpdateInfoProviderImpl @Inject constructor( private val appLoungePackageManager: AppLoungePackageManager, private val updatesManagerImpl: UpdatesManagerImpl, ) : InstalledAppUpdateInfoProvider { override fun getPackageStatus(packageName: String, versionCode: Long): Status { return appLoungePackageManager.getPackageStatus(packageName, versionCode) } override fun isInstalledFromOtherStore(packageName: String): Boolean { return updatesManagerImpl.isAppInstalledFromOtherStore(packageName) } override fun getInstallerLabel(packageName: String): String? { val installerPackageName = appLoungePackageManager.getInstallerName(packageName) .takeIf { it.isNotBlank() } ?: return null return runCatching { appLoungePackageManager.getAppNameFromPackageName(installerPackageName) }.getOrDefault(installerPackageName).ifBlank { installerPackageName } } } app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt +27 −7 Original line number Diff line number Diff line Loading @@ -68,12 +68,16 @@ class UpdatesManagerImpl @Inject constructor( if (appPreferencesRepository.shouldUpdateAppsFromOtherStores()) { withContext(Dispatchers.IO) { val otherStoresInstalledApps = getAppsFromOtherStores().toMutableList() val fdroidInstalledApps = getFDroidInstalledApps() openSourceInstalledApps.addAll(fdroidInstalledApps) val otherStoresInstalledApps = getAppsFromOtherStores() .filterNot { it in fdroidInstalledApps } .toMutableList() // This list is based on app signatures val updatableFDroidApps = findPackagesMatchingFDroidSignatures(otherStoresInstalledApps) openSourceInstalledApps.addAll(updatableFDroidApps) otherStoresInstalledApps.removeAll(updatableFDroidApps) Loading Loading @@ -130,7 +134,12 @@ class UpdatesManagerImpl @Inject constructor( val openSourceInstalledApps = getOpenSourceInstalledApps().toMutableList() if (appPreferencesRepository.shouldUpdateAppsFromOtherStores()) { val otherStoresInstalledApps = getAppsFromOtherStores().toMutableList() val fdroidInstalledApps = getFDroidInstalledApps() openSourceInstalledApps.addAll(fdroidInstalledApps) val otherStoresInstalledApps = getAppsFromOtherStores() .filterNot { it in fdroidInstalledApps } .toMutableList() // This list is based on app signatures val updatableFDroidApps = Loading Loading @@ -195,13 +204,21 @@ class UpdatesManagerImpl @Inject constructor( /** * Lists apps directly updatable by App Lounge from the Open Source category. * (This includes apps installed by F-Droid client app, if used by the user; * F-Droid is not considered a third party source.) * * Apps installed by external clients such as F-Droid are handled through the * "other stores" flow so the user preference can enable or disable them. */ private fun getOpenSourceInstalledApps(): List<String> { return userApplications.filter { appLoungePackageManager.getInstallerName(it.packageName) in listOf( context.packageName, ) }.map { it.packageName } } private fun getFDroidInstalledApps(): List<String> { return userApplications.filter { appLoungePackageManager.getInstallerName(it.packageName) in listOf( PACKAGE_NAME_F_DROID, PACKAGE_NAME_F_DROID_PRIVILEGED, ) Loading @@ -224,10 +241,9 @@ class UpdatesManagerImpl @Inject constructor( /** * Lists apps installed from other app stores. * (F-Droid client is not considered a third party source.) * * @return List of package names of apps installed from other app stores like * Aurora Store, Apk mirror, apps installed from browser, apps from ADB etc. * F-Droid, Aurora Store, ApkMirror, browser downloads, apps from ADB, etc. */ private fun getAppsFromOtherStores(): List<String> { val gplayAndOpenSourceInstalledApps = getGPlayInstalledApps() + getOpenSourceInstalledApps() Loading @@ -236,6 +252,10 @@ class UpdatesManagerImpl @Inject constructor( }.map { it.packageName } } fun isAppInstalledFromOtherStore(packageName: String): Boolean { return getAppsFromOtherStores().contains(packageName) } /** * Runs API (GPlay api or CleanApk) and accumulates the updatable apps * into a provided list. Loading app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerRepository.kt +12 −3 Original line number Diff line number Diff line Loading @@ -22,20 +22,29 @@ import foundation.e.apps.data.application.UpdatesDao import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Source import foundation.e.apps.domain.preferences.AppPreferencesRepository import javax.inject.Inject class UpdatesManagerRepository @Inject constructor( private val updatesManagerImpl: UpdatesManagerImpl private val updatesManagerImpl: UpdatesManagerImpl, private val appPreferencesRepository: AppPreferencesRepository, ) { fun isAppInstalledFromOtherStore(packageName: String): Boolean { return updatesManagerImpl.isAppInstalledFromOtherStore(packageName) } suspend fun getUpdates(): Pair<List<Application>, ResultStatus> { if (UpdatesDao.hasAnyAppsForUpdate()) { val includesOtherStores = appPreferencesRepository.shouldUpdateAppsFromOtherStores() if ( UpdatesDao.hasAnyAppsForUpdate() && UpdatesDao.appsAwaitingForUpdateIncludesOtherStores == includesOtherStores ) { return Pair(UpdatesDao.appsAwaitingForUpdate, ResultStatus.OK) } return updatesManagerImpl.getUpdates().run { val filteredApps = first.filter { !(!it.isFree && it.source == Source.PLAY_STORE && !it.isPurchased) } UpdatesDao.addItemsForUpdate(filteredApps) UpdatesDao.addItemsForUpdate(filteredApps, includesOtherStores) Pair(filteredApps, this.second) } } Loading Loading
app/src/main/java/foundation/e/apps/data/application/UpdatesDao.kt +15 −2 Original line number Diff line number Diff line Loading @@ -23,18 +23,31 @@ import foundation.e.apps.data.installation.model.AppInstall object UpdatesDao { private val _appsAwaitingForUpdate: MutableList<Application> = mutableListOf() val appsAwaitingForUpdate: List<Application> = _appsAwaitingForUpdate var appsAwaitingForUpdateIncludesOtherStores: Boolean = false private set private val _successfulUpdatedApps = mutableListOf<AppInstall>() val successfulUpdatedApps: List<AppInstall> = _successfulUpdatedApps fun addItemsForUpdate(appsNeedUpdate: List<Application>) { fun addItemsForUpdate( appsNeedUpdate: List<Application>, includesOtherStores: Boolean = false ) { _appsAwaitingForUpdate.clear() _appsAwaitingForUpdate.addAll(appsNeedUpdate) appsAwaitingForUpdateIncludesOtherStores = appsNeedUpdate.isNotEmpty() && includesOtherStores } fun hasAnyAppsForUpdate() = _appsAwaitingForUpdate.isNotEmpty() fun removeUpdateIfExists(packageName: String) = _appsAwaitingForUpdate.removeIf { it.package_name == packageName } fun removeUpdateIfExists(packageName: String): Boolean { val removed = _appsAwaitingForUpdate.removeIf { it.package_name == packageName } if (_appsAwaitingForUpdate.isEmpty()) { appsAwaitingForUpdateIncludesOtherStores = false } return removed } fun addSuccessfullyUpdatedApp(appInstall: AppInstall) { _successfulUpdatedApps.add(appInstall) Loading
app/src/main/java/foundation/e/apps/data/install/pkg/InstallerService.kt +24 −0 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ package foundation.e.apps.data.install.pkg import android.app.Service import android.content.Intent import android.content.pm.PackageInstaller import android.os.Build import android.os.IBinder import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.data.application.AppManager Loading Loading @@ -59,6 +60,13 @@ class InstallerService : Service() { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, DEFAULT_INSTALL_STATUS) if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) { launchUserConfirmation(intent) stopSelf() return START_NOT_STICKY } var packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) val extra = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) Loading @@ -75,6 +83,22 @@ class InstallerService : Service() { return START_NOT_STICKY } @Suppress("DEPRECATION") private fun launchUserConfirmation(intent: Intent) { val confirmIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) } else { intent.getParcelableExtra(Intent.EXTRA_INTENT) } if (confirmIntent != null) { confirmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(confirmIntent) Timber.d("Launched user confirmation dialog for install") } else { Timber.e("STATUS_PENDING_USER_ACTION but no confirmation intent found") } } private fun postStatus(status: Int, packageName: String?, extra: String?) { Timber.d("postStatus: $status $packageName $extra") if (status == PackageInstaller.STATUS_SUCCESS) { Loading
app/src/main/java/foundation/e/apps/data/updates/InstalledAppUpdateInfoProviderImpl.kt 0 → 100644 +50 −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.updates import foundation.e.apps.data.install.pkg.AppLoungePackageManager import foundation.e.apps.domain.install.InstalledAppUpdateInfoProvider import foundation.e.apps.domain.model.install.Status import javax.inject.Inject import javax.inject.Singleton @Singleton class InstalledAppUpdateInfoProviderImpl @Inject constructor( private val appLoungePackageManager: AppLoungePackageManager, private val updatesManagerImpl: UpdatesManagerImpl, ) : InstalledAppUpdateInfoProvider { override fun getPackageStatus(packageName: String, versionCode: Long): Status { return appLoungePackageManager.getPackageStatus(packageName, versionCode) } override fun isInstalledFromOtherStore(packageName: String): Boolean { return updatesManagerImpl.isAppInstalledFromOtherStore(packageName) } override fun getInstallerLabel(packageName: String): String? { val installerPackageName = appLoungePackageManager.getInstallerName(packageName) .takeIf { it.isNotBlank() } ?: return null return runCatching { appLoungePackageManager.getAppNameFromPackageName(installerPackageName) }.getOrDefault(installerPackageName).ifBlank { installerPackageName } } }
app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt +27 −7 Original line number Diff line number Diff line Loading @@ -68,12 +68,16 @@ class UpdatesManagerImpl @Inject constructor( if (appPreferencesRepository.shouldUpdateAppsFromOtherStores()) { withContext(Dispatchers.IO) { val otherStoresInstalledApps = getAppsFromOtherStores().toMutableList() val fdroidInstalledApps = getFDroidInstalledApps() openSourceInstalledApps.addAll(fdroidInstalledApps) val otherStoresInstalledApps = getAppsFromOtherStores() .filterNot { it in fdroidInstalledApps } .toMutableList() // This list is based on app signatures val updatableFDroidApps = findPackagesMatchingFDroidSignatures(otherStoresInstalledApps) openSourceInstalledApps.addAll(updatableFDroidApps) otherStoresInstalledApps.removeAll(updatableFDroidApps) Loading Loading @@ -130,7 +134,12 @@ class UpdatesManagerImpl @Inject constructor( val openSourceInstalledApps = getOpenSourceInstalledApps().toMutableList() if (appPreferencesRepository.shouldUpdateAppsFromOtherStores()) { val otherStoresInstalledApps = getAppsFromOtherStores().toMutableList() val fdroidInstalledApps = getFDroidInstalledApps() openSourceInstalledApps.addAll(fdroidInstalledApps) val otherStoresInstalledApps = getAppsFromOtherStores() .filterNot { it in fdroidInstalledApps } .toMutableList() // This list is based on app signatures val updatableFDroidApps = Loading Loading @@ -195,13 +204,21 @@ class UpdatesManagerImpl @Inject constructor( /** * Lists apps directly updatable by App Lounge from the Open Source category. * (This includes apps installed by F-Droid client app, if used by the user; * F-Droid is not considered a third party source.) * * Apps installed by external clients such as F-Droid are handled through the * "other stores" flow so the user preference can enable or disable them. */ private fun getOpenSourceInstalledApps(): List<String> { return userApplications.filter { appLoungePackageManager.getInstallerName(it.packageName) in listOf( context.packageName, ) }.map { it.packageName } } private fun getFDroidInstalledApps(): List<String> { return userApplications.filter { appLoungePackageManager.getInstallerName(it.packageName) in listOf( PACKAGE_NAME_F_DROID, PACKAGE_NAME_F_DROID_PRIVILEGED, ) Loading @@ -224,10 +241,9 @@ class UpdatesManagerImpl @Inject constructor( /** * Lists apps installed from other app stores. * (F-Droid client is not considered a third party source.) * * @return List of package names of apps installed from other app stores like * Aurora Store, Apk mirror, apps installed from browser, apps from ADB etc. * F-Droid, Aurora Store, ApkMirror, browser downloads, apps from ADB, etc. */ private fun getAppsFromOtherStores(): List<String> { val gplayAndOpenSourceInstalledApps = getGPlayInstalledApps() + getOpenSourceInstalledApps() Loading @@ -236,6 +252,10 @@ class UpdatesManagerImpl @Inject constructor( }.map { it.packageName } } fun isAppInstalledFromOtherStore(packageName: String): Boolean { return getAppsFromOtherStores().contains(packageName) } /** * Runs API (GPlay api or CleanApk) and accumulates the updatable apps * into a provided list. Loading
app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerRepository.kt +12 −3 Original line number Diff line number Diff line Loading @@ -22,20 +22,29 @@ import foundation.e.apps.data.application.UpdatesDao import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Source import foundation.e.apps.domain.preferences.AppPreferencesRepository import javax.inject.Inject class UpdatesManagerRepository @Inject constructor( private val updatesManagerImpl: UpdatesManagerImpl private val updatesManagerImpl: UpdatesManagerImpl, private val appPreferencesRepository: AppPreferencesRepository, ) { fun isAppInstalledFromOtherStore(packageName: String): Boolean { return updatesManagerImpl.isAppInstalledFromOtherStore(packageName) } suspend fun getUpdates(): Pair<List<Application>, ResultStatus> { if (UpdatesDao.hasAnyAppsForUpdate()) { val includesOtherStores = appPreferencesRepository.shouldUpdateAppsFromOtherStores() if ( UpdatesDao.hasAnyAppsForUpdate() && UpdatesDao.appsAwaitingForUpdateIncludesOtherStores == includesOtherStores ) { return Pair(UpdatesDao.appsAwaitingForUpdate, ResultStatus.OK) } return updatesManagerImpl.getUpdates().run { val filteredApps = first.filter { !(!it.isFree && it.source == Source.PLAY_STORE && !it.isPurchased) } UpdatesDao.addItemsForUpdate(filteredApps) UpdatesDao.addItemsForUpdate(filteredApps, includesOtherStores) Pair(filteredApps, this.second) } } Loading