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

Commit f83c892a authored by Abhishek Aggarwal's avatar Abhishek Aggarwal Committed by Nishith Khanna
Browse files

feat(update): Add support for updating apps installed via 3rd party app store

parent 354c30a7
Loading
Loading
Loading
Loading
Loading
+15 −2
Original line number Diff line number Diff line
@@ -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)
+24 −0
Original line number Diff line number Diff line
@@ -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
@@ -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)

@@ -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) {
+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 }
    }
}
+27 −7
Original line number Diff line number Diff line
@@ -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)
@@ -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 =
@@ -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,
            )
@@ -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()
@@ -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.
+12 −3
Original line number Diff line number Diff line
@@ -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