Minor updates to extension installs

downloadsStateFlow now uses pkgName instead of downloadId
Suppress downloadId warnings
ExtensionInstallJob now sets all pending extensions into "Pending" State
On load, extensions list now gets the current status of updating/installing extensions
Emit a finish to all pending extension on job cancel
This commit is contained in:
Jays2Kings 2021-08-22 23:30:27 -04:00
parent b3d05d50fa
commit 0d67d49fc9
4 changed files with 119 additions and 25 deletions

View File

@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.extension.ExtensionManager.ExtensionInfo import eu.kanade.tachiyomi.extension.ExtensionManager.ExtensionInfo
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.util.system.notificationManager import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -49,6 +50,8 @@ class ExtensionInstallService(
private val preferences: PreferencesHelper = Injekt.get() private val preferences: PreferencesHelper = Injekt.get()
private var activeInstalls = listOf<String>()
/** /**
* This method needs to be implemented, but it's not used/needed. * This method needs to be implemented, but it's not used/needed.
*/ */
@ -85,6 +88,11 @@ class ExtensionInstallService(
) < it.versionCode ) < it.versionCode
} }
?: return START_NOT_STICKY ?: return START_NOT_STICKY
activeInstalls = list.map { it.pkgName }
serviceScope.launch {
list.forEach { extensionManager.setPending(it.pkgName) }
}
var installed = 0 var installed = 0
val installedExtensions = mutableListOf<ExtensionInfo>() val installedExtensions = mutableListOf<ExtensionInfo>()
job = serviceScope.launch { job = serviceScope.launch {
@ -143,6 +151,8 @@ class ExtensionInstallService(
override fun onDestroy() { override fun onDestroy() {
job?.cancel() job?.cancel()
serviceScope.cancel() serviceScope.cancel()
activeInstalls.forEach { extensionManager.cleanUpInstallation(it) }
extensionManager.downloadRelay.tryEmit("Finished" to (InstallStep.Installed to null))
if (instance == this) { if (instance == this) {
instance = null instance = null
} }
@ -166,6 +176,8 @@ class ExtensionInstallService(
context.stopService(Intent(context, ExtensionUpdateJob::class.java)) context.stopService(Intent(context, ExtensionUpdateJob::class.java))
} }
fun activeInstalls(): List<String>? = instance?.activeInstalls
/** /**
* Returns the status of the service. * Returns the status of the service.
* *

View File

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
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.LoadResult import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
@ -54,7 +55,7 @@ class ExtensionManager(
val downloadRelay val downloadRelay
get() = installer.downloadsStateFlow get() = installer.downloadsStateFlow
fun getExtension(downloadId: Long): String? { private fun getExtension(downloadId: Long): String? {
return installer.activeDownloads.entries.find { downloadId == it.value }?.key return installer.activeDownloads.entries.find { downloadId == it.value }?.key
} }
@ -266,7 +267,16 @@ class ExtensionManager(
* @param result Whether the extension was installed or not. * @param result Whether the extension was installed or not.
*/ */
fun setInstallationResult(downloadId: Long, result: Boolean) { fun setInstallationResult(downloadId: Long, result: Boolean) {
installer.setInstallationResult(downloadId, result) val pkgName = getExtension(downloadId) ?: return
setInstallationResult(pkgName, result)
}
fun cleanUpInstallation(pkgName: String) {
installer.cleanUpInstallation(pkgName)
}
fun setInstallationResult(pkgName: String, result: Boolean) {
installer.setInstallationResult(pkgName, result)
} }
/** Sets the result of the installation of an extension. /** Sets the result of the installation of an extension.
@ -284,7 +294,39 @@ class ExtensionManager(
* @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) {
installer.setInstalling(downloadId, sessionId) val pkgName = getExtension(downloadId) ?: return
setInstalling(pkgName, sessionId)
}
fun setInstalling(pkgName: String, sessionId: Int) {
installer.setInstalling(pkgName, sessionId)
}
suspend fun setPending(pkgName: String) {
installer.setPending(pkgName)
}
fun getInstallInfo(pkgName: String): ExtensionIntallInfo? {
val installStep = when {
installer.downloadInstallerMap[pkgName] != null &&
context.packageManager.packageInstaller
.getSessionInfo(installer.downloadInstallerMap[pkgName] ?: 0) != null -> {
InstallStep.Installing
}
installer.activeDownloads[pkgName] != null -> InstallStep.Downloading
ExtensionInstallService.activeInstalls()
?.contains(pkgName) == true -> InstallStep.Pending
else -> return null
}
val sessionInfo = run {
val sessionId = installer.downloadInstallerMap[pkgName]
if (sessionId != null) {
context.packageManager.packageInstaller.getSessionInfo(sessionId)
} else {
null
}
}
return ExtensionIntallInfo(installStep, sessionInfo)
} }
/** /**

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.extension.util package eu.kanade.tachiyomi.extension.util
import android.annotation.SuppressLint
import android.app.DownloadManager import android.app.DownloadManager
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
@ -63,10 +64,10 @@ internal class ExtensionInstaller(private val context: Context) {
/** /**
* StateFlow used to notify the installation step of every download. * StateFlow used to notify the installation step of every download.
*/ */
val downloadsStateFlow = MutableStateFlow(0L to ExtensionIntallInfo(InstallStep.Pending, null)) val downloadsStateFlow = MutableStateFlow("" to ExtensionIntallInfo(InstallStep.Pending, null))
/** Map of download id to installer session id */ /** Map of download id to installer session id */
val downloadInstallerMap = hashMapOf<Long, Int>() val downloadInstallerMap = hashMapOf<String, Int>()
/** /**
* Adds the given extension to the downloads queue and returns a flow containing its * Adds the given extension to the downloads queue and returns a flow containing its
@ -103,7 +104,7 @@ internal class ExtensionInstaller(private val context: Context) {
scope.launch { scope.launch {
flowOf( flowOf(
pollStatus(id), pollStatus(id),
pollInstallStatus(id) pollInstallStatus(pkgName)
).flattenMerge() ).flattenMerge()
.transformWhile { .transformWhile {
emit(it) emit(it)
@ -118,11 +119,11 @@ internal class ExtensionInstaller(private val context: Context) {
deleteDownload(pkgName) deleteDownload(pkgName)
} }
.collect { .collect {
downloadsStateFlow.emit(id to it) downloadsStateFlow.emit(extension.pkgName to it)
} }
} }
return downloadsStateFlow.filter { it.first == id }.map { it.second } return downloadsStateFlow.filter { it.first == extension.pkgName }.map { it.second }
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.transformWhile { .transformWhile {
emit(it) emit(it)
@ -139,6 +140,7 @@ internal class ExtensionInstaller(private val context: Context) {
* *
* @param id The id of the download to poll. * @param id The id of the download to poll.
*/ */
@SuppressLint("Range")
private fun pollStatus(id: Long): Flow<ExtensionIntallInfo> { private fun pollStatus(id: Long): Flow<ExtensionIntallInfo> {
val query = DownloadManager.Query().setFilterById(id) val query = DownloadManager.Query().setFilterById(id)
@ -176,12 +178,12 @@ internal class ExtensionInstaller(private val context: Context) {
* Returns a flow that polls the given installer session for its status every half second, as the * Returns a flow that polls the given installer session for its status every half second, as the
* manager doesn't have any notification system. This will only stop once * manager doesn't have any notification system. This will only stop once
* *
* @param id The id of the download mapped to the session to poll. * @param pkgName The pkgName of the download mapped to the session to poll.
*/ */
private fun pollInstallStatus(id: Long): Flow<ExtensionIntallInfo> { private fun pollInstallStatus(pkgName: String): Flow<ExtensionIntallInfo> {
return flow { return flow {
while (true) { while (true) {
val sessionId = downloadInstallerMap[id] val sessionId = downloadInstallerMap[pkgName]
if (sessionId != null) { if (sessionId != null) {
val session = val session =
context.packageManager.packageInstaller.getSessionInfo(sessionId) context.packageManager.packageInstaller.getSessionInfo(sessionId)
@ -191,7 +193,7 @@ internal class ExtensionInstaller(private val context: Context) {
} }
} }
.takeWhile { info -> .takeWhile { info ->
val sessionId = downloadInstallerMap[id] val sessionId = downloadInstallerMap[pkgName]
if (sessionId != null) { if (sessionId != null) {
info.second != null info.second != null
} else { } else {
@ -250,15 +252,29 @@ 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(pkgName: String, sessionId: Int) {
downloadsStateFlow.tryEmit(downloadId to ExtensionIntallInfo(InstallStep.Installing, null)) downloadsStateFlow.tryEmit(pkgName to ExtensionIntallInfo(InstallStep.Installing, null))
downloadInstallerMap[downloadId] = sessionId downloadInstallerMap[pkgName] = sessionId
}
suspend fun setPending(pkgName: String) {
downloadsStateFlow.emit(pkgName to ExtensionIntallInfo(InstallStep.Pending, null))
} }
fun cancelInstallation(sessionId: Int) { fun cancelInstallation(sessionId: Int) {
val downloadId = downloadInstallerMap.entries.find { it.value == sessionId }?.key ?: return val downloadId = downloadInstallerMap.entries.find { it.value == sessionId }?.key ?: return
setInstallationResult(downloadId, false) setInstallationResult(downloadId, false)
try {
context.packageManager.packageInstaller.abandonSession(sessionId) context.packageManager.packageInstaller.abandonSession(sessionId)
} catch (_: Exception) { }
}
fun cleanUpInstallation(pkgName: String) {
val sessionId = downloadInstallerMap[pkgName] ?: return
downloadInstallerMap.remove(pkgName)
try {
context.packageManager.packageInstaller.abandonSession(sessionId)
} catch (_: Exception) { }
} }
/** /**
@ -267,10 +283,10 @@ internal class ExtensionInstaller(private val context: Context) {
* @param downloadId The id of the download. * @param downloadId The id of the download.
* @param result Whether the extension was installed or not. * @param result Whether the extension was installed or not.
*/ */
fun setInstallationResult(downloadId: Long, result: Boolean) { fun setInstallationResult(pkgName: String, 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(pkgName)
downloadsStateFlow.tryEmit(downloadId to ExtensionIntallInfo(step, null)) downloadsStateFlow.tryEmit(pkgName to ExtensionIntallInfo(step, null))
} }
fun softDeleteDownload(downloadId: Long) { fun softDeleteDownload(downloadId: Long) {
@ -327,6 +343,7 @@ internal class ExtensionInstaller(private val context: Context) {
* Called when a download event is received. It looks for the download in the current active * Called when a download event is received. It looks for the download in the current active
* downloads and notifies its installation step. * downloads and notifies its installation step.
*/ */
@SuppressLint("Range")
override fun onReceive(context: Context, intent: Intent?) { override fun onReceive(context: Context, intent: Intent?) {
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0) ?: return val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0) ?: return
@ -335,12 +352,13 @@ internal class ExtensionInstaller(private val context: Context) {
val uri = downloadManager.getUriForDownloadedFile(id) val uri = downloadManager.getUriForDownloadedFile(id)
val pkgName = activeDownloads.entries.find { id == it.value }?.key
// Set next installation step // Set next installation step
if (uri != null) { if (uri != null && pkgName != null) {
downloadsStateFlow.tryEmit(id to ExtensionIntallInfo(InstallStep.Loading, null)) downloadsStateFlow.tryEmit(pkgName to ExtensionIntallInfo(InstallStep.Loading, null))
} else { } else if (pkgName != null) {
Timber.e("Couldn't locate downloaded APK") Timber.e("Couldn't locate downloaded APK")
downloadsStateFlow.tryEmit(id to ExtensionIntallInfo(InstallStep.Error, null)) downloadsStateFlow.tryEmit(pkgName to ExtensionIntallInfo(InstallStep.Error, null))
return return
} }

View File

@ -59,6 +59,7 @@ class ExtensionBottomPresenter(
private val sourceManager: SourceManager = Injekt.get() private val sourceManager: SourceManager = Injekt.get()
private var selectedSource: Long? = null private var selectedSource: Long? = null
private var firstLoad = true
private val db: DatabaseHelper = Injekt.get() private val db: DatabaseHelper = Injekt.get()
override fun onCreate() { override fun onCreate() {
@ -100,9 +101,21 @@ class ExtensionBottomPresenter(
presenterScope.launch { presenterScope.launch {
extensionManager.downloadRelay extensionManager.downloadRelay
.collect { .collect {
val extPageName = extensionManager.getExtension(it.first) if (it.first == "Finished") {
firstLoad = true
currentDownloads.clear()
extensions = toItems(
Triple(
extensionManager.installedExtensions,
extensionManager.untrustedExtensions,
extensionManager.availableExtensions
)
)
withUIContext { bottomSheet.setExtensions(extensions) }
return@collect
}
val extension = extensions.find { item -> val extension = extensions.find { item ->
extPageName == item.extension.pkgName it.first == item.extension.pkgName
} ?: return@collect } ?: return@collect
when (it.second.first) { when (it.second.first) {
InstallStep.Installed, InstallStep.Error -> { InstallStep.Installed, InstallStep.Error -> {
@ -183,6 +196,15 @@ class ExtensionBottomPresenter(
val items = mutableListOf<ExtensionItem>() val items = mutableListOf<ExtensionItem>()
if (firstLoad) {
val listOfExtensions = installed + untrusted + available
listOfExtensions.forEach {
val installInfo = extensionManager.getInstallInfo(it.pkgName) ?: return@forEach
currentDownloads[it.pkgName] = installInfo
}
firstLoad = false
}
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { it.name } val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { it.name }
val sortOrder = InstalledExtensionsOrder.fromPreference(preferences) val sortOrder = InstalledExtensionsOrder.fromPreference(preferences)
val installedSorted = installed val installedSorted = installed