diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt index 03074dfbc3..b0f1921c90 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt @@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.extension.ExtensionManager.ExtensionInfo 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.toast import kotlinx.coroutines.CoroutineScope @@ -49,6 +50,8 @@ class ExtensionInstallService( private val preferences: PreferencesHelper = Injekt.get() + private var activeInstalls = listOf() + /** * This method needs to be implemented, but it's not used/needed. */ @@ -85,6 +88,11 @@ class ExtensionInstallService( ) < it.versionCode } ?: return START_NOT_STICKY + + activeInstalls = list.map { it.pkgName } + serviceScope.launch { + list.forEach { extensionManager.setPending(it.pkgName) } + } var installed = 0 val installedExtensions = mutableListOf() job = serviceScope.launch { @@ -143,6 +151,8 @@ class ExtensionInstallService( override fun onDestroy() { job?.cancel() serviceScope.cancel() + activeInstalls.forEach { extensionManager.cleanUpInstallation(it) } + extensionManager.downloadRelay.tryEmit("Finished" to (InstallStep.Installed to null)) if (instance == this) { instance = null } @@ -166,6 +176,8 @@ class ExtensionInstallService( context.stopService(Intent(context, ExtensionUpdateJob::class.java)) } + fun activeInstalls(): List? = instance?.activeInstalls + /** * Returns the status of the service. * 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 4ed3ed4b27..e89fd9def4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -8,6 +8,7 @@ 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.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver import eu.kanade.tachiyomi.extension.util.ExtensionInstaller @@ -54,7 +55,7 @@ class ExtensionManager( val downloadRelay get() = installer.downloadsStateFlow - fun getExtension(downloadId: Long): String? { + private fun getExtension(downloadId: Long): String? { return installer.activeDownloads.entries.find { downloadId == it.value }?.key } @@ -266,7 +267,16 @@ class ExtensionManager( * @param result Whether the extension was installed or not. */ 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. @@ -284,7 +294,39 @@ class ExtensionManager( * @param downloadId The id of the download. */ 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) } /** 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 09f9c49948..afbd6d92c4 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 @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.extension.util +import android.annotation.SuppressLint import android.app.DownloadManager import android.content.BroadcastReceiver 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. */ - 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 */ - val downloadInstallerMap = hashMapOf() + val downloadInstallerMap = hashMapOf() /** * 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 { flowOf( pollStatus(id), - pollInstallStatus(id) + pollInstallStatus(pkgName) ).flattenMerge() .transformWhile { emit(it) @@ -118,11 +119,11 @@ internal class ExtensionInstaller(private val context: Context) { deleteDownload(pkgName) } .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) .transformWhile { emit(it) @@ -139,6 +140,7 @@ internal class ExtensionInstaller(private val context: Context) { * * @param id The id of the download to poll. */ + @SuppressLint("Range") private fun pollStatus(id: Long): Flow { 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 * 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 { + private fun pollInstallStatus(pkgName: String): Flow { return flow { while (true) { - val sessionId = downloadInstallerMap[id] + val sessionId = downloadInstallerMap[pkgName] if (sessionId != null) { val session = context.packageManager.packageInstaller.getSessionInfo(sessionId) @@ -191,7 +193,7 @@ internal class ExtensionInstaller(private val context: Context) { } } .takeWhile { info -> - val sessionId = downloadInstallerMap[id] + val sessionId = downloadInstallerMap[pkgName] if (sessionId != null) { info.second != null } else { @@ -250,15 +252,29 @@ internal class ExtensionInstaller(private val context: Context) { * * @param downloadId The id of the download. */ - fun setInstalling(downloadId: Long, sessionId: Int) { - downloadsStateFlow.tryEmit(downloadId to ExtensionIntallInfo(InstallStep.Installing, null)) - downloadInstallerMap[downloadId] = sessionId + fun setInstalling(pkgName: String, sessionId: Int) { + downloadsStateFlow.tryEmit(pkgName to ExtensionIntallInfo(InstallStep.Installing, null)) + downloadInstallerMap[pkgName] = sessionId + } + + suspend fun setPending(pkgName: String) { + downloadsStateFlow.emit(pkgName to ExtensionIntallInfo(InstallStep.Pending, null)) } fun cancelInstallation(sessionId: Int) { val downloadId = downloadInstallerMap.entries.find { it.value == sessionId }?.key ?: return setInstallationResult(downloadId, false) - context.packageManager.packageInstaller.abandonSession(sessionId) + try { + 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 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 - downloadInstallerMap.remove(downloadId) - downloadsStateFlow.tryEmit(downloadId to ExtensionIntallInfo(step, null)) + downloadInstallerMap.remove(pkgName) + downloadsStateFlow.tryEmit(pkgName to ExtensionIntallInfo(step, null)) } 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 * downloads and notifies its installation step. */ + @SuppressLint("Range") override fun onReceive(context: Context, intent: Intent?) { 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 pkgName = activeDownloads.entries.find { id == it.value }?.key // Set next installation step - if (uri != null) { - downloadsStateFlow.tryEmit(id to ExtensionIntallInfo(InstallStep.Loading, null)) - } else { + if (uri != null && pkgName != null) { + downloadsStateFlow.tryEmit(pkgName to ExtensionIntallInfo(InstallStep.Loading, null)) + } else if (pkgName != null) { Timber.e("Couldn't locate downloaded APK") - downloadsStateFlow.tryEmit(id to ExtensionIntallInfo(InstallStep.Error, null)) + downloadsStateFlow.tryEmit(pkgName 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 07b26dfd7c..9c1671eacc 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 @@ -59,6 +59,7 @@ class ExtensionBottomPresenter( private val sourceManager: SourceManager = Injekt.get() private var selectedSource: Long? = null + private var firstLoad = true private val db: DatabaseHelper = Injekt.get() override fun onCreate() { @@ -100,9 +101,21 @@ class ExtensionBottomPresenter( presenterScope.launch { extensionManager.downloadRelay .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 -> - extPageName == item.extension.pkgName + it.first == item.extension.pkgName } ?: return@collect when (it.second.first) { InstallStep.Installed, InstallStep.Error -> { @@ -183,6 +196,15 @@ class ExtensionBottomPresenter( val items = mutableListOf() + 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 sortOrder = InstalledExtensionsOrder.fromPreference(preferences) val installedSorted = installed