mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Replace RxJava in extension installer (#9556)
* Replace RxJava in extension installer Replace common downloadsRelay with Map of individual StateFlows * Drop RxRelay dependency * Simplify updateAllExtensions * Simplify addDownloadState/removeDownloadState Use immutable Map functions instead of converting to MutableMap
This commit is contained in:
		| @@ -14,10 +14,11 @@ import eu.kanade.tachiyomi.extension.util.ExtensionLoader | ||||
| import eu.kanade.tachiyomi.util.preference.plusAssign | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import kotlinx.coroutines.async | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.MutableStateFlow | ||||
| import kotlinx.coroutines.flow.asStateFlow | ||||
| import kotlinx.coroutines.flow.emptyFlow | ||||
| import logcat.LogPriority | ||||
| import rx.Observable | ||||
| import tachiyomi.core.util.lang.launchNow | ||||
| import tachiyomi.core.util.lang.withUIContext | ||||
| import tachiyomi.core.util.system.logcat | ||||
| @@ -200,7 +201,7 @@ class ExtensionManager( | ||||
|      * | ||||
|      * @param extension The extension to be installed. | ||||
|      */ | ||||
|     fun installExtension(extension: Extension.Available): Observable<InstallStep> { | ||||
|     fun installExtension(extension: Extension.Available): Flow<InstallStep> { | ||||
|         return installer.downloadAndInstall(api.getApkUrl(extension), extension) | ||||
|     } | ||||
|  | ||||
| @@ -211,9 +212,9 @@ class ExtensionManager( | ||||
|      * | ||||
|      * @param extension The extension to be updated. | ||||
|      */ | ||||
|     fun updateExtension(extension: Extension.Installed): Observable<InstallStep> { | ||||
|     fun updateExtension(extension: Extension.Installed): Flow<InstallStep> { | ||||
|         val availableExt = _availableExtensionsFlow.value.find { it.pkgName == extension.pkgName } | ||||
|             ?: return Observable.empty() | ||||
|             ?: return emptyFlow() | ||||
|         return installExtension(availableExt) | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -10,20 +10,27 @@ import android.os.Environment | ||||
| import androidx.core.content.ContextCompat | ||||
| import androidx.core.content.getSystemService | ||||
| import androidx.core.net.toUri | ||||
| import com.jakewharton.rxrelay.PublishRelay | ||||
| import eu.kanade.domain.base.BasePreferences | ||||
| import eu.kanade.tachiyomi.extension.installer.Installer | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
| import eu.kanade.tachiyomi.extension.model.InstallStep | ||||
| import eu.kanade.tachiyomi.util.storage.getUriCompat | ||||
| import kotlinx.coroutines.delay | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.MutableStateFlow | ||||
| import kotlinx.coroutines.flow.distinctUntilChanged | ||||
| import kotlinx.coroutines.flow.flow | ||||
| import kotlinx.coroutines.flow.mapNotNull | ||||
| import kotlinx.coroutines.flow.merge | ||||
| import kotlinx.coroutines.flow.onCompletion | ||||
| import kotlinx.coroutines.flow.transformWhile | ||||
| import logcat.LogPriority | ||||
| import rx.Observable | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| import tachiyomi.core.util.lang.withUIContext | ||||
| import tachiyomi.core.util.system.logcat | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.io.File | ||||
| import java.util.concurrent.TimeUnit | ||||
| import kotlin.time.Duration.Companion.seconds | ||||
|  | ||||
| /** | ||||
|  * The installer which installs, updates and uninstalls the extensions. | ||||
| @@ -48,10 +55,7 @@ internal class ExtensionInstaller(private val context: Context) { | ||||
|      */ | ||||
|     private val activeDownloads = hashMapOf<String, Long>() | ||||
|  | ||||
|     /** | ||||
|      * Relay used to notify the installation step of every download. | ||||
|      */ | ||||
|     private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>() | ||||
|     private val downloadsStateFlows = hashMapOf<Long, MutableStateFlow<InstallStep>>() | ||||
|  | ||||
|     private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller() | ||||
|  | ||||
| @@ -62,7 +66,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) = Observable.defer { | ||||
|     fun downloadAndInstall(url: String, extension: Extension): Flow<InstallStep> { | ||||
|         val pkgName = extension.pkgName | ||||
|  | ||||
|         val oldDownload = activeDownloads[pkgName] | ||||
| @@ -83,48 +87,59 @@ internal class ExtensionInstaller(private val context: Context) { | ||||
|         val id = downloadManager.enqueue(request) | ||||
|         activeDownloads[pkgName] = id | ||||
|  | ||||
|         downloadsRelay.filter { it.first == id } | ||||
|             .map { it.second } | ||||
|             // Poll download status | ||||
|             .mergeWith(pollStatus(id)) | ||||
|         val downloadStateFlow = MutableStateFlow(InstallStep.Pending) | ||||
|         downloadsStateFlows[id] = downloadStateFlow | ||||
|  | ||||
|         // Poll download status | ||||
|         val pollStatusFlow = downloadStatusFlow(id).mapNotNull { downloadStatus -> | ||||
|             // Map to our model | ||||
|             when (downloadStatus) { | ||||
|                 DownloadManager.STATUS_PENDING -> InstallStep.Pending | ||||
|                 DownloadManager.STATUS_RUNNING -> InstallStep.Downloading | ||||
|                 else -> null | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return merge(downloadStateFlow, pollStatusFlow).transformWhile { | ||||
|             emit(it) | ||||
|             // Stop when the application is installed or errors | ||||
|             .takeUntil { it.isCompleted() } | ||||
|             !it.isCompleted() | ||||
|         }.onCompletion { | ||||
|             // Always notify on main thread | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             // Always remove the download when unsubscribed | ||||
|             .doOnUnsubscribe { deleteDownload(pkgName) } | ||||
|             withUIContext { | ||||
|                 // Always remove the download when unsubscribed | ||||
|                 deleteDownload(pkgName) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns an observable that polls the given download id for its status every second, as the | ||||
|      * 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. | ||||
|      * | ||||
|      * @param id The id of the download to poll. | ||||
|      */ | ||||
|     private fun pollStatus(id: Long): Observable<InstallStep> { | ||||
|     private fun downloadStatusFlow(id: Long): Flow<Int> = flow { | ||||
|         val query = DownloadManager.Query().setFilterById(id) | ||||
|  | ||||
|         return Observable.interval(0, 1, TimeUnit.SECONDS) | ||||
|         while (true) { | ||||
|             // Get the current download status | ||||
|             .map { | ||||
|                 downloadManager.query(query).use { cursor -> | ||||
|                     cursor.moveToFirst() | ||||
|                     cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) | ||||
|                 } | ||||
|             val downloadStatus = downloadManager.query(query).use { cursor -> | ||||
|                 if (!cursor.moveToFirst()) return@flow | ||||
|                 cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) | ||||
|             } | ||||
|             // Ignore duplicate results | ||||
|             .distinctUntilChanged() | ||||
|  | ||||
|             emit(downloadStatus) | ||||
|  | ||||
|             // Stop polling when the download fails or finishes | ||||
|             .takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED } | ||||
|             // Map to our model | ||||
|             .flatMap { status -> | ||||
|                 when (status) { | ||||
|                     DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending) | ||||
|                     DownloadManager.STATUS_RUNNING -> Observable.just(InstallStep.Downloading) | ||||
|                     else -> Observable.empty() | ||||
|                 } | ||||
|             if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) { | ||||
|                 return@flow | ||||
|             } | ||||
|  | ||||
|             delay(1.seconds) | ||||
|         } | ||||
|     } | ||||
|         // Ignore duplicate results | ||||
|         .distinctUntilChanged() | ||||
|  | ||||
|     /** | ||||
|      * Starts an intent to install the extension at the given uri. | ||||
| @@ -176,7 +191,7 @@ internal class ExtensionInstaller(private val context: Context) { | ||||
|      * @param step New install step. | ||||
|      */ | ||||
|     fun updateInstallStep(downloadId: Long, step: InstallStep) { | ||||
|         downloadsRelay.call(downloadId to step) | ||||
|         downloadsStateFlows[downloadId]?.let { it.value = step } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -188,6 +203,7 @@ internal class ExtensionInstaller(private val context: Context) { | ||||
|         val downloadId = activeDownloads.remove(pkgName) | ||||
|         if (downloadId != null) { | ||||
|             downloadManager.remove(downloadId) | ||||
|             downloadsStateFlows.remove(downloadId) | ||||
|         } | ||||
|         if (activeDownloads.isEmpty()) { | ||||
|             downloadReceiver.unregister() | ||||
| @@ -240,7 +256,7 @@ internal class ExtensionInstaller(private val context: Context) { | ||||
|             // Set next installation step | ||||
|             if (uri == null) { | ||||
|                 logcat(LogPriority.ERROR) { "Couldn't locate downloaded APK" } | ||||
|                 downloadsRelay.call(id to InstallStep.Error) | ||||
|                 updateInstallStep(id, InstallStep.Error) | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -14,16 +14,18 @@ import eu.kanade.tachiyomi.extension.model.InstallStep | ||||
| import eu.kanade.tachiyomi.source.online.HttpSource | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import kotlinx.coroutines.delay | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.MutableStateFlow | ||||
| import kotlinx.coroutines.flow.collect | ||||
| import kotlinx.coroutines.flow.collectLatest | ||||
| import kotlinx.coroutines.flow.combine | ||||
| import kotlinx.coroutines.flow.debounce | ||||
| import kotlinx.coroutines.flow.distinctUntilChanged | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.flow.onCompletion | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import kotlinx.coroutines.flow.update | ||||
| import rx.Observable | ||||
| import tachiyomi.core.util.lang.launchIO | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| @@ -130,28 +132,24 @@ class ExtensionsScreenModel( | ||||
|  | ||||
|     fun updateAllExtensions() { | ||||
|         coroutineScope.launchIO { | ||||
|             with(state.value) { | ||||
|                 if (isEmpty) return@launchIO | ||||
|                 items.values | ||||
|                     .flatten() | ||||
|                     .mapNotNull { | ||||
|                         when { | ||||
|                             it.extension !is Extension.Installed -> null | ||||
|                             !it.extension.hasUpdate -> null | ||||
|                             else -> it.extension | ||||
|                         } | ||||
|                     } | ||||
|                     .forEach(::updateExtension) | ||||
|             } | ||||
|             state.value.items.values.flatten() | ||||
|                 .map { it.extension } | ||||
|                 .filterIsInstance<Extension.Installed>() | ||||
|                 .filter { it.hasUpdate } | ||||
|                 .forEach(::updateExtension) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun installExtension(extension: Extension.Available) { | ||||
|         extensionManager.installExtension(extension).subscribeToInstallUpdate(extension) | ||||
|         coroutineScope.launchIO { | ||||
|             extensionManager.installExtension(extension).collectToInstallUpdate(extension) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun updateExtension(extension: Extension.Installed) { | ||||
|         extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension) | ||||
|         coroutineScope.launchIO { | ||||
|             extensionManager.updateExtension(extension).collectToInstallUpdate(extension) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun cancelInstallUpdateExtension(extension: Extension) { | ||||
| @@ -159,29 +157,18 @@ class ExtensionsScreenModel( | ||||
|     } | ||||
|  | ||||
|     private fun removeDownloadState(extension: Extension) { | ||||
|         _currentDownloads.update { _map -> | ||||
|             val map = _map.toMutableMap() | ||||
|             map.remove(extension.pkgName) | ||||
|             map | ||||
|         } | ||||
|         _currentDownloads.update { it - extension.pkgName } | ||||
|     } | ||||
|  | ||||
|     private fun addDownloadState(extension: Extension, installStep: InstallStep) { | ||||
|         _currentDownloads.update { _map -> | ||||
|             val map = _map.toMutableMap() | ||||
|             map[extension.pkgName] = installStep | ||||
|             map | ||||
|         } | ||||
|         _currentDownloads.update { it + Pair(extension.pkgName, installStep) } | ||||
|     } | ||||
|  | ||||
|     private fun Observable<InstallStep>.subscribeToInstallUpdate(extension: Extension) { | ||||
|     private suspend fun Flow<InstallStep>.collectToInstallUpdate(extension: Extension) = | ||||
|         this | ||||
|             .doOnUnsubscribe { removeDownloadState(extension) } | ||||
|             .subscribe( | ||||
|                 { installStep -> addDownloadState(extension, installStep) }, | ||||
|                 { removeDownloadState(extension) }, | ||||
|             ) | ||||
|     } | ||||
|             .onEach { installStep -> addDownloadState(extension, installStep) } | ||||
|             .onCompletion { removeDownloadState(extension) } | ||||
|             .collect() | ||||
|  | ||||
|     fun uninstallExtension(pkgName: String) { | ||||
|         extensionManager.uninstallExtension(pkgName) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user