From 58e96ad7d9f9a6e7b70a3b0ec7e75f65e257413c Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Wed, 20 Apr 2022 20:07:00 +0600 Subject: [PATCH] App Lounge: run install worker as foreground handle exception during installation --- app/src/main/AndroidManifest.xml | 1 + .../foundation/e/apps/AppLoungeApplication.kt | 2 + .../java/foundation/e/apps/MainActivity.kt | 3 +- .../ApplicationListViewModel.kt | 2 - .../e/apps/manager/fused/FusedManagerImpl.kt | 24 ++++-- .../e/apps/manager/pkg/InstallerService.kt | 85 +++++++++++++++++++ .../e/apps/manager/pkg/PkgManagerModule.kt | 19 +++-- .../manager/workmanager/InstallAppWorker.kt | 82 +++++++++++++----- .../manager/workmanager/InstallWorkManager.kt | 9 +- .../e/apps/utils/modules/PWAManagerModule.kt | 2 +- 10 files changed, 189 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/manager/pkg/InstallerService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5fef1623c..c9eae0e24 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -50,6 +50,7 @@ + downloadNativeApp(fusedDownload) - Type.PWA -> pwaManagerModule.installPWAApp(fusedDownload) + mutex.withLock { + when (fusedDownload.type) { + Type.NATIVE -> downloadNativeApp(fusedDownload) + Type.PWA -> pwaManagerModule.installPWAApp(fusedDownload) + } } } @@ -101,9 +109,13 @@ class FusedManagerImpl @Inject constructor( parentPathFile.listFiles()?.let { list.addAll(it) } list.sort() if (list.size != 0) { - Log.d(TAG, "installApp: STARTED ${fusedDownload.name} ${list.size}") - pkgManagerModule.installApplication(list, fusedDownload.packageName) - Log.d(TAG, "installApp: ENDED ${fusedDownload.name} ${list.size}") + try { + Log.d(TAG, "installApp: STARTED ${fusedDownload.name} ${list.size}") + pkgManagerModule.installApplication(list, fusedDownload.packageName) + Log.d(TAG, "installApp: ENDED ${fusedDownload.name} ${list.size}") + } catch (e: Exception) { + installationIssue(fusedDownload) + } } } else -> { diff --git a/app/src/main/java/foundation/e/apps/manager/pkg/InstallerService.kt b/app/src/main/java/foundation/e/apps/manager/pkg/InstallerService.kt new file mode 100644 index 000000000..236dea7d2 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/manager/pkg/InstallerService.kt @@ -0,0 +1,85 @@ +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2021 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 . + */ + +package foundation.e.apps.manager.pkg + +import android.app.Service +import android.content.Intent +import android.content.pm.PackageInstaller +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.annotation.RequiresApi +import dagger.hilt.android.AndroidEntryPoint +import foundation.e.apps.manager.fused.FusedManagerRepository +import foundation.e.apps.utils.enums.Status +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +@AndroidEntryPoint +class InstallerService : Service() { + + @Inject + lateinit var fusedManagerRepository: FusedManagerRepository + + @Inject + lateinit var pkgManagerModule: PkgManagerModule + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -69) + val packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) + val extra = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + postStatus(status, packageName, extra) + stopSelf() + return START_NOT_STICKY + } + + private fun postStatus(status: Int, packageName: String?, extra: String?) { + Log.d("InstallerService", "postStatus: $status $packageName $extra") + if (status != PackageInstaller.STATUS_SUCCESS) { + updateInstallationIssue(packageName ?: "") + } + } + + override fun onBind(intent: Intent): IBinder? { + return null + } + + private fun updateDownloadStatus(pkgName: String) { + if (pkgName.isEmpty()) { + Log.d("PkgManagerBR", "updateDownloadStatus: package name should not be empty!") + } + GlobalScope.launch { + val fusedDownload = fusedManagerRepository.getFusedDownload(packageName = pkgName) + pkgManagerModule.setFakeStoreAsInstallerIfNeeded(fusedDownload) + fusedManagerRepository.updateDownloadStatus(fusedDownload, Status.INSTALLED) + } + } + + private fun updateInstallationIssue(pkgName: String) { + if (pkgName.isEmpty()) { + Log.d("PkgManagerBR", "updateDownloadStatus: package name should not be empty!") + } + GlobalScope.launch { + val fusedDownload = fusedManagerRepository.getFusedDownload(packageName = pkgName) + fusedManagerRepository.installationIssue(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 4ed8fff90..716a935d5 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 @@ -148,15 +148,18 @@ class PkgManagerModule @Inject constructor( inputStream.close() outputStream.close() } - val pendingIntent = PendingIntent.getBroadcast( + + val callBackIntent = Intent(context, InstallerService::class.java) + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE else + PendingIntent.FLAG_UPDATE_CURRENT + val servicePendingIntent = PendingIntent.getService( context, sessionId, - Intent(Intent.ACTION_PACKAGE_ADDED), - PendingIntent.FLAG_IMMUTABLE + callBackIntent, + flags ) - - session.commit(pendingIntent.intentSender) - session.close() + session.commit(servicePendingIntent.intentSender) } catch (e: Exception) { Log.e(TAG, "$packageName: \n${e.stackTraceToString()}") val pendingIntent = PendingIntent.getBroadcast( @@ -165,8 +168,10 @@ class PkgManagerModule @Inject constructor( Intent(ERROR_PACKAGE_INSTALL), PendingIntent.FLAG_IMMUTABLE ) - session.commit(pendingIntent.intentSender) + session.abandon() + throw e + } finally { session.close() } } diff --git a/app/src/main/java/foundation/e/apps/manager/workmanager/InstallAppWorker.kt b/app/src/main/java/foundation/e/apps/manager/workmanager/InstallAppWorker.kt index 196950bea..73438981e 100644 --- a/app/src/main/java/foundation/e/apps/manager/workmanager/InstallAppWorker.kt +++ b/app/src/main/java/foundation/e/apps/manager/workmanager/InstallAppWorker.kt @@ -1,17 +1,25 @@ package foundation.e.apps.manager.workmanager import android.app.DownloadManager +import android.app.NotificationChannel +import android.app.NotificationManager import android.content.Context +import android.os.Build import android.util.Log +import androidx.core.app.NotificationCompat import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker +import androidx.work.ForegroundInfo +import androidx.work.WorkManager import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import foundation.e.apps.R import foundation.e.apps.manager.database.DatabaseRepository import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.fused.FusedManagerRepository import foundation.e.apps.utils.enums.Status +import foundation.e.apps.utils.enums.Type import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -19,6 +27,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.takeWhile +import java.util.concurrent.atomic.AtomicInteger import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -39,20 +48,26 @@ class InstallAppWorker @AssistedInject constructor( const val INPUT_DATA_FUSED_DOWNLOAD = "input_data_fused_download" } + private val atomicInteger = AtomicInteger() + override suspend fun doWork(): Result { var fusedDownload: FusedDownload? = null try { val fusedDownloadString = params.inputData.getString(INPUT_DATA_FUSED_DOWNLOAD) ?: "" Log.d(TAG, "Fused download name $fusedDownloadString") - fusedDownload = databaseRepository.getDownloadById(fusedDownloadString) fusedDownload?.let { if (fusedDownload.status != Status.AWAITING) { return@let } + setForeground( + createForegroundInfo( + "Installing ${it.name}" + ) + ) startAppInstallationProcess(it) } - + Log.d(TAG, "doWork: RESULT SUCCESS: ${fusedDownload?.name}") return Result.success() } catch (e: Exception) { Log.e(TAG, "doWork: Failed: ${e.stackTraceToString()}") @@ -68,14 +83,14 @@ class InstallAppWorker @AssistedInject constructor( ) { fusedManagerRepository.downloadApp(fusedDownload) Log.d(TAG, "===> doWork: Download started ${fusedDownload.name} ${fusedDownload.status}") - isDownloading = true - - tickerFlow(3.seconds) - .onEach { - checkDownloadProcess(fusedDownload) - }.launchIn(CoroutineScope(Dispatchers.Default)) - - observeDownload(fusedDownload) + if (fusedDownload.type == Type.NATIVE) { + isDownloading = true + tickerFlow(1.seconds) + .onEach { + checkDownloadProcess(fusedDownload) + }.launchIn(CoroutineScope(Dispatchers.IO)) + observeDownload(fusedDownload) + } } private fun tickerFlow(period: Duration, initialDelay: Duration = Duration.ZERO) = flow { @@ -99,16 +114,12 @@ class InstallAppWorker @AssistedInject constructor( cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)) val bytesDownloadedSoFar = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)) - Log.d( TAG, - "checkDownloadProcess: ${fusedDownload.name} $id $status $totalSizeBytes $bytesDownloadedSoFar" + "checkDownloadProcess: ${fusedDownload.name} $bytesDownloadedSoFar/$totalSizeBytes $status" ) - if (status == DownloadManager.STATUS_SUCCESSFUL || status == DownloadManager.STATUS_FAILED) { - isDownloading = false - if (status == DownloadManager.STATUS_FAILED) { - fusedManagerRepository.installationIssue(fusedDownload) - } + if (status == DownloadManager.STATUS_FAILED) { + fusedManagerRepository.installationIssue(fusedDownload) } } } @@ -133,14 +144,13 @@ class InstallAppWorker @AssistedInject constructor( private fun handleFusedDownloadStatus(fusedDownload: FusedDownload) { when (fusedDownload.status) { - Status.DOWNLOADING -> { + Status.AWAITING, Status.DOWNLOADING -> { } Status.INSTALLING -> { Log.d( TAG, "===> doWork: Installing ${fusedDownload.name} ${fusedDownload.status}" ) - isDownloading = false } Status.INSTALLED, Status.INSTALLATION_ISSUE -> { isDownloading = false @@ -158,4 +168,38 @@ class InstallAppWorker @AssistedInject constructor( } } } + + private fun createForegroundInfo(progress: String): ForegroundInfo { + val title = applicationContext.getString(R.string.app_name) + val cancel = applicationContext.getString(R.string.cancel) + // This PendingIntent can be used to cancel the worker + val intent = WorkManager.getInstance(applicationContext) + .createCancelPendingIntent(getId()) + + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as + NotificationManager + // Create a Notification channel if necessary + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val mChannel = NotificationChannel( + "applounge_notification", + title, + NotificationManager.IMPORTANCE_DEFAULT + ) + notificationManager.createNotificationChannel(mChannel) + } + + val notification = NotificationCompat.Builder(applicationContext, "applounge_notification") + .setContentTitle(title) + .setTicker(title) + .setContentText(progress) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setOngoing(true) + // Add the cancel action to the notification which can + // be used to cancel the worker + .addAction(android.R.drawable.ic_delete, cancel, intent) + .build() + + return ForegroundInfo(atomicInteger.getAndIncrement(), notification) + } } 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 ffdd9e719..b94135fbd 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 @@ -3,21 +3,22 @@ package foundation.e.apps.manager.workmanager import android.content.Context import androidx.work.Data import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest +import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import foundation.e.apps.manager.database.fusedDownload.FusedDownload object InstallWorkManager { - private const val INSTALL_WORK_NAME = "APP_LOUNGE_INSTALL_APP" + const val INSTALL_WORK_NAME = "APP_LOUNGE_INSTALL_APP" fun enqueueWork(context: Context, fusedDownload: FusedDownload) { WorkManager.getInstance(context).enqueueUniqueWork( INSTALL_WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, - OneTimeWorkRequest.Builder(InstallAppWorker::class.java).setInputData( + OneTimeWorkRequestBuilder().setInputData( Data.Builder() .putString(InstallAppWorker.INPUT_DATA_FUSED_DOWNLOAD, fusedDownload.id) .build() - ).build() + ).addTag(fusedDownload.name) + .build() ) } } diff --git a/app/src/main/java/foundation/e/apps/utils/modules/PWAManagerModule.kt b/app/src/main/java/foundation/e/apps/utils/modules/PWAManagerModule.kt index a9fcd8239..1ea385d85 100644 --- a/app/src/main/java/foundation/e/apps/utils/modules/PWAManagerModule.kt +++ b/app/src/main/java/foundation/e/apps/utils/modules/PWAManagerModule.kt @@ -142,7 +142,7 @@ class PWAManagerModule @Inject constructor( // Update status fusedDownload.status = Status.INSTALLED databaseRepository.updateDownload(fusedDownload) - + delay(500) databaseRepository.deleteDownload(fusedDownload) } } -- GitLab