diff --git a/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt index 875997773f127f10c1e9516f0bac87947ffe37e2..373c5903e352f7b737a8a5d8741daf8e57577ec6 100644 --- a/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt @@ -478,6 +478,7 @@ class MainActivityViewModel @Inject constructor( if (shouldShowPaidAppsSnackBar(app)) { return } + viewModelScope.launch { val fusedDownload: FusedDownload try { diff --git a/app/src/main/java/foundation/e/apps/api/DownloadManager.kt b/app/src/main/java/foundation/e/apps/api/DownloadManager.kt index 850c0acb142b9869ba312f049271fd0fe3f6359a..3d6f64dbfc2f703494c72708bc100098a7724122 100644 --- a/app/src/main/java/foundation/e/apps/api/DownloadManager.kt +++ b/app/src/main/java/foundation/e/apps/api/DownloadManager.kt @@ -127,11 +127,36 @@ class DownloadManager @Inject constructor( } } - private fun tickerFlow(downloadId: Long, period: Duration, initialDelay: Duration = Duration.ZERO) = flow { + private fun tickerFlow( + downloadId: Long, + period: Duration, + initialDelay: Duration = Duration.ZERO + ) = flow { delay(initialDelay) while (downloadsMaps[downloadId]!!) { emit(Unit) delay(period) } } + + fun isDownloadSuccessful(downloadId: Long): Boolean { + return getDownloadStatus(downloadId) == DownloadManager.STATUS_SUCCESSFUL + } + + private fun getDownloadStatus(downloadId: Long): Int { + try { + downloadManager.query(downloadManagerQuery.setFilterById(downloadId)) + .use { cursor -> + if (cursor.moveToFirst()) { + val status = + cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) + Timber.d("Download Failed: downloadId: $downloadId $status") + return status + } + } + } catch (e: Exception) { + Timber.e(e) + } + return DownloadManager.STATUS_FAILED + } } diff --git a/app/src/main/java/foundation/e/apps/api/cleanapk/ApkSignatureManager.kt b/app/src/main/java/foundation/e/apps/api/cleanapk/ApkSignatureManager.kt index f9bd5443639eb6523efafa4ddfc1afeb754e08b7..ea7e58338ecc22ede69a76fd091d9731bcc1bfb2 100644 --- a/app/src/main/java/foundation/e/apps/api/cleanapk/ApkSignatureManager.kt +++ b/app/src/main/java/foundation/e/apps/api/cleanapk/ApkSignatureManager.kt @@ -27,6 +27,7 @@ import org.bouncycastle.openpgp.PGPUtil import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator +import timber.log.Timber import java.io.BufferedInputStream import java.io.FileInputStream import java.io.InputStream @@ -35,11 +36,16 @@ import java.security.Security object ApkSignatureManager { fun verifyFdroidSignature(context: Context, apkFilePath: String, signature: String): Boolean { Security.addProvider(BouncyCastleProvider()) - return verifyAPKSignature( - BufferedInputStream(FileInputStream(apkFilePath)), - signature.byteInputStream(Charsets.UTF_8), - context.assets.open("f-droid.org-signing-key.gpg") - ) + try { + return verifyAPKSignature( + BufferedInputStream(FileInputStream(apkFilePath)), + signature.byteInputStream(Charsets.UTF_8), + context.assets.open("f-droid.org-signing-key.gpg") + ) + } catch (e: Exception) { + Timber.e(e) + } + return false } private fun verifyAPKSignature( diff --git a/app/src/main/java/foundation/e/apps/api/fused/UpdatesDao.kt b/app/src/main/java/foundation/e/apps/api/fused/UpdatesDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..04d6ea2e28391b60dbe61611aadf8fdb6405f883 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/fused/UpdatesDao.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 MURENA SAS + * + * 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 . + */ + +package foundation.e.apps.api.fused + +import foundation.e.apps.api.fused.data.FusedApp + +object UpdatesDao { + var appsAwaitingForUpdate: List = listOf() + + fun hasAnyAppsForUpdate() = appsAwaitingForUpdate.isNotEmpty() +} diff --git a/app/src/main/java/foundation/e/apps/manager/download/DownloadManagerUtils.kt b/app/src/main/java/foundation/e/apps/manager/download/DownloadManagerUtils.kt index c5a949a87fc41a71944a3dbabe36ca3b6492ff36..2661920c362ed334b2c85c40cd4aa9dcbca4f46b 100644 --- a/app/src/main/java/foundation/e/apps/manager/download/DownloadManagerUtils.kt +++ b/app/src/main/java/foundation/e/apps/manager/download/DownloadManagerUtils.kt @@ -1,88 +1,115 @@ -/* - * Copyright ECORP SAS 2022 - * Apps Quickly and easily install Android apps onto your device! - * - * 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 . - */ - -package foundation.e.apps.manager.download - -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext -import foundation.e.apps.manager.database.fusedDownload.FusedDownload -import foundation.e.apps.manager.fused.FusedManagerRepository -import foundation.e.apps.utils.enums.Origin -import foundation.e.apps.utils.enums.Status -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class DownloadManagerUtils @Inject constructor( - private val fusedManagerRepository: FusedManagerRepository, - @ApplicationContext private val context: Context -) { - private val TAG = DownloadManagerUtils::class.java.simpleName - private val mutex = Mutex() - - @DelicateCoroutinesApi - fun cancelDownload(downloadId: Long) { - GlobalScope.launch { - val fusedDownload = fusedManagerRepository.getFusedDownload(downloadId) - fusedManagerRepository.cancelDownload(fusedDownload) - } - } - - @DelicateCoroutinesApi - fun updateDownloadStatus(downloadId: Long) { - GlobalScope.launch { - mutex.withLock { - delay(1500) // Waiting for downloadmanager to publish the progress of last bytes - val fusedDownload = fusedManagerRepository.getFusedDownload(downloadId) - if (fusedDownload.id.isNotEmpty()) { - fusedDownload.downloadIdMap[downloadId] = true - fusedManagerRepository.updateFusedDownload(fusedDownload) - val downloaded = fusedDownload.downloadIdMap.values.filter { it }.size - Timber.d("===> updateDownloadStatus: ${fusedDownload.name}: $downloadId: $downloaded/${fusedDownload.downloadIdMap.size}") - if (downloaded == fusedDownload.downloadIdMap.size && checkCleanApkSignatureOK(fusedDownload)) { - fusedManagerRepository.moveOBBFileToOBBDirectory(fusedDownload) - fusedDownload.status = Status.DOWNLOADED - fusedManagerRepository.updateFusedDownload(fusedDownload) - } - } - } - } - } - - private suspend fun checkCleanApkSignatureOK(fusedDownload: FusedDownload): Boolean { - if (fusedDownload.origin != Origin.CLEANAPK || fusedManagerRepository.isFdroidApplicationSigned( - context, - fusedDownload - ) - ) { - Timber.d("Apk signature is OK") - return true - } - fusedDownload.status = Status.INSTALLATION_ISSUE - fusedManagerRepository.updateFusedDownload(fusedDownload) - Timber.d("CleanApk signature is Wrong!") - return false - } -} +/* + * Copyright ECORP SAS 2022 + * Apps Quickly and easily install Android apps onto your device! + * + * 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 . + */ + +package foundation.e.apps.manager.download + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.api.DownloadManager +import foundation.e.apps.manager.database.fusedDownload.FusedDownload +import foundation.e.apps.manager.fused.FusedManagerRepository +import foundation.e.apps.utils.enums.Origin +import foundation.e.apps.utils.enums.Status +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DownloadManagerUtils @Inject constructor( + @ApplicationContext private val context: Context, + private val fusedManagerRepository: FusedManagerRepository, + private val downloadManager: DownloadManager +) { + private val TAG = DownloadManagerUtils::class.java.simpleName + private val mutex = Mutex() + + @DelicateCoroutinesApi + fun cancelDownload(downloadId: Long) { + GlobalScope.launch { + val fusedDownload = fusedManagerRepository.getFusedDownload(downloadId) + fusedManagerRepository.cancelDownload(fusedDownload) + } + } + + @DelicateCoroutinesApi + fun updateDownloadStatus(downloadId: Long) { + GlobalScope.launch { + mutex.withLock { + delay(1500) // Waiting for downloadmanager to publish the progress of last bytes + val fusedDownload = fusedManagerRepository.getFusedDownload(downloadId) + if (fusedDownload.id.isNotEmpty()) { + updateDownloadIdMap(fusedDownload, downloadId) + val numberOfDownloadedItems = + fusedDownload.downloadIdMap.values.filter { it }.size + Timber.d("===> updateDownloadStatus: ${fusedDownload.name}: $downloadId: $numberOfDownloadedItems/${fusedDownload.downloadIdMap.size}") + + if (validateDownload(numberOfDownloadedItems, fusedDownload, downloadId)) { + Timber.d("===> Download is completed for: ${fusedDownload.name}") + fusedManagerRepository.moveOBBFileToOBBDirectory(fusedDownload) + fusedDownload.status = Status.DOWNLOADED + fusedManagerRepository.updateFusedDownload(fusedDownload) + } + } + } + } + } + + private suspend fun validateDownload( + numberOfDownloadedItems: Int, + fusedDownload: FusedDownload, + downloadId: Long + ) = downloadManager.isDownloadSuccessful(downloadId) && + areAllFilesDownloaded( + numberOfDownloadedItems, + fusedDownload + ) && checkCleanApkSignatureOK(fusedDownload) + + private fun areAllFilesDownloaded( + numberOfDownloadedItems: Int, + fusedDownload: FusedDownload + ) = numberOfDownloadedItems == fusedDownload.downloadIdMap.size + + private suspend fun updateDownloadIdMap( + fusedDownload: FusedDownload, + downloadId: Long + ) { + fusedDownload.downloadIdMap[downloadId] = true + fusedManagerRepository.updateFusedDownload(fusedDownload) + } + + private suspend fun checkCleanApkSignatureOK(fusedDownload: FusedDownload): Boolean { + if (fusedDownload.origin != Origin.CLEANAPK || fusedManagerRepository.isFdroidApplicationSigned( + context, + fusedDownload + ) + ) { + Timber.d("Apk signature is OK") + return true + } + fusedDownload.status = Status.INSTALLATION_ISSUE + fusedManagerRepository.updateFusedDownload(fusedDownload) + Timber.d("CleanApk signature is Wrong!") + return false + } +} diff --git a/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerImpl.kt b/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerImpl.kt index ece25c8b6fc811064746e58bad59c3f222918c15..ed576ff67a627e49376c3abde7908d7ae6f0313b 100644 --- a/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerImpl.kt +++ b/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerImpl.kt @@ -74,6 +74,10 @@ class FusedManagerImpl @Inject constructor( databaseRepository.addDownload(fusedDownload) } + suspend fun getDownloadById(fusedDownload: FusedDownload): FusedDownload? { + return databaseRepository.getDownloadById(fusedDownload.id) + } + suspend fun getDownloadList(): List { return databaseRepository.getDownloadList() } diff --git a/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt b/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt index e6ffa4627e2111c65a4c3450d390a81aab1c0d14..98c24363be6927f5f2b50dcc84c2dd121aa81bcb 100644 --- a/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt +++ b/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt @@ -9,6 +9,7 @@ import foundation.e.apps.api.fdroid.FdroidRepository import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.download.data.DownloadProgress +import foundation.e.apps.manager.workmanager.InstallWorkManager import foundation.e.apps.utils.enums.Status import kotlinx.coroutines.flow.Flow import javax.inject.Inject @@ -34,6 +35,16 @@ class FusedManagerRepository @Inject constructor( } suspend fun addDownload(fusedDownload: FusedDownload) { + if (InstallWorkManager.checkWorkIsAlreadyAvailable(fusedDownload.id)) { + return + } + + val existingFusedDownload = fusedManagerImpl.getDownloadById(fusedDownload) + // We don't want to add any thing, if it already exists without INSTALLATION_ISSUE + if (existingFusedDownload != null && existingFusedDownload.status != Status.INSTALLATION_ISSUE) { + return + } + return fusedManagerImpl.addDownload(fusedDownload) } diff --git a/app/src/main/java/foundation/e/apps/manager/pkg/PkgManagerModule.kt b/app/src/main/java/foundation/e/apps/manager/pkg/PkgManagerModule.kt index ad411ac0a4e79d1e065d45f3b5f3957df2db5e85..7d00c04b4211dc5e6475752a8a0ef1de5921853e 100644 --- a/app/src/main/java/foundation/e/apps/manager/pkg/PkgManagerModule.kt +++ b/app/src/main/java/foundation/e/apps/manager/pkg/PkgManagerModule.kt @@ -50,6 +50,7 @@ class PkgManagerModule @Inject constructor( companion object { const val ERROR_PACKAGE_INSTALL = "ERROR_PACKAGE_INSTALL" const val PACKAGE_NAME = "packageName" + const val FAKE_STORE_PACKAGE_NAME = "com.android.vending" private const val TAG = "PkgManagerModule" } private val packageManager = context.packageManager @@ -115,12 +116,11 @@ class PkgManagerModule @Inject constructor( return } if (fusedDownload.origin == Origin.GPLAY) { - val fakeStorePackageName = "com.android.vending" - if (fusedDownload.type == Type.NATIVE && isInstalled(fakeStorePackageName)) { + if (fusedDownload.type == Type.NATIVE && isInstalled(FAKE_STORE_PACKAGE_NAME)) { val targetPackage = fusedDownload.packageName try { - packageManager.setInstallerPackageName(targetPackage, fakeStorePackageName) - Timber.d("Changed installer to $fakeStorePackageName for $targetPackage") + packageManager.setInstallerPackageName(targetPackage, FAKE_STORE_PACKAGE_NAME) + Timber.d("Changed installer to $FAKE_STORE_PACKAGE_NAME for $targetPackage") } catch (e: Exception) { e.printStackTrace() } @@ -239,6 +239,11 @@ class PkgManagerModule @Inject constructor( return userPackages } + fun isGplay(packageName: String): Boolean { + val installerPackageName = packageManager.getInstallerPackageName(packageName) + return installerPackageName?.contains(FAKE_STORE_PACKAGE_NAME) == true + } + fun getAllSystemApps(): List { return packageManager.getInstalledApplications(PackageManager.MATCH_SYSTEM_ONLY) } diff --git a/app/src/main/java/foundation/e/apps/manager/workmanager/InstallWorkManager.kt b/app/src/main/java/foundation/e/apps/manager/workmanager/InstallWorkManager.kt index 08edd715f426ccfc23b3d99626c2298befa8a5ba..fd782aea4cee10d8763fe358aced297daf469e8a 100644 --- a/app/src/main/java/foundation/e/apps/manager/workmanager/InstallWorkManager.kt +++ b/app/src/main/java/foundation/e/apps/manager/workmanager/InstallWorkManager.kt @@ -6,6 +6,7 @@ import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import foundation.e.apps.manager.database.fusedDownload.FusedDownload +import java.lang.Exception object InstallWorkManager { const val INSTALL_WORK_NAME = "APP_LOUNGE_INSTALL_APP" @@ -27,4 +28,18 @@ object InstallWorkManager { fun cancelWork(tag: String) { WorkManager.getInstance(context).cancelAllWorkByTag(tag) } + + fun checkWorkIsAlreadyAvailable(tag: String): Boolean { + val works = WorkManager.getInstance(context).getWorkInfosByTag(tag) + try { + works.get().forEach { + if (it.tags.contains(tag) && !it.state.isFinished) { + return true + } + } + } catch (e: Exception) { + e.printStackTrace() + } + return false + } } diff --git a/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt b/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt index 5e8d1a1009037984b9021cdec2827acc0a86c4cc..aae5bb2b8f99878ec0616f4b592a02d646f1518c 100644 --- a/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt +++ b/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt @@ -28,6 +28,7 @@ import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.work.WorkInfo import androidx.work.WorkManager import com.aurora.gplayapi.data.models.AuthData import dagger.hilt.android.AndroidEntryPoint @@ -200,10 +201,27 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte updatesViewModel.getUpdates(authData) binding.button.setOnClickListener { UpdatesWorkManager.startUpdateAllWork(requireContext().applicationContext) + observeUpdateWork() binding.button.isEnabled = false } } + private fun observeUpdateWork() { + WorkManager.getInstance(requireContext()) + .getWorkInfosByTagLiveData(UpdatesWorkManager.UPDATES_WORK_NAME) + .observe(viewLifecycleOwner) { + val errorStates = + listOf( + WorkInfo.State.FAILED, + WorkInfo.State.BLOCKED, + WorkInfo.State.CANCELLED + ) + if (!it.isNullOrEmpty() && errorStates.contains(it.last().state)) { + binding.button.isEnabled = true + } + } + } + private fun showLoadingUI() { binding.button.isEnabled = false binding.noUpdates.visibility = View.GONE diff --git a/app/src/main/java/foundation/e/apps/updates/UpdatesNotifier.kt b/app/src/main/java/foundation/e/apps/updates/UpdatesNotifier.kt index 2eaa3325743355c6ac648d85d1400715dbd60f40..61a5116ebf1a32dcd8e3e33200e6a7f120cdf63c 100644 --- a/app/src/main/java/foundation/e/apps/updates/UpdatesNotifier.kt +++ b/app/src/main/java/foundation/e/apps/updates/UpdatesNotifier.kt @@ -29,15 +29,13 @@ import androidx.core.app.NotificationManagerCompat import foundation.e.apps.MainActivity import foundation.e.apps.R -class UpdatesNotifier { - companion object { - const val UPDATES_NOTIFICATION_CLICK_EXTRA = "updates_notification_click_extra" - private const val UPDATES_NOTIFICATION_ID = 76 - private const val UPDATES_NOTIFICATION_CHANNEL_ID = "updates_notification" - private const val UPDATES_NOTIFICATION_CHANNEL_TITLE = "App updates" - } +object UpdatesNotifier { + const val UPDATES_NOTIFICATION_CLICK_EXTRA = "updates_notification_click_extra" + private const val UPDATES_NOTIFICATION_ID = 76 + private const val UPDATES_NOTIFICATION_CHANNEL_ID = "updates_notification" + private const val UPDATES_NOTIFICATION_CHANNEL_TITLE = "App updates" - private fun getNotification( + fun getNotification( context: Context, numberOfApps: Int, installAutomatically: Boolean, @@ -48,22 +46,31 @@ class UpdatesNotifier { NotificationCompat.Builder(context, UPDATES_NOTIFICATION_CHANNEL_ID) notificationBuilder.setSmallIcon(R.drawable.ic_app_updated_on) notificationBuilder.priority = NotificationCompat.PRIORITY_DEFAULT - if (numberOfApps == 1) { - notificationBuilder.setContentTitle( - context.resources.getQuantityString( - R.plurals.updates_notification_title, - 1, - numberOfApps + + when (numberOfApps) { + 0 -> { + notificationBuilder.setContentTitle( + "Checking Updates..." ) - ) - } else { - notificationBuilder.setContentTitle( - context.resources.getQuantityString( - R.plurals.updates_notification_title, - numberOfApps, - numberOfApps + } + 1 -> { + notificationBuilder.setContentTitle( + context.resources.getQuantityString( + R.plurals.updates_notification_title, + 1, + numberOfApps + ) ) - ) + } + else -> { + notificationBuilder.setContentTitle( + context.resources.getQuantityString( + R.plurals.updates_notification_title, + numberOfApps, + numberOfApps + ) + ) + } } if (installAutomatically) { notificationBuilder.setContentText(context.getString(R.string.automatically_install_updates_notification_text)) @@ -124,4 +131,10 @@ class UpdatesNotifier { ) } } + + fun cancelNotification(context: Context) { + val notificationManager: NotificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(UPDATES_NOTIFICATION_ID) + } } diff --git a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt index ffd48afa1631b5db47d3ad74ea6a3197f6907286..5fd534f7ad62f1482751c1dc15916693f8fd578e 100644 --- a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt +++ b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt @@ -47,8 +47,10 @@ class UpdatesManagerImpl @Inject constructor( if (pkgList.isNotEmpty()) { // Get updates from CleanAPK + val openSourcePackages = userApplications.filter { !pkgManagerModule.isGplay(it.packageName) }.map { it.packageName } + pkgList.removeAll(openSourcePackages) val cleanAPKResult = fusedAPIRepository.getApplicationDetails( - pkgList, + openSourcePackages, authData, Origin.CLEANAPK ) diff --git a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerRepository.kt b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerRepository.kt index dadc807ca4a276286a39aba811090c4924e9c965..0805a8a5bd6b54b23c9745153b65a8361c1d7250 100644 --- a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerRepository.kt +++ b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerRepository.kt @@ -19,6 +19,7 @@ package foundation.e.apps.updates.manager import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.api.fused.UpdatesDao import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.utils.enums.ResultStatus import javax.inject.Inject @@ -28,8 +29,12 @@ class UpdatesManagerRepository @Inject constructor( ) { suspend fun getUpdates(authData: AuthData): Pair, ResultStatus> { + if (UpdatesDao.hasAnyAppsForUpdate()) { + return Pair(UpdatesDao.appsAwaitingForUpdate, ResultStatus.OK) + } return updatesManagerImpl.getUpdates(authData).run { val filteredApps = first.filter { !(!it.isFree && authData.isAnonymous) } + UpdatesDao.appsAwaitingForUpdate = filteredApps Pair(filteredApps, this.second) } } diff --git a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorkManager.kt b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorkManager.kt index c70345a09b504d7d280b24677cde634997ed556b..f237e51c684630b4b0e2a7056465837bffe1bb3a 100644 --- a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorkManager.kt +++ b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorkManager.kt @@ -20,24 +20,28 @@ package foundation.e.apps.updates.manager import android.content.Context import android.util.Log import androidx.work.Constraints +import androidx.work.Data import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingWorkPolicy import androidx.work.NetworkType import androidx.work.OneTimeWorkRequest import androidx.work.PeriodicWorkRequest import androidx.work.WorkManager +import java.util.UUID import java.util.concurrent.TimeUnit object UpdatesWorkManager { const val UPDATES_WORK_NAME = "updates_work" private const val TAG = "UpdatesManager" - fun startUpdateAllWork(context: Context) { + fun startUpdateAllWork(context: Context): UUID { + val oneTimeWorkRequest = buildOneTimeWorkRequest() WorkManager.getInstance(context).enqueueUniqueWork( UPDATES_WORK_NAME, ExistingWorkPolicy.REPLACE, buildOneTimeWorkRequest() ) + return oneTimeWorkRequest.id } private fun buildWorkerConstraints() = Constraints.Builder().apply { @@ -52,13 +56,16 @@ object UpdatesWorkManager { TimeUnit.HOURS ).apply { setConstraints(buildWorkerConstraints()) + addTag(TAG) }.build() } private fun buildOneTimeWorkRequest(): OneTimeWorkRequest { return OneTimeWorkRequest.Builder(UpdatesWorker::class.java).apply { setConstraints(buildWorkerConstraints()) - }.build() + addTag(UPDATES_WORK_NAME) + }.setInputData(Data.Builder().putBoolean(UpdatesWorker.IS_AUTO_UPDATE, false).build()) + .build() } fun enqueueWork( diff --git a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt index 46597fee660d8c09b7064fef1824a208f0cc417f..e8965a51d8f4624c9a4af43eab0379d74d14b453 100644 --- a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt +++ b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt @@ -34,39 +34,47 @@ import java.net.URL @HiltWorker class UpdatesWorker @AssistedInject constructor( @Assisted private val context: Context, - @Assisted params: WorkerParameters, + @Assisted private val params: WorkerParameters, private val updatesManagerRepository: UpdatesManagerRepository, private val fusedAPIRepository: FusedAPIRepository, private val fusedManagerRepository: FusedManagerRepository, private val dataStoreModule: DataStoreModule, private val gson: Gson, ) : CoroutineWorker(context, params) { + + companion object { + const val IS_AUTO_UPDATE = "IS_AUTO_UPDATE" + } + val TAG = UpdatesWorker::class.simpleName private var shouldShowNotification = true private var automaticInstallEnabled = true private var onlyOnUnmeteredNetwork = false + private var isAutoUpdate = true // indicates it is auto update or user initiated update override suspend fun doWork(): Result { return try { + isAutoUpdate = params.inputData.getBoolean(IS_AUTO_UPDATE, true) checkForUpdates() Result.success() } catch (e: Throwable) { Result.failure() + } finally { + if (shouldShowNotification && automaticInstallEnabled) { + UpdatesNotifier.cancelNotification(context) + } } } private suspend fun checkForUpdates() { loadSettings() + val isConnectedToUnmeteredNetwork = isConnectedToUnmeteredNetwork(applicationContext) val authData = getAuthData() val appsNeededToUpdate = updatesManagerRepository.getUpdates(authData).first - val isConnectedToUnmeteredNetwork = isConnectedToUnmeteredNetwork(applicationContext) - /* - * Show notification only if enabled. - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5376 - */ - if (shouldShowNotification) { - handleNotification(appsNeededToUpdate, isConnectedToUnmeteredNetwork) + if (isAutoUpdate && shouldShowNotification) { + handleNotification(appsNeededToUpdate.size, isConnectedToUnmeteredNetwork) } + triggerUpdateProcessOnSettings( isConnectedToUnmeteredNetwork, appsNeededToUpdate, @@ -79,7 +87,7 @@ class UpdatesWorker @AssistedInject constructor( appsNeededToUpdate: List, authData: AuthData ) { - if (automaticInstallEnabled && + if ((!isAutoUpdate || automaticInstallEnabled) && applicationContext.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED ) { if (onlyOnUnmeteredNetwork && isConnectedToUnmeteredNetwork) { @@ -91,13 +99,13 @@ class UpdatesWorker @AssistedInject constructor( } private fun handleNotification( - appsNeededToUpdate: List, + numberOfAppsNeedUpdate: Int, isConnectedToUnmeteredNetwork: Boolean ) { - if (appsNeededToUpdate.isNotEmpty()) { - UpdatesNotifier().showNotification( + if (numberOfAppsNeedUpdate > 0) { + UpdatesNotifier.showNotification( applicationContext, - appsNeededToUpdate.size, + numberOfAppsNeedUpdate, automaticInstallEnabled, onlyOnUnmeteredNetwork, isConnectedToUnmeteredNetwork diff --git a/app/src/main/java/foundation/e/apps/utils/modules/CommonUtilsFunctions.kt b/app/src/main/java/foundation/e/apps/utils/modules/CommonUtilsFunctions.kt index e31a5efcd22a21943d51972e1ec91f117aef4290..adf4d1552c87cb632a778055fd3361daa2c80ce3 100644 --- a/app/src/main/java/foundation/e/apps/utils/modules/CommonUtilsFunctions.kt +++ b/app/src/main/java/foundation/e/apps/utils/modules/CommonUtilsFunctions.kt @@ -20,6 +20,7 @@ package foundation.e.apps.utils.modules import android.annotation.SuppressLint import android.content.ClipData import android.content.ClipboardManager +import java.lang.Exception object CommonUtilsFunctions { diff --git a/app/src/test/java/foundation/e/apps/util/LiveDataTestUtil.kt b/app/src/test/java/foundation/e/apps/util/LiveDataTestUtil.kt index 679cc04e20a3f9ee6028459e0d239150f0cf790a..58e478c3403c125ee73da31fa21ff648d4d1fcc1 100644 --- a/app/src/test/java/foundation/e/apps/util/LiveDataTestUtil.kt +++ b/app/src/test/java/foundation/e/apps/util/LiveDataTestUtil.kt @@ -20,8 +20,6 @@ package foundation.e.apps.util import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import kotlinx.coroutines.delay -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit /** * Gets the value of a [LiveData] or waits for it to have one, with a timeout.