Add update all button for extensions update notifications

This commit is contained in:
Jays2Kings 2021-07-17 01:16:29 -04:00
parent 2ea3142b32
commit ea0d8224b2
12 changed files with 382 additions and 80 deletions

View File

@ -204,6 +204,10 @@
android:name=".data.library.LibraryUpdateService"
android:exported="false" />
<service
android:name=".extension.ExtensionInstallService"
android:exported="false" />
<service
android:name=".data.download.DownloadService"
android:exported="false" />

View File

@ -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
*

View File

@ -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"

View File

@ -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()
)
}
}

View File

@ -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<ExtensionInfo>(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<Extension.Available>): Intent {
return Intent(context, ExtensionInstallService::class.java).apply {
val info = extensions.map(::ExtensionInfo)
putParcelableArrayListExtra(KEY_EXTENSION, ArrayList(info))
}
}
}
}

View File

@ -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<ExtensionIntallInfo> {
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<ExtensionIntallInfo> {
val availableExt = availableExtensions.find { it.pkgName == extension.pkgName }
?: return emptyFlow()
return installExtension(availableExt)
suspend fun installExtension(extension: ExtensionInfo, scope: CoroutineScope): Flow<ExtensionIntallInfo> {
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 {

View File

@ -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<String>) {
private fun createUpdateNotification(extensions: List<Extension.Available>) {
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)
}
)

View File

@ -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<Extension.Installed> {
suspend fun checkForUpdates(context: Context): List<Extension.Available> {
return withContext(Dispatchers.IO) {
val extensions = findExtensions()
@ -38,7 +39,7 @@ internal class ExtensionGithubApi {
.filterIsInstance<LoadResult.Success>()
.map { it.extension }
val extensionsWithUpdate = mutableListOf<Extension.Installed>()
val extensionsWithUpdate = mutableListOf<Extension.Available>()
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}"
}

View File

@ -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

View File

@ -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<String, Long>()
val activeDownloads = hashMapOf<String, Long>()
/**
* 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<Long, Int>()
@ -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<ExtensionIntallInfo> {
suspend fun downloadAndInstall(url: String, extension: ExtensionManager.ExtensionInfo, scope: CoroutineScope): Flow<ExtensionIntallInfo> {
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
}

View File

@ -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<Manga>): List<SourceItem> {
@ -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<ExtensionIntallInfo>.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)
}

View File

@ -313,6 +313,7 @@
<string name="_must_be_enabled_first">%1$s must be enabled first</string>
<string name="could_not_install_extension">Could not install extension</string>
<string name="update_all">Update all</string>
<string name="updating_extensions">Updating extensions</string>
<plurals name="_updates_pending">
<item quantity="one">%d update pending</item>
<item quantity="other">%d updates pending</item>