mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-23 19:11:49 +01:00
Convert extension install process from observable to flow
This commit is contained in:
parent
d8a72f78d5
commit
2ea3142b32
@ -17,6 +17,8 @@ import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
|
|||||||
import eu.kanade.tachiyomi.util.system.launchNow
|
import eu.kanade.tachiyomi.util.system.launchNow
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -232,26 +234,26 @@ class ExtensionManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable of the installation process for the given extension. It will complete
|
* Returns a flow of the installation process for the given extension. It will complete
|
||||||
* once the extension is installed or throws an error. The process will be canceled if
|
* once the extension is installed or throws an error. The process will be canceled the scope
|
||||||
* unsubscribed before its completion.
|
* is canceled before its completion.
|
||||||
*
|
*
|
||||||
* @param extension The extension to be installed.
|
* @param extension The extension to be installed.
|
||||||
*/
|
*/
|
||||||
fun installExtension(extension: Extension.Available): Observable<ExtensionIntallInfo> {
|
fun installExtension(extension: Extension.Available): Flow<ExtensionIntallInfo> {
|
||||||
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
|
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable of the installation process for the given extension. It will complete
|
* 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 if
|
* once the extension is updated or throws an error. The process will be canceled the scope
|
||||||
* unsubscribed before its completion.
|
* is canceled before its completion.
|
||||||
*
|
*
|
||||||
* @param extension The extension to be updated.
|
* @param extension The extension to be updated.
|
||||||
*/
|
*/
|
||||||
fun updateExtension(extension: Extension.Installed): Observable<ExtensionIntallInfo> {
|
fun updateExtension(extension: Extension.Installed): Flow<ExtensionIntallInfo> {
|
||||||
val availableExt = availableExtensions.find { it.pkgName == extension.pkgName }
|
val availableExt = availableExtensions.find { it.pkgName == extension.pkgName }
|
||||||
?: return Observable.empty()
|
?: return emptyFlow()
|
||||||
return installExtension(availableExt)
|
return installExtension(availableExt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,16 +9,28 @@ import android.content.pm.PackageInstaller
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
|
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import rx.Observable
|
import kotlinx.coroutines.Dispatchers
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.flatMapConcat
|
||||||
|
import kotlinx.coroutines.flow.flattenMerge
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
|
import kotlinx.coroutines.flow.transformWhile
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The installer which installs, updates and uninstalls the extensions.
|
* The installer which installs, updates and uninstalls the extensions.
|
||||||
@ -30,7 +42,8 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* The system's download manager
|
* The system's download manager
|
||||||
*/
|
*/
|
||||||
private val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
private val downloadManager =
|
||||||
|
context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The broadcast receiver which listens to download completion events.
|
* The broadcast receiver which listens to download completion events.
|
||||||
@ -44,27 +57,21 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
private val activeDownloads = hashMapOf<String, Long>()
|
private val activeDownloads = hashMapOf<String, Long>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relay used to notify the installation step of every download.
|
* StateFlow used to notify the installation step of every download.
|
||||||
*/
|
*/
|
||||||
private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
|
private val downloadsStateFlow = MutableStateFlow(0L to InstallStep.Pending)
|
||||||
|
|
||||||
/** Map of download id to installer session id */
|
/** Map of download id to installer session id */
|
||||||
val downloadInstallerMap = hashMapOf<Long, Int>()
|
val downloadInstallerMap = hashMapOf<Long, Int>()
|
||||||
|
|
||||||
data class DownloadSessionInfo(
|
|
||||||
val downloadId: Long,
|
|
||||||
val session: PackageInstaller.Session,
|
|
||||||
val sessionId: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the given extension to the downloads queue and returns an observable containing its
|
* Adds the given extension to the downloads queue and returns a flow containing its
|
||||||
* step in the installation process.
|
* step in the installation process.
|
||||||
*
|
*
|
||||||
* @param url The url of the apk.
|
* @param url The url of the apk.
|
||||||
* @param extension The extension to install.
|
* @param extension The extension to install.
|
||||||
*/
|
*/
|
||||||
fun downloadAndInstall(url: String, extension: Extension) = Observable.defer {
|
fun downloadAndInstall(url: String, extension: Extension): Flow<ExtensionIntallInfo> {
|
||||||
val pkgName = extension.pkgName
|
val pkgName = extension.pkgName
|
||||||
|
|
||||||
val oldDownload = activeDownloads[pkgName]
|
val oldDownload = activeDownloads[pkgName]
|
||||||
@ -79,75 +86,96 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
val request = DownloadManager.Request(downloadUri)
|
val request = DownloadManager.Request(downloadUri)
|
||||||
.setTitle(extension.name)
|
.setTitle(extension.name)
|
||||||
.setMimeType(APK_MIME)
|
.setMimeType(APK_MIME)
|
||||||
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment)
|
.setDestinationInExternalFilesDir(
|
||||||
|
context,
|
||||||
|
Environment.DIRECTORY_DOWNLOADS,
|
||||||
|
downloadUri.lastPathSegment
|
||||||
|
)
|
||||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||||
|
|
||||||
val id = downloadManager.enqueue(request)
|
val id = downloadManager.enqueue(request)
|
||||||
activeDownloads[pkgName] = id
|
activeDownloads[pkgName] = id
|
||||||
|
|
||||||
downloadsRelay.filter { it.first == id }
|
return flowOf(
|
||||||
.map {
|
pollStatus(id),
|
||||||
val sessionId = downloadInstallerMap[it.first] ?: return@map it.second to null
|
pollInstallStatus(id),
|
||||||
val session = context.packageManager.packageInstaller.getSessionInfo(sessionId)
|
downloadsStateFlow.filter { it.first == id }
|
||||||
it.second to session
|
.map {
|
||||||
|
it.second to findSession(it.first)
|
||||||
|
}
|
||||||
|
).flattenMerge()
|
||||||
|
.transformWhile {
|
||||||
|
emit(it)
|
||||||
|
!it.first.isCompleted()
|
||||||
}
|
}
|
||||||
// Poll download status
|
.flowOn(Dispatchers.IO)
|
||||||
.mergeWith(pollStatus(id))
|
.catch { e ->
|
||||||
// Poll installation status
|
Timber.e(e)
|
||||||
.mergeWith(pollInstallStatus(id))
|
emit(InstallStep.Error to null)
|
||||||
// Force an error if the download takes more than 3 minutes
|
}
|
||||||
.mergeWith(Observable.timer(3, TimeUnit.MINUTES).map { InstallStep.Error to null })
|
.onCompletion {
|
||||||
// Stop when the application is installed or errors
|
deleteDownload(pkgName)
|
||||||
.takeUntil { it.first.isCompleted() }
|
}
|
||||||
// Always notify on main thread
|
}
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
// Always remove the download when unsubscribed
|
private fun findSession(downloadId: Long): PackageInstaller.SessionInfo? {
|
||||||
.doOnUnsubscribe { deleteDownload(pkgName) }
|
val sessionId = downloadInstallerMap[downloadId] ?: return null
|
||||||
|
return context.packageManager.packageInstaller.getSessionInfo(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
* manager doesn't have any notification system. It'll stop once the download finishes.
|
||||||
*
|
*
|
||||||
* @param id The id of the download to poll.
|
* @param id The id of the download to poll.
|
||||||
*/
|
*/
|
||||||
private fun pollStatus(id: Long): Observable<ExtensionIntallInfo> {
|
private fun pollStatus(id: Long): Flow<ExtensionIntallInfo> {
|
||||||
val query = DownloadManager.Query().setFilterById(id)
|
val query = DownloadManager.Query().setFilterById(id)
|
||||||
|
|
||||||
return Observable.interval(0, 1, TimeUnit.SECONDS)
|
return flow {
|
||||||
// Get the current download status
|
while (true) {
|
||||||
.map {
|
val newDownloadState = downloadManager.query(query).use { cursor ->
|
||||||
downloadManager.query(query).use { cursor ->
|
|
||||||
cursor.moveToFirst()
|
cursor.moveToFirst()
|
||||||
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||||
}
|
}
|
||||||
|
emit(newDownloadState)
|
||||||
|
delay(1000)
|
||||||
}
|
}
|
||||||
// Ignore duplicate results
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
// Stop polling when the download fails or finishes
|
.transformWhile {
|
||||||
.takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED }
|
emit(it)
|
||||||
// Map to our model
|
!(it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED)
|
||||||
.flatMap { status ->
|
}
|
||||||
val step = when (status) {
|
.flatMapConcat { downloadState ->
|
||||||
|
val step = when (downloadState) {
|
||||||
DownloadManager.STATUS_PENDING -> InstallStep.Pending
|
DownloadManager.STATUS_PENDING -> InstallStep.Pending
|
||||||
DownloadManager.STATUS_RUNNING -> InstallStep.Downloading
|
DownloadManager.STATUS_RUNNING -> InstallStep.Downloading
|
||||||
else -> return@flatMap Observable.empty()
|
else -> return@flatMapConcat emptyFlow()
|
||||||
}
|
}
|
||||||
Observable.just(ExtensionIntallInfo(step, null))
|
flowOf(ExtensionIntallInfo(step, null))
|
||||||
}
|
|
||||||
.doOnError {
|
|
||||||
Timber.e(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pollInstallStatus(id: Long): Observable<ExtensionIntallInfo> {
|
/**
|
||||||
return Observable.interval(0, 500, TimeUnit.MILLISECONDS)
|
* Returns a flow that polls the given installer session for its status every half second, as the
|
||||||
.flatMap {
|
* manager doesn't have any notification system. This will only stop once
|
||||||
val sessionId = downloadInstallerMap[id] ?: return@flatMap Observable.empty()
|
*
|
||||||
val session = context.packageManager.packageInstaller.getSessionInfo(sessionId)
|
* @param id The id of the download mapped to the session to poll.
|
||||||
Observable.just(InstallStep.Installing to session)
|
*/
|
||||||
|
private fun pollInstallStatus(id: Long): Flow<ExtensionIntallInfo> {
|
||||||
|
return flow {
|
||||||
|
while (true) {
|
||||||
|
val sessionId = downloadInstallerMap[id]
|
||||||
|
if (sessionId != null) {
|
||||||
|
val session =
|
||||||
|
context.packageManager.packageInstaller.getSessionInfo(sessionId)
|
||||||
|
emit(InstallStep.Installing to session)
|
||||||
|
}
|
||||||
|
delay(500)
|
||||||
}
|
}
|
||||||
.doOnError {
|
}
|
||||||
|
.catch {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,7 +213,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
* @param downloadId The id of the download.
|
* @param downloadId The id of the download.
|
||||||
*/
|
*/
|
||||||
fun setInstalling(downloadId: Long, sessionId: Int) {
|
fun setInstalling(downloadId: Long, sessionId: Int) {
|
||||||
downloadsRelay.call(downloadId to InstallStep.Installing)
|
downloadsStateFlow.tryEmit(downloadId to InstallStep.Installing)
|
||||||
downloadInstallerMap[downloadId] = sessionId
|
downloadInstallerMap[downloadId] = sessionId
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,7 +232,7 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
fun setInstallationResult(downloadId: Long, result: Boolean) {
|
fun setInstallationResult(downloadId: Long, result: Boolean) {
|
||||||
val step = if (result) InstallStep.Installed else InstallStep.Error
|
val step = if (result) InstallStep.Installed else InstallStep.Error
|
||||||
downloadInstallerMap.remove(downloadId)
|
downloadInstallerMap.remove(downloadId)
|
||||||
downloadsRelay.call(downloadId to step)
|
downloadsStateFlow.tryEmit(downloadId to step)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -267,10 +295,10 @@ internal class ExtensionInstaller(private val context: Context) {
|
|||||||
|
|
||||||
// Set next installation step
|
// Set next installation step
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
downloadsRelay.call(id to InstallStep.Loading)
|
downloadsStateFlow.tryEmit(id to InstallStep.Loading)
|
||||||
} else {
|
} else {
|
||||||
Timber.e("Couldn't locate downloaded APK")
|
Timber.e("Couldn't locate downloaded APK")
|
||||||
downloadsRelay.call(id to InstallStep.Error)
|
downloadsStateFlow.tryEmit(id to InstallStep.Error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,12 +19,17 @@ import eu.kanade.tachiyomi.ui.migration.SelectionHeader
|
|||||||
import eu.kanade.tachiyomi.ui.migration.SourceItem
|
import eu.kanade.tachiyomi.ui.migration.SourceItem
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import eu.kanade.tachiyomi.util.system.executeOnIO
|
import eu.kanade.tachiyomi.util.system.executeOnIO
|
||||||
|
import eu.kanade.tachiyomi.util.system.withUIContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
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.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
@ -216,7 +221,7 @@ class ExtensionBottomPresenter(
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
private fun updateInstallStep(
|
private fun updateInstallStep(
|
||||||
extension: Extension,
|
extension: Extension,
|
||||||
state: InstallStep,
|
state: InstallStep?,
|
||||||
session: PackageInstaller.SessionInfo?
|
session: PackageInstaller.SessionInfo?
|
||||||
): ExtensionItem? {
|
): ExtensionItem? {
|
||||||
val extensions = extensions.toMutableList()
|
val extensions = extensions.toMutableList()
|
||||||
@ -243,13 +248,17 @@ class ExtensionBottomPresenter(
|
|||||||
|
|
||||||
fun installExtension(extension: Extension.Available) {
|
fun installExtension(extension: Extension.Available) {
|
||||||
if (isNotMIUIOptimized()) {
|
if (isNotMIUIOptimized()) {
|
||||||
extensionManager.installExtension(extension).subscribeToInstallUpdate(extension)
|
presenterScope.launch {
|
||||||
|
extensionManager.installExtension(extension).collectForInstallUpdate(extension)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateExtension(extension: Extension.Installed) {
|
fun updateExtension(extension: Extension.Installed) {
|
||||||
if (isNotMIUIOptimized()) {
|
if (isNotMIUIOptimized()) {
|
||||||
extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension)
|
presenterScope.launch {
|
||||||
|
extensionManager.updateExtension(extension).collectForInstallUpdate(extension)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,13 +270,20 @@ class ExtensionBottomPresenter(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Observable<ExtensionIntallInfo>.subscribeToInstallUpdate(extension: Extension) {
|
private suspend fun Flow<ExtensionIntallInfo>.collectForInstallUpdate(extension: Extension) {
|
||||||
this.doOnNext { currentDownloads[extension.pkgName] = it }
|
this
|
||||||
.doOnUnsubscribe { currentDownloads.remove(extension.pkgName) }
|
.onEach { currentDownloads[extension.pkgName] = it }
|
||||||
.map { state -> updateInstallStep(extension, state.first, state.second) }
|
.onCompletion {
|
||||||
.subscribe { item ->
|
currentDownloads.remove(extension.pkgName)
|
||||||
|
val item = updateInstallStep(extension, null, null)
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
bottomSheet.downloadUpdate(item)
|
withUIContext { bottomSheet.downloadUpdate(item) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.collect { state ->
|
||||||
|
val item = updateInstallStep(extension, state.first, state.second)
|
||||||
|
if (item != null) {
|
||||||
|
withUIContext { bottomSheet.downloadUpdate(item) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user