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 {