From 2d86a70ce28098e8d9fcccd9e13122e8edf8667f Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Sun, 27 Sep 2026 23:59:53 +0600 Subject: [PATCH] Offer updates for extensions that aren't loaded Extensions that aren't loaded now get updates like loaded ones, which is often what gets them loaded again. They're counted, notified and listed with the other pending updates. Every installed extension, loaded or not, records the added store whose key signed it when it loads. --- .../interactor/GetExtensionsByType.kt | 8 ++++-- .../domain/extension/model/Extensions.kt | 2 +- .../presentation/browse/ExtensionsScreen.kt | 25 +++++++++++++++---- .../tachiyomi/extension/ExtensionManager.kt | 21 ++++++++++++---- .../tachiyomi/extension/api/ExtensionApi.kt | 9 ++++--- .../extension/util/ExtensionLoader.kt | 16 ++++++++++-- .../browse/extension/ExtensionsViewModel.kt | 4 +-- .../kanade/tachiyomi/ui/main/MainActivity.kt | 4 ++- app/src/main/java/mihon/app/di/AppGraph.kt | 2 ++ .../tachiyomi/extension/model/Extension.kt | 10 ++++++-- 10 files changed, 77 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt index 6e07571fb..cfda4f249 100644 --- a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt +++ b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt @@ -23,14 +23,18 @@ class GetExtensionsByType( extensionManager.notLoadedExtensionsFlow, extensionManager.availableExtensionsFlow, ) { enabledLanguages, _loaded, _notLoaded, _available -> - val (updates, loaded) = _loaded + val (loadedUpdates, loaded) = _loaded .sortedWith( compareBy { !it.isObsolete } .thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }, ) .partition { it.hasUpdate } - val notLoaded = _notLoaded + val (notLoadedUpdates, notLoaded) = _notLoaded + .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) + .partition { it.hasUpdate } + + val updates = (loadedUpdates + notLoadedUpdates) .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) val available = _available diff --git a/app/src/main/java/eu/kanade/domain/extension/model/Extensions.kt b/app/src/main/java/eu/kanade/domain/extension/model/Extensions.kt index 502ff008f..1c3fceb42 100644 --- a/app/src/main/java/eu/kanade/domain/extension/model/Extensions.kt +++ b/app/src/main/java/eu/kanade/domain/extension/model/Extensions.kt @@ -3,7 +3,7 @@ package eu.kanade.domain.extension.model import eu.kanade.tachiyomi.extension.model.Extension data class Extensions( - val updates: List, + val updates: List, val loaded: List, val available: List, val notLoaded: List, diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt index 258b324cd..55bd24f90 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -83,7 +83,7 @@ fun ExtensionScreen( onOpenWebView: (Extension.Available) -> Unit, onInstallExtension: (Extension.Available) -> Unit, onUninstallExtension: (Extension.Installed) -> Unit, - onUpdateExtension: (Extension.Loaded) -> Unit, + onUpdateExtension: (Extension.Installed) -> Unit, onTrustExtension: (Extension.NotLoaded) -> Unit, onOpenExtension: (Extension.Loaded) -> Unit, onClickUpdateAll: () -> Unit, @@ -144,7 +144,7 @@ private fun ExtensionContent( onOpenWebView: (Extension.Available) -> Unit, onInstallExtension: (Extension.Available) -> Unit, onUninstallExtension: (Extension.Installed) -> Unit, - onUpdateExtension: (Extension.Loaded) -> Unit, + onUpdateExtension: (Extension.Installed) -> Unit, onTrustExtension: (Extension.NotLoaded) -> Unit, onOpenExtension: (Extension.Loaded) -> Unit, onClickUpdateAll: () -> Unit, @@ -232,7 +232,9 @@ private fun ExtensionContent( when (it) { is Extension.Available -> onOpenWebView(it) is Extension.Loaded -> onOpenExtension(it) - else -> {} + is Extension.NotLoaded -> { + notLoadedState = it + } } }, onClickItemCancel = onClickItemCancel, @@ -247,7 +249,11 @@ private fun ExtensionContent( } } is Extension.NotLoaded -> { - notLoadedState = it + if (it.hasUpdate) { + onUpdateExtension(it) + } else { + notLoadedState = it + } } } }, @@ -479,7 +485,7 @@ private fun ExtensionItemActions( } is Extension.NotLoaded -> { val isUntrusted = extension.reason is Extension.NotLoaded.Reason.Untrusted - IconButton(onClick = { onClickItemAction(extension) }) { + IconButton(onClick = { onClickItemSecondaryAction(extension) }) { Icon( imageVector = if (isUntrusted) { MaterialSymbols.Rounded.VerifiedUser @@ -493,6 +499,15 @@ private fun ExtensionItemActions( }, ) } + + if (extension.hasUpdate) { + IconButton(onClick = { onClickItemAction(extension) }) { + Icon( + imageVector = MaterialSymbols.Rounded.Download, + contentDescription = stringResource(MR.strings.ext_update), + ) + } + } } is Extension.Available -> { if (extension.sources.isNotEmpty()) { 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 cd5a4d3a3..acdaa1489 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -215,7 +215,7 @@ class ExtensionManager( } /** - * Sets the update field of the installed extensions with the given [availableExtensions]. + * Sets the update fields of the installed extensions, loaded or not, with the given [availableExtensions]. * * @param availableExtensions The list of extensions given by the [api]. */ @@ -228,6 +228,9 @@ class ExtensionManager( loadedExtensionMapFlow.value = loadedExtensionMapFlow.value.mapValues { (_, extension) -> extension.withStatus(availableExtensions) } + notLoadedExtensionMapFlow.value = notLoadedExtensionMapFlow.value.mapValues { (_, extension) -> + extension.withStatus(availableExtensions) + } updatePendingUpdatesCount() } @@ -249,7 +252,7 @@ class ExtensionManager( * * @param extension The extension to be updated. */ - fun updateExtension(extension: Extension.Loaded): Flow { + fun updateExtension(extension: Extension.Installed): Flow { val update = extension.findUpdate(availableExtensionListFlow.value) ?: return emptyFlow() val isUpdateForPrivatelyInstalled = !extension.isShared return installer.downloadAndInstall(update, isUpdateForPrivatelyInstalled) @@ -328,7 +331,7 @@ class ExtensionManager( override fun onExtensionNotLoaded(extension: Extension.NotLoaded) { loadedExtensionMapFlow.value -= extension.pkgName - notLoadedExtensionMapFlow.value += extension + notLoadedExtensionMapFlow.value += extension.withStatus(availableExtensionListFlow.value) updatePendingUpdatesCount() } @@ -349,12 +352,20 @@ class ExtensionManager( return copy( hasUpdate = findUpdate(availableExtensions) != null, isObsolete = listing == null, - store = listing?.store ?: store, ) } + /** + * An extension that isn't loaded can still be updated, which is often what gets it loaded again. + */ + private fun Extension.NotLoaded.withStatus(availableExtensions: List): Extension.NotLoaded { + if (availableExtensions.isEmpty()) return this + return copy(hasUpdate = findUpdate(availableExtensions) != null) + } + private fun updatePendingUpdatesCount() { - val pendingUpdateCount = loadedExtensionMapFlow.value.values.count { it.hasUpdate } + val pendingUpdateCount = loadedExtensionMapFlow.value.values.count { it.hasUpdate } + + notLoadedExtensionMapFlow.value.values.count { it.hasUpdate } preferences.extensionUpdatesCount.set(pendingUpdateCount) if (pendingUpdateCount == 0) { extensionUpdateNotifier.dismiss() diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt index 7ab402a68..26239de75 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt @@ -21,15 +21,16 @@ class ExtensionApi( } /** - * @param loadedExtensions Extensions already loaded by [eu.kanade.tachiyomi.extension.ExtensionManager]. - * Only their versions are read, so there's nothing to gain from loading them a second time. + * @param installedExtensions Extensions already read by [eu.kanade.tachiyomi.extension.ExtensionManager], + * loaded or not. Only their versions and signatures are read, so there's nothing to gain from reading them + * a second time. */ - suspend fun checkForUpdates(loadedExtensions: List) { + suspend fun checkForUpdates(installedExtensions: List) { updateExtensionStores() val extensions = findExtensions() - val extensionsWithUpdate = loadedExtensions.filter { it.findUpdate(extensions) != null } + val extensionsWithUpdate = installedExtensions.filter { it.findUpdate(extensions) != null } if (extensionsWithUpdate.isNotEmpty()) { extensionUpdateNotifier.promptUpdates(extensionsWithUpdate.map { it.name }) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index ae09978da..8a26ef096 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -18,6 +18,7 @@ import logcat.LogPriority import mihon.app.di.appGraph import mihon.data.dalvik.DelegateLastClassLoaderCompat import mihon.domain.extension.model.ContentWarning +import mihon.domain.extension.model.ExtensionStore import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat import java.io.File @@ -122,6 +123,7 @@ internal object ExtensionLoader { ): List { val trustExtension = context.appGraph.trustExtension val sourcePreferences = context.appGraph.sourcePreferences + val stores = context.appGraph.extensionStoreRepository.getAll() val enabledContentWarnings = sourcePreferences.enabledContentWarnings.get() val applyContentWarningsToInstalled = sourcePreferences.applyContentWarningsToInstalled.get() @@ -178,6 +180,7 @@ internal object ExtensionLoader { context = context, extensionInfo = it, trustExtension = trustExtension, + stores = stores, enabledContentWarnings = enabledContentWarnings, applyContentWarningsToInstalled = applyContentWarningsToInstalled, alreadyLoaded = alreadyLoaded[it.packageInfo.packageName], @@ -204,6 +207,7 @@ internal object ExtensionLoader { context = context, extensionInfo = extensionPackage, trustExtension = context.appGraph.trustExtension, + stores = context.appGraph.extensionStoreRepository.getAll(), enabledContentWarnings = sourcePreferences.enabledContentWarnings.get(), applyContentWarningsToInstalled = sourcePreferences.applyContentWarningsToInstalled.get(), ) @@ -254,6 +258,7 @@ internal object ExtensionLoader { context: Context, extensionInfo: ExtensionInfo, trustExtension: TrustExtension, + stores: List, enabledContentWarnings: Set, applyContentWarningsToInstalled: Boolean, alreadyLoaded: Extension.Loaded? = null, @@ -263,6 +268,7 @@ internal object ExtensionLoader { context = context, extensionInfo = extensionInfo, trustExtension = trustExtension, + stores = stores, enabledContentWarnings = enabledContentWarnings, applyContentWarningsToInstalled = applyContentWarningsToInstalled, alreadyLoaded = alreadyLoaded, @@ -270,6 +276,7 @@ internal object ExtensionLoader { } catch (e: Throwable) { val pkgInfo = extensionInfo.packageInfo logcat(LogPriority.ERROR, e) { "Extension load error: ${pkgInfo.packageName}" } + val signatures = getSignatures(pkgInfo).orEmpty() Extension.NotLoaded( name = pkgInfo.packageName, pkgName = pkgInfo.packageName, @@ -277,7 +284,8 @@ internal object ExtensionLoader { versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo), isShared = extensionInfo.isShared, contentWarning = ContentWarning.SAFE, - signatures = getSignatures(pkgInfo).orEmpty(), + signatures = signatures, + store = stores.firstOrNull { it.signingKey in signatures }, reason = Extension.NotLoaded.Reason.Failed(e.rootMessage, e.stackTraceToString()), ) } @@ -293,6 +301,7 @@ internal object ExtensionLoader { context: Context, extensionInfo: ExtensionInfo, trustExtension: TrustExtension, + stores: List, enabledContentWarnings: Set, applyContentWarningsToInstalled: Boolean, alreadyLoaded: Extension.Loaded? = null, @@ -309,6 +318,7 @@ internal object ExtensionLoader { val versionName = pkgInfo.versionName val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo) val signatures = getSignatures(pkgInfo).orEmpty() + val store = stores.firstOrNull { it.signingKey in signatures } val contentWarning = when { metaData == null -> ContentWarning.SAFE metaData.containsKey(METADATA_CONTENT_WARNING) -> { @@ -333,6 +343,7 @@ internal object ExtensionLoader { isShared = extensionInfo.isShared, contentWarning = contentWarning, signatures = signatures, + store = store, libVersion = libVersion, reason = reason, ) @@ -379,7 +390,7 @@ internal object ExtensionLoader { alreadyLoaded.versionCode == versionCode && alreadyLoaded.isShared == extensionInfo.isShared ) { - return alreadyLoaded + return alreadyLoaded.copy(store = store) } val classLoader = try { @@ -441,6 +452,7 @@ internal object ExtensionLoader { icon = runCatching { appInfo.loadIcon(pkgManager) }.getOrNull(), isShared = extensionInfo.isShared, signatures = signatures, + store = store, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsViewModel.kt index 285e6e53f..e31546440 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsViewModel.kt @@ -168,7 +168,7 @@ class ExtensionsViewModel( viewModelScope.launchIO { state.value.items.values.flatten() .map { it.extension } - .filterIsInstance() + .filterIsInstance() .filter { it.hasUpdate } .forEach(::updateExtension) } @@ -180,7 +180,7 @@ class ExtensionsViewModel( } } - fun updateExtension(extension: Extension.Loaded) { + fun updateExtension(extension: Extension.Installed) { viewModelScope.launchIO { extensionManager.updateExtension(extension).collectToInstallUpdate(extension) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 168c374d6..7e179c905 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -342,7 +342,9 @@ class MainActivity : BaseActivity() { // Extensions updates LaunchedEffect(Unit) { try { - extensionApi.checkForUpdates(extensionManager.getLoadedExtensions()) + extensionApi.checkForUpdates( + extensionManager.getLoadedExtensions() + extensionManager.getNotLoadedExtensions(), + ) } catch (e: Exception) { logcat(LogPriority.ERROR, e) } diff --git a/app/src/main/java/mihon/app/di/AppGraph.kt b/app/src/main/java/mihon/app/di/AppGraph.kt index 7d3acdb6c..c1f0fbf75 100644 --- a/app/src/main/java/mihon/app/di/AppGraph.kt +++ b/app/src/main/java/mihon/app/di/AppGraph.kt @@ -44,6 +44,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.protobuf.ProtoBuf import mihon.core.metro.IsDebugBuild import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow +import mihon.domain.extension.repository.ExtensionStoreRepository import nl.adaptivity.xmlutil.serialization.XML import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.category.interactor.GetCategories @@ -100,6 +101,7 @@ interface AppGraph : ViewModelGraph { val updateChecker: AppUpdateChecker val trustExtension: TrustExtension + val extensionStoreRepository: ExtensionStoreRepository val sourceManager: SourceManager val trackerManager: TrackerManager diff --git a/domain/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt b/domain/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt index ad56c45e9..1f9b49b57 100644 --- a/domain/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt +++ b/domain/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt @@ -25,6 +25,10 @@ sealed interface Extension { /** SHA-256 digests of the certificates its apk is signed with. */ val signatures: List + val hasUpdate: Boolean + + val store: ExtensionStore? + /** * The newest listing of this extension among the stores whose signing key it's signed with. An * apk from any other store can't replace it, so only these are where its updates come from. @@ -92,9 +96,9 @@ sealed interface Extension { val pkgFactory: String?, val sources: List, val icon: Drawable?, - val hasUpdate: Boolean = false, + override val hasUpdate: Boolean = false, val isObsolete: Boolean = false, - val store: ExtensionStore? = null, + override val store: ExtensionStore? = null, ) : Installed /** @@ -111,6 +115,8 @@ sealed interface Extension { override val signatures: List, override val libVersion: Double? = null, override val lang: String? = null, + override val hasUpdate: Boolean = false, + override val store: ExtensionStore? = null, val reason: Reason, ) : Installed {