From ea0d8224b2e8a9ef6c11ad213b392683b24be122 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Sat, 17 Jul 2021 01:16:29 -0400 Subject: [PATCH] Add update all button for extensions update notifications --- app/src/main/AndroidManifest.xml | 4 + .../data/notification/NotificationReceiver.kt | 29 ++++ .../data/notification/Notifications.kt | 1 + .../extension/ExtensionInstallNotifier.kt | 62 ++++++++ .../extension/ExtensionInstallService.kt | 145 ++++++++++++++++++ .../tachiyomi/extension/ExtensionManager.kt | 43 ++++-- .../tachiyomi/extension/ExtensionUpdateJob.kt | 30 +++- .../extension/api/ExtensionGithubApi.kt | 9 +- .../util/ExtensionInstallActivity.kt | 5 + .../extension/util/ExtensionInstaller.kt | 77 ++++++---- .../ui/extension/ExtensionBottomPresenter.kt | 56 ++++--- app/src/main/res/values/strings.xml | 1 + 12 files changed, 382 insertions(+), 80 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallNotifier.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d9e377c7f4..dd419fe399 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -204,6 +204,10 @@ android:name=".data.library.LibraryUpdateService" android:exported="false" /> + + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index a1cb5ff53c..c9ccbd1c30 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.updater.UpdaterService +import eu.kanade.tachiyomi.extension.ExtensionInstallService import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.manga.MangaDetailsController @@ -71,6 +72,7 @@ class NotificationReceiver : BroadcastReceiver() { ) // Cancel library update and dismiss notification ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context) + ACTION_CANCEL_EXTENSION_UPDATE -> cancelExtensionUpdate(context) ACTION_CANCEL_UPDATE_DOWNLOAD -> cancelDownloadUpdate(context) ACTION_CANCEL_RESTORE -> cancelRestoreUpdate(context) // Share backup file @@ -199,6 +201,17 @@ class NotificationReceiver : BroadcastReceiver() { Handler().post { dismissNotification(context, Notifications.ID_LIBRARY_PROGRESS) } } + /** + * Method called when user wants to stop a library update + * + * @param context context of application + * @param notificationId id of notification + */ + private fun cancelExtensionUpdate(context: Context) { + ExtensionInstallService.stop(context) + Handler().post { dismissNotification(context, Notifications.ID_EXTENSION_PROGRESS) } + } + /** * Method called when user wants to mark as read * @@ -251,6 +264,9 @@ class NotificationReceiver : BroadcastReceiver() { // Called to cancel library update. private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE" + // Called to cancel extension update. + private const val ACTION_CANCEL_EXTENSION_UPDATE = "$ID.$NAME.CANCEL_EXTENSION_UPDATE" + private const val ACTION_CANCEL_UPDATE_DOWNLOAD = "$ID.$NAME.CANCEL_UPDATE_DOWNLOAD" // Called to mark as read @@ -568,6 +584,19 @@ class NotificationReceiver : BroadcastReceiver() { return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } + /** + * Returns [PendingIntent] that starts a service which stops the library update + * + * @param context context of application + * @return [PendingIntent] + */ + internal fun cancelExtensionUpdatePendingBroadcast(context: Context): PendingIntent { + val intent = Intent(context, NotificationReceiver::class.java).apply { + action = ACTION_CANCEL_EXTENSION_UPDATE + } + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) + } + /** * Returns [PendingIntent] that cancels the download for a Tachiyomi update * diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt index 2b0fe72714..a9dea6e342 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt @@ -47,6 +47,7 @@ object Notifications { */ const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel" const val ID_UPDATES_TO_EXTS = -401 + const val ID_EXTENSION_PROGRESS = -402 private const val GROUP_BACKUP_RESTORE = "group_backup_restore" const val CHANNEL_BACKUP_RESTORE_PROGRESS = "backup_restore_progress_channel" diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallNotifier.kt new file mode 100644 index 0000000000..cb85b0b8c9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallNotifier.kt @@ -0,0 +1,62 @@ +package eu.kanade.tachiyomi.extension + +import android.content.Context +import android.graphics.BitmapFactory +import androidx.core.content.ContextCompat +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.notification.NotificationReceiver +import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.util.system.notificationBuilder +import eu.kanade.tachiyomi.util.system.notificationManager + +class ExtensionInstallNotifier(private val context: Context) { + + /** + * Bitmap of the app for notifications. + */ + private val notificationBitmap by lazy { + BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher) + } + + /** + * Pending intent of action that cancels the library update + */ + private val cancelIntent by lazy { + NotificationReceiver.cancelExtensionUpdatePendingBroadcast(context) + } + + /** + * Cached progress notification to avoid creating a lot. + */ + val progressNotificationBuilder by lazy { + context.notificationBuilder(Notifications.CHANNEL_UPDATES_TO_EXTS) { + setContentTitle(context.getString(R.string.app_name)) + setSmallIcon(android.R.drawable.stat_sys_download) + setLargeIcon(notificationBitmap) + setContentTitle(context.getString(R.string.updating_extensions)) + setProgress(0, 0, true) + setOngoing(true) + setSilent(true) + setOnlyAlertOnce(true) + color = ContextCompat.getColor(context, R.color.colorAccent) + addAction(R.drawable.ic_close_24dp, context.getString(android.R.string.cancel), cancelIntent) + } + } + + /** + * Shows the notification containing the currently updating manga and the progress. + * + * @param manga the manga that's being updated. + * @param current the current progress. + * @param total the total progress. + */ + fun showProgressNotification() { + context.notificationManager.notify( + Notifications.ID_EXTENSION_PROGRESS, + progressNotificationBuilder + .setContentTitle(context.getString(R.string.updating_extensions)) + .setProgress(0, 0, true) + .build() + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt new file mode 100644 index 0000000000..0c9918ed57 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt @@ -0,0 +1,145 @@ +package eu.kanade.tachiyomi.extension + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.IBinder +import android.os.PowerManager +import eu.kanade.tachiyomi.data.notification.Notifications +import eu.kanade.tachiyomi.extension.ExtensionManager.ExtensionInfo +import eu.kanade.tachiyomi.extension.model.Extension +import eu.kanade.tachiyomi.util.system.notificationManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.ArrayList +import java.util.concurrent.TimeUnit + +class ExtensionInstallService( + val extensionManager: ExtensionManager = Injekt.get(), +) : Service() { + + /** + * Wake lock that will be held until the service is destroyed. + */ + private lateinit var wakeLock: PowerManager.WakeLock + + private lateinit var notifier: ExtensionInstallNotifier + + private var job: Job? = null + + private var serviceScope = CoroutineScope(Job() + Dispatchers.Default) + + private val requestSemaphore = Semaphore(3) + + /** + * This method needs to be implemented, but it's not used/needed. + */ + override fun onBind(intent: Intent): IBinder? { + return null + } + + /** + * Method called when the service receives an intent. + * + * @param intent the start intent from. + * @param flags the flags of the command. + * @param startId the start id of this command. + * @return the start value of the command. + */ + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent == null) return START_NOT_STICKY + + instance = this + + val list = intent.getParcelableArrayListExtra(KEY_EXTENSION) + ?: return START_NOT_STICKY + job = serviceScope.launch { + val results = list.map { + async { + installExtension(it) + } + } + results.awaitAll() + } + job?.invokeOnCompletion { stopSelf(startId) } + + return START_REDELIVER_INTENT + } + + suspend fun installExtension(extension: ExtensionInfo) { + requestSemaphore.withPermit { + extensionManager.installExtension(extension, serviceScope) + .collect { + notifier.showProgressNotification() + } + } + } + + /** + * Method called when the service is created. It injects dagger dependencies and acquire + * the wake lock. + */ + override fun onCreate() { + super.onCreate() + notificationManager.cancel(Notifications.ID_UPDATES_TO_EXTS) + notifier = ExtensionInstallNotifier(this) + wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, + "ExtensionInstallService:WakeLock" + ) + wakeLock.acquire(TimeUnit.MINUTES.toMillis(30)) + startForeground(Notifications.ID_EXTENSION_PROGRESS, notifier.progressNotificationBuilder.build()) + } + + /** + * Method called when the service is destroyed. It cancels jobs and releases the wake lock. + */ + override fun onDestroy() { + job?.cancel() + serviceScope.cancel() + if (instance == this) { + instance = null + } + if (wakeLock.isHeld) { + wakeLock.release() + } + super.onDestroy() + } + + companion object { + + private var instance: ExtensionInstallService? = null + + /** + * Stops the service. + * + * @param context the application context. + */ + fun stop(context: Context) { + instance?.serviceScope?.cancel() + context.stopService(Intent(context, ExtensionUpdateJob::class.java)) + } + + /** + * Key that defines what should be updated. + */ + private const val KEY_EXTENSION = "extension" + + fun jobIntent(context: Context, extensions: List): Intent { + return Intent(context, ExtensionInstallService::class.java).apply { + val info = extensions.map(::ExtensionInfo) + putParcelableArrayListExtra(KEY_EXTENSION, ArrayList(info)) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index 8ab5d70421..b9d3ff7dc5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension import android.content.Context import android.graphics.drawable.Drawable +import android.os.Parcelable import com.jakewharton.rxrelay.BehaviorRelay import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault @@ -15,11 +16,12 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo import eu.kanade.tachiyomi.util.system.launchNow +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize import rx.Observable import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -49,6 +51,15 @@ class ExtensionManager( */ private val installer by lazy { ExtensionInstaller(context) } + val downloadRelay + get() = installer.downloadsStateFlow + + fun getExtension(downloadId: Long): String? { + return installer.activeDownloads.entries.find { downloadId == it.value }?.key + } + + fun getActiveInstalls(): Int = installer.activeDownloads.size + /** * Relay used to notify the installed extensions. */ @@ -240,21 +251,8 @@ class ExtensionManager( * * @param extension The extension to be installed. */ - fun installExtension(extension: Extension.Available): Flow { - return installer.downloadAndInstall(api.getApkUrl(extension), extension) - } - - /** - * Returns a flow of the installation process for the given extension. It will complete - * once the extension is updated or throws an error. The process will be canceled the scope - * is canceled before its completion. - * - * @param extension The extension to be updated. - */ - fun updateExtension(extension: Extension.Installed): Flow { - val availableExt = availableExtensions.find { it.pkgName == extension.pkgName } - ?: return emptyFlow() - return installExtension(availableExt) + suspend fun installExtension(extension: ExtensionInfo, scope: CoroutineScope): Flow { + return installer.downloadAndInstall(api.getApkUrl(extension), extension, scope) } /** @@ -407,6 +405,19 @@ class ExtensionManager( } return this } + + @Parcelize + data class ExtensionInfo( + val apkName: String, + val pkgName: String, + val name: String, + ) : Parcelable { + constructor(extension: Extension.Available) : this( + apkName = extension.apkName, + pkgName = extension.pkgName, + name = extension.name + ) + } } interface ExtensionsChangedListener { diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt index ce9dbe322a..7f546e0db2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.extension +import android.app.PendingIntent import android.content.Context +import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat @@ -17,6 +19,7 @@ import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi +import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.util.system.notification import kotlinx.coroutines.coroutineScope import uy.kohesive.injekt.Injekt @@ -35,15 +38,20 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam } if (pendingUpdates.isNotEmpty()) { - createUpdateNotification(pendingUpdates.map { it.name }) + createUpdateNotification(pendingUpdates) } Result.success() } - private fun createUpdateNotification(names: List) { + private fun createUpdateNotification(extensions: List) { val preferences: PreferencesHelper by injectLazy() - preferences.extensionUpdatesCount().set(names.size) + preferences.extensionUpdatesCount().set(extensions.size) +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && preferences.autoUpdateExtensions()) { +// val intent = ExtensionInstallService.jobIntent(context, extensions) +// context.startForegroundService(intent) +// return +// } NotificationManagerCompat.from(context).apply { notify( Notifications.ID_UPDATES_TO_EXTS, @@ -51,11 +59,11 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam setContentTitle( context.resources.getQuantityString( R.plurals.extension_updates_available, - names.size, - names.size + extensions.size, + extensions.size ) ) - val extNames = names.joinToString(", ") + val extNames = extensions.joinToString(", ") { it.name } setContentText(extNames) setStyle(NotificationCompat.BigTextStyle().bigText(extNames)) setSmallIcon(R.drawable.ic_extension_update_24dp) @@ -65,6 +73,16 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam context ) ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val intent = ExtensionInstallService.jobIntent(context, extensions) + val pendingIntent = + PendingIntent.getForegroundService(context, 0, intent, 0) + addAction( + R.drawable.ic_file_download_24dp, + context.getString(R.string.update_all), + pendingIntent + ) + } setAutoCancel(true) } ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt index a2ff814ce2..c5caeac509 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.extension.api import android.content.Context +import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.util.ExtensionLoader @@ -30,7 +31,7 @@ internal class ExtensionGithubApi { } } - suspend fun checkForUpdates(context: Context): List { + suspend fun checkForUpdates(context: Context): List { return withContext(Dispatchers.IO) { val extensions = findExtensions() @@ -38,7 +39,7 @@ internal class ExtensionGithubApi { .filterIsInstance() .map { it.extension } - val extensionsWithUpdate = mutableListOf() + val extensionsWithUpdate = mutableListOf() val mutInstalledExtensions = installedExtensions.toMutableList() for (installedExt in mutInstalledExtensions) { val pkgName = installedExt.pkgName @@ -46,7 +47,7 @@ internal class ExtensionGithubApi { val hasUpdate = availableExt.versionCode > installedExt.versionCode if (hasUpdate) { - extensionsWithUpdate.add(installedExt) + extensionsWithUpdate.add(availableExt) } } @@ -75,7 +76,7 @@ internal class ExtensionGithubApi { } } - fun getApkUrl(extension: Extension.Available): String { + fun getApkUrl(extension: ExtensionManager.ExtensionInfo): String { return "${REPO_URL_PREFIX}apk/${extension.apkName}" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt index a8d6622b2a..8d081dd37c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt @@ -1,7 +1,9 @@ package eu.kanade.tachiyomi.extension.util import android.app.Activity +import android.app.DownloadManager import android.app.PendingIntent +import android.content.Context import android.content.Intent import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller.SessionParams @@ -56,6 +58,9 @@ class ExtensionInstallActivity : Activity() { session.commit(statusReceiver) val extensionManager: ExtensionManager by injectLazy() extensionManager.setInstalling(downloadId, sessionId) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + (getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager).remove(downloadId) + } data.close() } catch (error: Exception) { // Either install package can't be found (probably bots) or there's a security exception diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt index 2c9e8722f8..cc8966b858 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt @@ -5,14 +5,14 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.content.pm.PackageInstaller import android.net.Uri import android.os.Environment import androidx.core.net.toUri -import eu.kanade.tachiyomi.extension.model.Extension +import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo import eu.kanade.tachiyomi.util.storage.getUriCompat +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -29,6 +29,8 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.transformWhile +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import timber.log.Timber import java.io.File @@ -54,12 +56,12 @@ internal class ExtensionInstaller(private val context: Context) { * The currently requested downloads, with the package name (unique id) as key, and the id * returned by the download manager. */ - private val activeDownloads = hashMapOf() + val activeDownloads = hashMapOf() /** * StateFlow used to notify the installation step of every download. */ - private val downloadsStateFlow = MutableStateFlow(0L to InstallStep.Pending) + val downloadsStateFlow = MutableStateFlow(0L to ExtensionIntallInfo(InstallStep.Pending, null)) /** Map of download id to installer session id */ val downloadInstallerMap = hashMapOf() @@ -71,7 +73,7 @@ internal class ExtensionInstaller(private val context: Context) { * @param url The url of the apk. * @param extension The extension to install. */ - fun downloadAndInstall(url: String, extension: Extension): Flow { + suspend fun downloadAndInstall(url: String, extension: ExtensionManager.ExtensionInfo, scope: CoroutineScope): Flow { val pkgName = extension.pkgName val oldDownload = activeDownloads[pkgName] @@ -96,33 +98,39 @@ internal class ExtensionInstaller(private val context: Context) { val id = downloadManager.enqueue(request) activeDownloads[pkgName] = id - return flowOf( - pollStatus(id), - pollInstallStatus(id), - downloadsStateFlow.filter { it.first == id } - .map { - it.second to findSession(it.first) + scope.launch { + flowOf( + pollStatus(id), + pollInstallStatus(id) + ).flattenMerge() + .transformWhile { + emit(it) + !it.first.isCompleted() } - ).flattenMerge() + .flowOn(Dispatchers.IO) + .catch { e -> + Timber.e(e) + emit(InstallStep.Error to null) + } + .onCompletion { + deleteDownload(pkgName) + } + .collect { + downloadsStateFlow.emit(id to it) + } + } + + return downloadsStateFlow.filter { it.first == id }.map { it.second } + .flowOn(Dispatchers.IO) .transformWhile { emit(it) !it.first.isCompleted() } - .flowOn(Dispatchers.IO) - .catch { e -> - Timber.e(e) - emit(InstallStep.Error to null) - } .onCompletion { deleteDownload(pkgName) } } - private fun findSession(downloadId: Long): PackageInstaller.SessionInfo? { - val sessionId = downloadInstallerMap[downloadId] ?: return null - return context.packageManager.packageInstaller.getSessionInfo(sessionId) - } - /** * Returns a flow that polls the given download id for its status every second, as the * manager doesn't have any notification system. It'll stop once the download finishes. @@ -134,11 +142,16 @@ internal class ExtensionInstaller(private val context: Context) { return flow { while (true) { - val newDownloadState = downloadManager.query(query).use { cursor -> - cursor.moveToFirst() - cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) + val newDownloadState = try { + downloadManager.query(query)?.use { cursor -> + cursor.moveToFirst() + cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) + } + } catch (_: Exception) { + } + if (newDownloadState != null) { + emit(newDownloadState) } - emit(newDownloadState) delay(1000) } } @@ -213,7 +226,7 @@ internal class ExtensionInstaller(private val context: Context) { * @param downloadId The id of the download. */ fun setInstalling(downloadId: Long, sessionId: Int) { - downloadsStateFlow.tryEmit(downloadId to InstallStep.Installing) + downloadsStateFlow.tryEmit(downloadId to ExtensionIntallInfo(InstallStep.Installing, null)) downloadInstallerMap[downloadId] = sessionId } @@ -232,7 +245,11 @@ internal class ExtensionInstaller(private val context: Context) { fun setInstallationResult(downloadId: Long, result: Boolean) { val step = if (result) InstallStep.Installed else InstallStep.Error downloadInstallerMap.remove(downloadId) - downloadsStateFlow.tryEmit(downloadId to step) + downloadsStateFlow.tryEmit(downloadId to ExtensionIntallInfo(step, null)) + } + + fun softDeleteDownload(downloadId: Long) { + downloadManager.remove(downloadId) } /** @@ -295,10 +312,10 @@ internal class ExtensionInstaller(private val context: Context) { // Set next installation step if (uri != null) { - downloadsStateFlow.tryEmit(id to InstallStep.Loading) + downloadsStateFlow.tryEmit(id to ExtensionIntallInfo(InstallStep.Loading, null)) } else { Timber.e("Couldn't locate downloaded APK") - downloadsStateFlow.tryEmit(id to InstallStep.Error) + downloadsStateFlow.tryEmit(id to ExtensionIntallInfo(InstallStep.Error, null)) return } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt index f3c8377d31..777d89602d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt @@ -23,11 +23,9 @@ import eu.kanade.tachiyomi.util.system.withUIContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import uy.kohesive.injekt.Injekt @@ -81,7 +79,10 @@ class ExtensionBottomPresenter( sourceItems = findSourcesWithManga(favs) mangaItems = HashMap( sourceItems.associate { - it.source.id to this@ExtensionBottomPresenter.libraryToMigrationItem(favs, it.source.id) + it.source.id to this@ExtensionBottomPresenter.libraryToMigrationItem( + favs, + it.source.id + ) } ) withContext(Dispatchers.Main) { @@ -94,6 +95,27 @@ class ExtensionBottomPresenter( } listOf(migrationJob, extensionJob).awaitAll() } + presenterScope.launch { + extensionManager.downloadRelay + .collect { + val extPageName = extensionManager.getExtension(it.first) + val extension = extensions.find { item -> + extPageName == item.extension.pkgName + } ?: return@collect + when (it.second.first) { + InstallStep.Installed, InstallStep.Error -> { + currentDownloads.remove(extension.extension.pkgName) + } + else -> { + currentDownloads[extension.extension.pkgName] = it.second + } + } + val item = updateInstallStep(extension.extension, it.second.first, it.second.second) + if (item != null) { + withUIContext { bottomSheet.downloadUpdate(item) } + } + } + } } private fun findSourcesWithManga(library: List): List { @@ -249,15 +271,19 @@ class ExtensionBottomPresenter( fun installExtension(extension: Extension.Available) { if (isNotMIUIOptimized()) { presenterScope.launch { - extensionManager.installExtension(extension).collectForInstallUpdate(extension) + extensionManager.installExtension(ExtensionManager.ExtensionInfo(extension), presenterScope) + .launchIn(this) } } } fun updateExtension(extension: Extension.Installed) { if (isNotMIUIOptimized()) { + val availableExt = + extensionManager.availableExtensions.find { it.pkgName == extension.pkgName } ?: return presenterScope.launch { - extensionManager.updateExtension(extension).collectForInstallUpdate(extension) + extensionManager.installExtension(ExtensionManager.ExtensionInfo(availableExt), presenterScope) + .launchIn(this) } } } @@ -270,24 +296,6 @@ class ExtensionBottomPresenter( return true } - private suspend fun Flow.collectForInstallUpdate(extension: Extension) { - this - .onEach { currentDownloads[extension.pkgName] = it } - .onCompletion { - currentDownloads.remove(extension.pkgName) - val item = updateInstallStep(extension, null, null) - if (item != null) { - withUIContext { bottomSheet.downloadUpdate(item) } - } - } - .collect { state -> - val item = updateInstallStep(extension, state.first, state.second) - if (item != null) { - withUIContext { bottomSheet.downloadUpdate(item) } - } - } - } - fun uninstallExtension(pkgName: String) { extensionManager.uninstallExtension(pkgName) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 28396c1835..fcc9f427c9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -313,6 +313,7 @@ %1$s must be enabled first Could not install extension Update all + Updating extensions %d update pending %d updates pending