Loading app/src/main/java/foundation/e/apps/manager/database/DatabaseRepository.kt +1 −1 Original line number Diff line number Diff line Loading @@ -53,7 +53,7 @@ class DatabaseRepository @Inject constructor( } } fun getDownloadFlowById(id: String): Flow<FusedDownload> { fun getDownloadFlowById(id: String): Flow<FusedDownload?> { return fusedDownloadDAO.getDownloadFlowById(id).asFlow() } } app/src/main/java/foundation/e/apps/manager/download/DownloadManagerBR.kt +2 −2 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.app.DownloadManager import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import androidx.annotation.UiThread import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.DelicateCoroutinesApi import timber.log.Timber Loading @@ -36,9 +37,9 @@ class DownloadManagerBR : BroadcastReceiver() { companion object { private const val TAG = "DownloadManagerBR" val downloadedList = mutableListOf<Long>() } @UiThread override fun onReceive(context: Context?, intent: Intent?) { val action = intent?.action if (context != null && action != null) { Loading @@ -46,7 +47,6 @@ class DownloadManagerBR : BroadcastReceiver() { Timber.i("onReceive: DownloadBR $action $id") when (action) { DownloadManager.ACTION_DOWNLOAD_COMPLETE -> { downloadedList.add(id) downloadManagerUtils.updateDownloadStatus(id) } DownloadManager.ACTION_NOTIFICATION_CLICKED -> { Loading app/src/main/java/foundation/e/apps/manager/fused/FusedManagerImpl.kt +19 −16 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.download.DownloadManagerBR import foundation.e.apps.manager.download.data.DownloadProgressLD import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.manager.workmanager.InstallWorkManager import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.Type import foundation.e.apps.utils.modules.PWAManagerModule Loading Loading @@ -61,6 +62,8 @@ class FusedManagerImpl @Inject constructor( private val TAG = FusedManagerImpl::class.java.simpleName private val mutex = Mutex() @RequiresApi(Build.VERSION_CODES.O) override fun createNotificationChannels() { notificationManager.apply { Loading Loading @@ -95,7 +98,6 @@ class FusedManagerImpl @Inject constructor( override suspend fun updateDownloadStatus(fusedDownload: FusedDownload, status: Status) { if (status == Status.INSTALLED) { fusedDownload.status = status DownloadManagerBR.downloadedList.clear() flushOldDownload(fusedDownload.packageName) databaseRepository.deleteDownload(fusedDownload) } else if (status == Status.INSTALLING) { Loading @@ -106,8 +108,6 @@ class FusedManagerImpl @Inject constructor( } } private val mutex = Mutex() override suspend fun downloadApp(fusedDownload: FusedDownload) { mutex.withLock { when (fusedDownload.type) { Loading Loading @@ -147,23 +147,26 @@ class FusedManagerImpl @Inject constructor( @OptIn(DelicateCoroutinesApi::class) override suspend fun cancelDownload(fusedDownload: FusedDownload) { mutex.withLock { if (fusedDownload.id.isNotBlank()) { removeFusedDownload(fusedDownload) } else { Timber.d("Unable to cancel download!") } } } private suspend fun removeFusedDownload(fusedDownload: FusedDownload) { fusedDownload.downloadIdMap.forEach { (key, _) -> downloadManager.remove(key) } DownloadProgressLD.setDownloadId(-1) DownloadManagerBR.downloadedList.clear() // Reset the status before deleting download updateDownloadStatus(fusedDownload, fusedDownload.orgStatus) if (fusedDownload.status != Status.INSTALLATION_ISSUE) { databaseRepository.deleteDownload(fusedDownload) } flushOldDownload(fusedDownload.packageName) } else { Timber.d("Unable to cancel download!") } } override suspend fun getFusedDownload(downloadId: Long, packageName: String): FusedDownload { Loading app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt +10 −2 Original line number Diff line number Diff line Loading @@ -37,11 +37,11 @@ class FusedManagerRepository @Inject constructor( } suspend fun addDownload(fusedDownload: FusedDownload): Boolean { if (InstallWorkManager.checkWorkIsAlreadyAvailable(fusedDownload.id)) { val existingFusedDownload = fusedManagerImpl.getDownloadById(fusedDownload) if (isInstallWorkRunning(existingFusedDownload, fusedDownload)) { return false } 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 false Loading @@ -51,6 +51,14 @@ class FusedManagerRepository @Inject constructor( return true } private fun isInstallWorkRunning( existingFusedDownload: FusedDownload?, fusedDownload: FusedDownload ) = existingFusedDownload != null && InstallWorkManager.checkWorkIsAlreadyAvailable( fusedDownload.id ) suspend fun addFusedDownloadPurchaseNeeded(fusedDownload: FusedDownload) { fusedManagerImpl.insertFusedDownloadPurchaseNeeded(fusedDownload) } Loading app/src/main/java/foundation/e/apps/manager/workmanager/AppInstallProcessor.kt +39 −60 Original line number Diff line number Diff line Loading @@ -29,20 +29,12 @@ import foundation.e.apps.updates.UpdatesNotifier import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.modules.DataStoreManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.flow.transformWhile import timber.log.Timber import java.text.NumberFormat import java.text.SimpleDateFormat import java.util.Date import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds class AppInstallProcessor @Inject constructor( @ApplicationContext private val context: Context, Loading @@ -51,15 +43,12 @@ class AppInstallProcessor @Inject constructor( private val dataStoreManager: DataStoreManager ) { private var isDownloading: Boolean = false private var isItUpdateWork = false companion object { private const val TAG = "AppInstallProcessor" } private val mutex = Mutex(true) suspend fun processInstall( fusedDownloadId: String, isItUpdateWork: Boolean, Loading Loading @@ -96,7 +85,6 @@ class AppInstallProcessor @Inject constructor( runInForeground?.invoke(it.name) startAppInstallationProcess(it) mutex.lock() } } catch (e: Exception) { Timber.e("doWork: Failed: ${e.stackTraceToString()}") Loading @@ -121,7 +109,8 @@ class AppInstallProcessor @Inject constructor( ) { if (isItUpdateWork) { fusedDownload?.let { val packageStatus = fusedManagerRepository.getFusedDownloadPackageStatus(fusedDownload) val packageStatus = fusedManagerRepository.getFusedDownloadPackageStatus(fusedDownload) if (packageStatus == Status.INSTALLED) { UpdatesDao.addSuccessfullyUpdatedApp(it) Loading Loading @@ -165,33 +154,44 @@ class AppInstallProcessor @Inject constructor( ) } private suspend fun startAppInstallationProcess( fusedDownload: FusedDownload ): Boolean { private suspend fun startAppInstallationProcess(fusedDownload: FusedDownload) { if (fusedDownload.isAwaiting()) { fusedManagerRepository.downloadApp(fusedDownload) Timber.i("===> doWork: Download started ${fusedDownload.name} ${fusedDownload.status}") } isDownloading = true databaseRepository.getDownloadFlowById(fusedDownload.id) .transformWhile { emit(it) isInstallRunning(it) }.collect { latestFusedDownload -> handleFusedDownload(latestFusedDownload, fusedDownload) } } /** * observe app download/install process in a separate thread as DownloadManager download artifacts in a separate process * It checks install status every three seconds * Takes actions depending on the status of [FusedDownload] * * @param latestFusedDownload comes from Room database when [Status] is updated * @param fusedDownload is the original object when install process isn't started. It's used when [latestFusedDownload] * becomes null, After installation is completed. */ tickerFlow(3.seconds) .onEach { val download = databaseRepository.getDownloadById(fusedDownload.id) if (download == null) { private suspend fun handleFusedDownload( latestFusedDownload: FusedDownload?, fusedDownload: FusedDownload ) { if (latestFusedDownload == null) { Timber.d("===> download null: finish installation") finishInstallation(fusedDownload) } else { handleFusedDownloadStatusCheckingException(download) return } }.launchIn(CoroutineScope(Dispatchers.IO)) Timber.d(">>> ===> doWork: Download started " + fusedDownload.name + " " + fusedDownload.status) return true handleFusedDownloadStatusCheckingException(latestFusedDownload) } private fun isInstallRunning(it: FusedDownload?) = it != null && it.status != Status.INSTALLATION_ISSUE private suspend fun handleFusedDownloadStatusCheckingException( download: FusedDownload ) { Loading @@ -204,19 +204,6 @@ class AppInstallProcessor @Inject constructor( } } /** * Triggers a repetitive event according to the delay passed in the parameter * @param period delay of each event * @param initialDelay initial delay to trigger the first event */ private fun tickerFlow(period: Duration, initialDelay: Duration = Duration.ZERO) = flow { delay(initialDelay) while (isDownloading) { emit(Unit) delay(period) } } private suspend fun handleFusedDownloadStatus(fusedDownload: FusedDownload) { when (fusedDownload.status) { Status.AWAITING, Status.DOWNLOADING -> { Loading @@ -243,13 +230,5 @@ class AppInstallProcessor @Inject constructor( private suspend fun finishInstallation(fusedDownload: FusedDownload) { checkUpdateWork(fusedDownload) isDownloading = false unlockMutex() } private fun unlockMutex() { if (mutex.isLocked) { mutex.unlock() } } } Loading
app/src/main/java/foundation/e/apps/manager/database/DatabaseRepository.kt +1 −1 Original line number Diff line number Diff line Loading @@ -53,7 +53,7 @@ class DatabaseRepository @Inject constructor( } } fun getDownloadFlowById(id: String): Flow<FusedDownload> { fun getDownloadFlowById(id: String): Flow<FusedDownload?> { return fusedDownloadDAO.getDownloadFlowById(id).asFlow() } }
app/src/main/java/foundation/e/apps/manager/download/DownloadManagerBR.kt +2 −2 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.app.DownloadManager import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import androidx.annotation.UiThread import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.DelicateCoroutinesApi import timber.log.Timber Loading @@ -36,9 +37,9 @@ class DownloadManagerBR : BroadcastReceiver() { companion object { private const val TAG = "DownloadManagerBR" val downloadedList = mutableListOf<Long>() } @UiThread override fun onReceive(context: Context?, intent: Intent?) { val action = intent?.action if (context != null && action != null) { Loading @@ -46,7 +47,6 @@ class DownloadManagerBR : BroadcastReceiver() { Timber.i("onReceive: DownloadBR $action $id") when (action) { DownloadManager.ACTION_DOWNLOAD_COMPLETE -> { downloadedList.add(id) downloadManagerUtils.updateDownloadStatus(id) } DownloadManager.ACTION_NOTIFICATION_CLICKED -> { Loading
app/src/main/java/foundation/e/apps/manager/fused/FusedManagerImpl.kt +19 −16 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.download.DownloadManagerBR import foundation.e.apps.manager.download.data.DownloadProgressLD import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.manager.workmanager.InstallWorkManager import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.Type import foundation.e.apps.utils.modules.PWAManagerModule Loading Loading @@ -61,6 +62,8 @@ class FusedManagerImpl @Inject constructor( private val TAG = FusedManagerImpl::class.java.simpleName private val mutex = Mutex() @RequiresApi(Build.VERSION_CODES.O) override fun createNotificationChannels() { notificationManager.apply { Loading Loading @@ -95,7 +98,6 @@ class FusedManagerImpl @Inject constructor( override suspend fun updateDownloadStatus(fusedDownload: FusedDownload, status: Status) { if (status == Status.INSTALLED) { fusedDownload.status = status DownloadManagerBR.downloadedList.clear() flushOldDownload(fusedDownload.packageName) databaseRepository.deleteDownload(fusedDownload) } else if (status == Status.INSTALLING) { Loading @@ -106,8 +108,6 @@ class FusedManagerImpl @Inject constructor( } } private val mutex = Mutex() override suspend fun downloadApp(fusedDownload: FusedDownload) { mutex.withLock { when (fusedDownload.type) { Loading Loading @@ -147,23 +147,26 @@ class FusedManagerImpl @Inject constructor( @OptIn(DelicateCoroutinesApi::class) override suspend fun cancelDownload(fusedDownload: FusedDownload) { mutex.withLock { if (fusedDownload.id.isNotBlank()) { removeFusedDownload(fusedDownload) } else { Timber.d("Unable to cancel download!") } } } private suspend fun removeFusedDownload(fusedDownload: FusedDownload) { fusedDownload.downloadIdMap.forEach { (key, _) -> downloadManager.remove(key) } DownloadProgressLD.setDownloadId(-1) DownloadManagerBR.downloadedList.clear() // Reset the status before deleting download updateDownloadStatus(fusedDownload, fusedDownload.orgStatus) if (fusedDownload.status != Status.INSTALLATION_ISSUE) { databaseRepository.deleteDownload(fusedDownload) } flushOldDownload(fusedDownload.packageName) } else { Timber.d("Unable to cancel download!") } } override suspend fun getFusedDownload(downloadId: Long, packageName: String): FusedDownload { Loading
app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt +10 −2 Original line number Diff line number Diff line Loading @@ -37,11 +37,11 @@ class FusedManagerRepository @Inject constructor( } suspend fun addDownload(fusedDownload: FusedDownload): Boolean { if (InstallWorkManager.checkWorkIsAlreadyAvailable(fusedDownload.id)) { val existingFusedDownload = fusedManagerImpl.getDownloadById(fusedDownload) if (isInstallWorkRunning(existingFusedDownload, fusedDownload)) { return false } 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 false Loading @@ -51,6 +51,14 @@ class FusedManagerRepository @Inject constructor( return true } private fun isInstallWorkRunning( existingFusedDownload: FusedDownload?, fusedDownload: FusedDownload ) = existingFusedDownload != null && InstallWorkManager.checkWorkIsAlreadyAvailable( fusedDownload.id ) suspend fun addFusedDownloadPurchaseNeeded(fusedDownload: FusedDownload) { fusedManagerImpl.insertFusedDownloadPurchaseNeeded(fusedDownload) } Loading
app/src/main/java/foundation/e/apps/manager/workmanager/AppInstallProcessor.kt +39 −60 Original line number Diff line number Diff line Loading @@ -29,20 +29,12 @@ import foundation.e.apps.updates.UpdatesNotifier import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.modules.DataStoreManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.flow.transformWhile import timber.log.Timber import java.text.NumberFormat import java.text.SimpleDateFormat import java.util.Date import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds class AppInstallProcessor @Inject constructor( @ApplicationContext private val context: Context, Loading @@ -51,15 +43,12 @@ class AppInstallProcessor @Inject constructor( private val dataStoreManager: DataStoreManager ) { private var isDownloading: Boolean = false private var isItUpdateWork = false companion object { private const val TAG = "AppInstallProcessor" } private val mutex = Mutex(true) suspend fun processInstall( fusedDownloadId: String, isItUpdateWork: Boolean, Loading Loading @@ -96,7 +85,6 @@ class AppInstallProcessor @Inject constructor( runInForeground?.invoke(it.name) startAppInstallationProcess(it) mutex.lock() } } catch (e: Exception) { Timber.e("doWork: Failed: ${e.stackTraceToString()}") Loading @@ -121,7 +109,8 @@ class AppInstallProcessor @Inject constructor( ) { if (isItUpdateWork) { fusedDownload?.let { val packageStatus = fusedManagerRepository.getFusedDownloadPackageStatus(fusedDownload) val packageStatus = fusedManagerRepository.getFusedDownloadPackageStatus(fusedDownload) if (packageStatus == Status.INSTALLED) { UpdatesDao.addSuccessfullyUpdatedApp(it) Loading Loading @@ -165,33 +154,44 @@ class AppInstallProcessor @Inject constructor( ) } private suspend fun startAppInstallationProcess( fusedDownload: FusedDownload ): Boolean { private suspend fun startAppInstallationProcess(fusedDownload: FusedDownload) { if (fusedDownload.isAwaiting()) { fusedManagerRepository.downloadApp(fusedDownload) Timber.i("===> doWork: Download started ${fusedDownload.name} ${fusedDownload.status}") } isDownloading = true databaseRepository.getDownloadFlowById(fusedDownload.id) .transformWhile { emit(it) isInstallRunning(it) }.collect { latestFusedDownload -> handleFusedDownload(latestFusedDownload, fusedDownload) } } /** * observe app download/install process in a separate thread as DownloadManager download artifacts in a separate process * It checks install status every three seconds * Takes actions depending on the status of [FusedDownload] * * @param latestFusedDownload comes from Room database when [Status] is updated * @param fusedDownload is the original object when install process isn't started. It's used when [latestFusedDownload] * becomes null, After installation is completed. */ tickerFlow(3.seconds) .onEach { val download = databaseRepository.getDownloadById(fusedDownload.id) if (download == null) { private suspend fun handleFusedDownload( latestFusedDownload: FusedDownload?, fusedDownload: FusedDownload ) { if (latestFusedDownload == null) { Timber.d("===> download null: finish installation") finishInstallation(fusedDownload) } else { handleFusedDownloadStatusCheckingException(download) return } }.launchIn(CoroutineScope(Dispatchers.IO)) Timber.d(">>> ===> doWork: Download started " + fusedDownload.name + " " + fusedDownload.status) return true handleFusedDownloadStatusCheckingException(latestFusedDownload) } private fun isInstallRunning(it: FusedDownload?) = it != null && it.status != Status.INSTALLATION_ISSUE private suspend fun handleFusedDownloadStatusCheckingException( download: FusedDownload ) { Loading @@ -204,19 +204,6 @@ class AppInstallProcessor @Inject constructor( } } /** * Triggers a repetitive event according to the delay passed in the parameter * @param period delay of each event * @param initialDelay initial delay to trigger the first event */ private fun tickerFlow(period: Duration, initialDelay: Duration = Duration.ZERO) = flow { delay(initialDelay) while (isDownloading) { emit(Unit) delay(period) } } private suspend fun handleFusedDownloadStatus(fusedDownload: FusedDownload) { when (fusedDownload.status) { Status.AWAITING, Status.DOWNLOADING -> { Loading @@ -243,13 +230,5 @@ class AppInstallProcessor @Inject constructor( private suspend fun finishInstallation(fusedDownload: FusedDownload) { checkUpdateWork(fusedDownload) isDownloading = false unlockMutex() } private fun unlockMutex() { if (mutex.isLocked) { mutex.unlock() } } }