mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-16 13:48:09 -07:00
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.
This commit is contained in:
@@ -23,14 +23,18 @@ class GetExtensionsByType(
|
||||
extensionManager.notLoadedExtensionsFlow,
|
||||
extensionManager.availableExtensionsFlow,
|
||||
) { enabledLanguages, _loaded, _notLoaded, _available ->
|
||||
val (updates, loaded) = _loaded
|
||||
val (loadedUpdates, loaded) = _loaded
|
||||
.sortedWith(
|
||||
compareBy<Extension.Loaded> { !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
|
||||
|
||||
@@ -3,7 +3,7 @@ package eu.kanade.domain.extension.model
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
|
||||
data class Extensions(
|
||||
val updates: List<Extension.Loaded>,
|
||||
val updates: List<Extension.Installed>,
|
||||
val loaded: List<Extension.Loaded>,
|
||||
val available: List<Extension.Available>,
|
||||
val notLoaded: List<Extension.NotLoaded>,
|
||||
|
||||
@@ -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,9 +249,13 @@ private fun ExtensionContent(
|
||||
}
|
||||
}
|
||||
is Extension.NotLoaded -> {
|
||||
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()) {
|
||||
|
||||
@@ -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<InstallStep> {
|
||||
fun updateExtension(extension: Extension.Installed): Flow<InstallStep> {
|
||||
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.Available>): 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()
|
||||
|
||||
@@ -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<Extension.Loaded>) {
|
||||
suspend fun checkForUpdates(installedExtensions: List<Extension.Installed>) {
|
||||
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 })
|
||||
|
||||
@@ -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<Extension.Installed> {
|
||||
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<ExtensionStore>,
|
||||
enabledContentWarnings: Set<ContentWarning>,
|
||||
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<ExtensionStore>,
|
||||
enabledContentWarnings: Set<ContentWarning>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ class ExtensionsViewModel(
|
||||
viewModelScope.launchIO {
|
||||
state.value.items.values.flatten()
|
||||
.map { it.extension }
|
||||
.filterIsInstance<Extension.Loaded>()
|
||||
.filterIsInstance<Extension.Installed>()
|
||||
.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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,6 +25,10 @@ sealed interface Extension {
|
||||
/** SHA-256 digests of the certificates its apk is signed with. */
|
||||
val signatures: List<String>
|
||||
|
||||
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<Source>,
|
||||
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<String>,
|
||||
override val libVersion: Double? = null,
|
||||
override val lang: String? = null,
|
||||
override val hasUpdate: Boolean = false,
|
||||
override val store: ExtensionStore? = null,
|
||||
val reason: Reason,
|
||||
) : Installed {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user