Expose installed extensions that failed to load (#3953)
This commit is contained in:
@@ -12,7 +12,7 @@ class GetExtensionSources(
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun subscribe(extension: Extension.Installed): Flow<List<ExtensionSourceItem>> {
|
||||
fun subscribe(extension: Extension.Loaded): Flow<List<ExtensionSourceItem>> {
|
||||
val isMultiSource = extension.sources.size > 1
|
||||
val isMultiLangSingleSource =
|
||||
isMultiSource && extension.sources.map { it.name }.distinct().size == 1
|
||||
|
||||
@@ -19,24 +19,24 @@ class GetExtensionsByType(
|
||||
|
||||
return combine(
|
||||
preferences.enabledLanguages.changes(),
|
||||
extensionManager.installedExtensionsFlow,
|
||||
extensionManager.untrustedExtensionsFlow,
|
||||
extensionManager.loadedExtensionsFlow,
|
||||
extensionManager.notLoadedExtensionsFlow,
|
||||
extensionManager.availableExtensionsFlow,
|
||||
) { enabledLanguages, _installed, _untrusted, _available ->
|
||||
val (updates, installed) = _installed
|
||||
) { enabledLanguages, _loaded, _notLoaded, _available ->
|
||||
val (updates, loaded) = _loaded
|
||||
.sortedWith(
|
||||
compareBy<Extension.Installed> { !it.isObsolete }
|
||||
compareBy<Extension.Loaded> { !it.isObsolete }
|
||||
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
||||
)
|
||||
.partition { it.hasUpdate }
|
||||
|
||||
val untrusted = _untrusted
|
||||
val notLoaded = _notLoaded
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
|
||||
val available = _available
|
||||
.filter { extension ->
|
||||
_installed.none { it.pkgName == extension.pkgName } &&
|
||||
_untrusted.none { it.pkgName == extension.pkgName } &&
|
||||
_loaded.none { it.pkgName == extension.pkgName } &&
|
||||
_notLoaded.none { it.pkgName == extension.pkgName } &&
|
||||
extension.contentWarning in enabledContentWarnings
|
||||
}
|
||||
.flatMap { ext ->
|
||||
@@ -52,7 +52,7 @@ class GetExtensionsByType(
|
||||
}
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
|
||||
Extensions(updates, installed, available, untrusted)
|
||||
Extensions(updates, loaded, available, notLoaded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package eu.kanade.domain.extension.model
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
|
||||
data class Extensions(
|
||||
val updates: List<Extension.Installed>,
|
||||
val installed: List<Extension.Installed>,
|
||||
val updates: List<Extension.Loaded>,
|
||||
val loaded: List<Extension.Loaded>,
|
||||
val available: List<Extension.Available>,
|
||||
val untrusted: List<Extension.Untrusted>,
|
||||
val notLoaded: List<Extension.NotLoaded>,
|
||||
)
|
||||
|
||||
@@ -145,7 +145,7 @@ fun ExtensionDetailsScreen(
|
||||
@Composable
|
||||
private fun ExtensionDetails(
|
||||
contentPadding: PaddingValues,
|
||||
extension: Extension.Installed,
|
||||
extension: Extension.Loaded,
|
||||
sources: List<ExtensionSourceItem>,
|
||||
incognitoMode: Boolean,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
@@ -239,7 +239,7 @@ private fun DetailsHeader(
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
if (extension is Extension.Installed) {
|
||||
if (extension is Extension.Loaded) {
|
||||
append("\n\n")
|
||||
appendLine(
|
||||
"""
|
||||
|
||||
@@ -31,6 +31,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
@@ -48,14 +49,17 @@ import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsViewModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||
import mihon.icons.materialsymbols.MaterialSymbols
|
||||
import mihon.icons.materialsymbols.rounded.Close
|
||||
import mihon.icons.materialsymbols.rounded.Download
|
||||
import mihon.icons.materialsymbols.rounded.Info
|
||||
import mihon.icons.materialsymbols.rounded.Public
|
||||
import mihon.icons.materialsymbols.rounded.Refresh
|
||||
import mihon.icons.materialsymbols.rounded.Settings
|
||||
import mihon.icons.materialsymbols.rounded.VerifiedUser
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||
@@ -78,10 +82,10 @@ fun ExtensionScreen(
|
||||
onClickItemCancel: (Extension) -> Unit,
|
||||
onOpenWebView: (Extension.Available) -> Unit,
|
||||
onInstallExtension: (Extension.Available) -> Unit,
|
||||
onUninstallExtension: (Extension) -> Unit,
|
||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
||||
onTrustExtension: (Extension.Untrusted) -> Unit,
|
||||
onOpenExtension: (Extension.Installed) -> Unit,
|
||||
onUninstallExtension: (Extension.Installed) -> Unit,
|
||||
onUpdateExtension: (Extension.Loaded) -> Unit,
|
||||
onTrustExtension: (Extension.NotLoaded) -> Unit,
|
||||
onOpenExtension: (Extension.Loaded) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
) {
|
||||
@@ -139,14 +143,14 @@ private fun ExtensionContent(
|
||||
onClickItemCancel: (Extension) -> Unit,
|
||||
onOpenWebView: (Extension.Available) -> Unit,
|
||||
onInstallExtension: (Extension.Available) -> Unit,
|
||||
onUninstallExtension: (Extension) -> Unit,
|
||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
||||
onTrustExtension: (Extension.Untrusted) -> Unit,
|
||||
onOpenExtension: (Extension.Installed) -> Unit,
|
||||
onUninstallExtension: (Extension.Installed) -> Unit,
|
||||
onUpdateExtension: (Extension.Loaded) -> Unit,
|
||||
onTrustExtension: (Extension.NotLoaded) -> Unit,
|
||||
onOpenExtension: (Extension.Loaded) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
|
||||
var notLoadedState by remember { mutableStateOf<Extension.NotLoaded?>(null) }
|
||||
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
|
||||
|
||||
FastScrollLazyColumn(
|
||||
@@ -205,8 +209,8 @@ private fun ExtensionContent(
|
||||
contentType = { "item" },
|
||||
key = { item ->
|
||||
when (item.extension) {
|
||||
is Extension.Untrusted -> "extension-untrusted-${item.hashCode()}"
|
||||
is Extension.Installed -> "extension-installed-${item.hashCode()}"
|
||||
is Extension.NotLoaded -> "extension-not-loaded-${item.hashCode()}"
|
||||
is Extension.Loaded -> "extension-loaded-${item.hashCode()}"
|
||||
is Extension.Available -> "extension-available-${item.hashCode()}"
|
||||
}
|
||||
},
|
||||
@@ -217,9 +221,9 @@ private fun ExtensionContent(
|
||||
onClickItem = {
|
||||
when (it) {
|
||||
is Extension.Available -> onInstallExtension(it)
|
||||
is Extension.Installed -> onOpenExtension(it)
|
||||
is Extension.Untrusted -> {
|
||||
trustState = it
|
||||
is Extension.Loaded -> onOpenExtension(it)
|
||||
is Extension.NotLoaded -> {
|
||||
notLoadedState = it
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -227,7 +231,7 @@ private fun ExtensionContent(
|
||||
onClickItemSecondaryAction = {
|
||||
when (it) {
|
||||
is Extension.Available -> onOpenWebView(it)
|
||||
is Extension.Installed -> onOpenExtension(it)
|
||||
is Extension.Loaded -> onOpenExtension(it)
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
@@ -235,15 +239,15 @@ private fun ExtensionContent(
|
||||
onClickItemAction = {
|
||||
when (it) {
|
||||
is Extension.Available -> onInstallExtension(it)
|
||||
is Extension.Installed -> {
|
||||
is Extension.Loaded -> {
|
||||
if (it.hasUpdate) {
|
||||
onUpdateExtension(it)
|
||||
} else {
|
||||
onOpenExtension(it)
|
||||
}
|
||||
}
|
||||
is Extension.Untrusted -> {
|
||||
trustState = it
|
||||
is Extension.NotLoaded -> {
|
||||
notLoadedState = it
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -251,20 +255,30 @@ private fun ExtensionContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (trustState != null) {
|
||||
ExtensionTrustDialog(
|
||||
onClickConfirm = {
|
||||
onTrustExtension(trustState!!)
|
||||
trustState = null
|
||||
},
|
||||
onClickDismiss = {
|
||||
onUninstallExtension(trustState!!)
|
||||
trustState = null
|
||||
},
|
||||
onDismissRequest = {
|
||||
trustState = null
|
||||
},
|
||||
)
|
||||
notLoadedState?.let { extension ->
|
||||
val dismiss = { notLoadedState = null }
|
||||
if (extension.reason is Extension.NotLoaded.Reason.Untrusted) {
|
||||
ExtensionTrustDialog(
|
||||
onClickConfirm = {
|
||||
onTrustExtension(extension)
|
||||
dismiss()
|
||||
},
|
||||
onClickDismiss = {
|
||||
onUninstallExtension(extension)
|
||||
dismiss()
|
||||
},
|
||||
onDismissRequest = dismiss,
|
||||
)
|
||||
} else {
|
||||
ExtensionNotLoadedDialog(
|
||||
reason = extension.reason,
|
||||
onClickUninstall = {
|
||||
onUninstallExtension(extension)
|
||||
dismiss()
|
||||
},
|
||||
onDismissRequest = dismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +367,7 @@ private fun ExtensionItemContent(
|
||||
) {
|
||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||
var hasAlreadyShownAnElement by remember { mutableStateOf(false) }
|
||||
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
|
||||
if (extension is Extension.Loaded && extension.lang.isNotEmpty()) {
|
||||
hasAlreadyShownAnElement = true
|
||||
Text(
|
||||
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current),
|
||||
@@ -370,9 +384,9 @@ private fun ExtensionItemContent(
|
||||
|
||||
val warnings = listOfNotNull(
|
||||
when {
|
||||
extension is Extension.Untrusted ->
|
||||
MR.strings.ext_untrusted to MaterialTheme.colorScheme.error
|
||||
extension is Extension.Installed && extension.isObsolete ->
|
||||
extension is Extension.NotLoaded ->
|
||||
extension.reason.labelRes?.let { it to MaterialTheme.colorScheme.error }
|
||||
extension is Extension.Loaded && extension.isObsolete ->
|
||||
MR.strings.ext_obsolete to MaterialTheme.colorScheme.error
|
||||
else -> null
|
||||
},
|
||||
@@ -389,7 +403,7 @@ private fun ExtensionItemContent(
|
||||
)
|
||||
}
|
||||
|
||||
if (extension is Extension.Installed && !extension.isShared) {
|
||||
if (extension is Extension.Loaded && !extension.isShared) {
|
||||
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
||||
Text(
|
||||
text = stringResource(MR.strings.ext_installer_private),
|
||||
@@ -446,7 +460,7 @@ private fun ExtensionItemActions(
|
||||
}
|
||||
installStep == InstallStep.Idle -> {
|
||||
when (extension) {
|
||||
is Extension.Installed -> {
|
||||
is Extension.Loaded -> {
|
||||
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = MaterialSymbols.Rounded.Settings,
|
||||
@@ -463,11 +477,20 @@ private fun ExtensionItemActions(
|
||||
}
|
||||
}
|
||||
}
|
||||
is Extension.Untrusted -> {
|
||||
is Extension.NotLoaded -> {
|
||||
val isUntrusted = extension.reason is Extension.NotLoaded.Reason.Untrusted
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = MaterialSymbols.Rounded.VerifiedUser,
|
||||
contentDescription = stringResource(MR.strings.ext_trust),
|
||||
imageVector = if (isUntrusted) {
|
||||
MaterialSymbols.Rounded.VerifiedUser
|
||||
} else {
|
||||
MaterialSymbols.Rounded.Info
|
||||
},
|
||||
contentDescription = if (isUntrusted) {
|
||||
stringResource(MR.strings.ext_trust)
|
||||
} else {
|
||||
stringResource(MR.strings.ext_not_loaded)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -530,6 +553,83 @@ private fun ExtensionHeader(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only the reasons the user can act on are worth naming in the row; the rest all mean "broken" to
|
||||
* them and are spelled out in [ExtensionNotLoadedDialog] instead.
|
||||
*/
|
||||
private val Extension.NotLoaded.Reason.labelRes: StringResource?
|
||||
get() = when (this) {
|
||||
is Extension.NotLoaded.Reason.Untrusted -> MR.strings.ext_untrusted
|
||||
Extension.NotLoaded.Reason.Filtered -> MR.strings.ext_filtered
|
||||
// The section header already says these aren't loaded; the dialog says why
|
||||
Extension.NotLoaded.Reason.Unsigned,
|
||||
Extension.NotLoaded.Reason.UnsupportedLibVersion,
|
||||
Extension.NotLoaded.Reason.Malformed,
|
||||
is Extension.NotLoaded.Reason.Failed,
|
||||
-> null
|
||||
}
|
||||
|
||||
private val Extension.NotLoaded.Reason.descriptionRes: StringResource
|
||||
get() = when (this) {
|
||||
is Extension.NotLoaded.Reason.Untrusted -> MR.strings.untrusted_extension_message
|
||||
Extension.NotLoaded.Reason.Filtered -> MR.strings.ext_filtered_message
|
||||
Extension.NotLoaded.Reason.Unsigned -> MR.strings.ext_unsigned_message
|
||||
Extension.NotLoaded.Reason.UnsupportedLibVersion -> MR.strings.ext_unsupported_message
|
||||
Extension.NotLoaded.Reason.Malformed -> MR.strings.ext_malformed_message
|
||||
is Extension.NotLoaded.Reason.Failed -> MR.strings.ext_load_failed_message
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionNotLoadedDialog(
|
||||
reason: Extension.NotLoaded.Reason,
|
||||
onClickUninstall: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.ext_not_loaded_dialog))
|
||||
},
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small)) {
|
||||
Text(text = stringResource(reason.descriptionRes))
|
||||
|
||||
if (reason is Extension.NotLoaded.Reason.Failed) {
|
||||
Text(
|
||||
text = reason.message,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
val context = LocalContext.current
|
||||
TextButton(
|
||||
onClick = {
|
||||
context.copyToClipboard(
|
||||
label = context.stringResource(MR.strings.ext_copy_stacktrace),
|
||||
content = reason.stackTrace,
|
||||
)
|
||||
},
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.ext_copy_stacktrace))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onClickUninstall) {
|
||||
Text(text = stringResource(MR.strings.ext_uninstall))
|
||||
}
|
||||
},
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionTrustDialog(
|
||||
onClickConfirm: () -> Unit,
|
||||
|
||||
@@ -96,7 +96,7 @@ fun ExtensionIcon(
|
||||
.clip(MaterialTheme.shapes.extraSmall),
|
||||
)
|
||||
}
|
||||
is Extension.Installed -> {
|
||||
is Extension.Loaded -> {
|
||||
val icon by extension.getIcon(density)
|
||||
when (icon) {
|
||||
Result.Loading -> Box(modifier = modifier)
|
||||
@@ -112,7 +112,7 @@ fun ExtensionIcon(
|
||||
)
|
||||
}
|
||||
}
|
||||
is Extension.Untrusted -> Image(
|
||||
is Extension.NotLoaded -> Image(
|
||||
imageVector = MaterialSymbols.Rounded.Dangerous,
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
|
||||
|
||||
@@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.extension.api.ExtensionApi
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionUpdateNotifier
|
||||
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
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
@@ -60,14 +59,14 @@ class ExtensionManager(
|
||||
|
||||
private val iconMap = mutableMapOf<String, Drawable>()
|
||||
|
||||
private val installedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Installed>())
|
||||
val installedExtensionsFlow = installedExtensionMapFlow.mapExtensionsWhenInitialized()
|
||||
private val loadedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Loaded>())
|
||||
val loadedExtensionsFlow = loadedExtensionMapFlow.mapExtensionsWhenInitialized()
|
||||
|
||||
private val availableExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Available>())
|
||||
val availableExtensionsFlow = availableExtensionMapFlow.mapExtensions(scope)
|
||||
|
||||
private val untrustedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Untrusted>())
|
||||
val untrustedExtensionsFlow = untrustedExtensionMapFlow.mapExtensionsWhenInitialized()
|
||||
private val notLoadedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.NotLoaded>())
|
||||
val notLoadedExtensionsFlow = notLoadedExtensionMapFlow.mapExtensionsWhenInitialized()
|
||||
|
||||
init {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
@@ -78,20 +77,25 @@ class ExtensionManager(
|
||||
|
||||
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages.isSet()
|
||||
|
||||
suspend fun getInstalledExtensions(): List<Extension.Installed> {
|
||||
suspend fun getLoadedExtensions(): List<Extension.Loaded> {
|
||||
initialized.await()
|
||||
return installedExtensionMapFlow.value.values.toList()
|
||||
return loadedExtensionMapFlow.value.values.toList()
|
||||
}
|
||||
|
||||
suspend fun getNotLoadedExtensions(): List<Extension.NotLoaded> {
|
||||
initialized.await()
|
||||
return notLoadedExtensionMapFlow.value.values.toList()
|
||||
}
|
||||
|
||||
suspend fun getExtensionPackage(sourceId: Long): String? {
|
||||
return getInstalledExtensions().find { extension ->
|
||||
return getLoadedExtensions().find { extension ->
|
||||
extension.sources.any { it.id == sourceId }
|
||||
}
|
||||
?.pkgName
|
||||
}
|
||||
|
||||
fun getExtensionPackageAsFlow(sourceId: Long): Flow<String?> {
|
||||
return installedExtensionsFlow.map { extensions ->
|
||||
return loadedExtensionsFlow.map { extensions ->
|
||||
extensions.find { extension ->
|
||||
extension.sources.any { it.id == sourceId }
|
||||
}
|
||||
@@ -126,13 +130,13 @@ class ExtensionManager(
|
||||
try {
|
||||
val extensions = ExtensionLoader.loadExtensions(context)
|
||||
|
||||
installedExtensionMapFlow.value = extensions
|
||||
.filterIsInstance<LoadResult.Success>()
|
||||
.associate { it.extension.pkgName to it.extension }
|
||||
loadedExtensionMapFlow.value = extensions
|
||||
.filterIsInstance<Extension.Loaded>()
|
||||
.associateBy { it.pkgName }
|
||||
|
||||
untrustedExtensionMapFlow.value = extensions
|
||||
.filterIsInstance<LoadResult.Untrusted>()
|
||||
.associate { it.extension.pkgName to it.extension }
|
||||
notLoadedExtensionMapFlow.value = extensions
|
||||
.filterIsInstance<Extension.NotLoaded>()
|
||||
.associateBy { it.pkgName }
|
||||
|
||||
initialized.complete(Unit)
|
||||
} catch (e: Throwable) {
|
||||
@@ -202,23 +206,23 @@ class ExtensionManager(
|
||||
return
|
||||
}
|
||||
|
||||
val installedExtensionsMap = installedExtensionMapFlow.value.toMutableMap()
|
||||
val loadedExtensionsMap = loadedExtensionMapFlow.value.toMutableMap()
|
||||
var changed = false
|
||||
for ((pkgName, extension) in installedExtensionsMap) {
|
||||
for ((pkgName, extension) in loadedExtensionsMap) {
|
||||
val availableExt = availableExtensions.find { it.pkgName == pkgName }
|
||||
|
||||
if (availableExt == null && !extension.isObsolete) {
|
||||
installedExtensionsMap[pkgName] = extension.copy(isObsolete = true)
|
||||
loadedExtensionsMap[pkgName] = extension.copy(isObsolete = true)
|
||||
changed = true
|
||||
} else if (availableExt != null) {
|
||||
val hasUpdate = extension.updateExists(availableExt)
|
||||
if (extension.hasUpdate != hasUpdate) {
|
||||
installedExtensionsMap[pkgName] = extension.copy(
|
||||
loadedExtensionsMap[pkgName] = extension.copy(
|
||||
hasUpdate = hasUpdate,
|
||||
store = availableExt.store,
|
||||
)
|
||||
} else {
|
||||
installedExtensionsMap[pkgName] = extension.copy(
|
||||
loadedExtensionsMap[pkgName] = extension.copy(
|
||||
store = availableExt.store,
|
||||
)
|
||||
}
|
||||
@@ -226,7 +230,7 @@ class ExtensionManager(
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
installedExtensionMapFlow.value = installedExtensionsMap
|
||||
loadedExtensionMapFlow.value = loadedExtensionsMap
|
||||
}
|
||||
updatePendingUpdatesCount()
|
||||
}
|
||||
@@ -249,7 +253,7 @@ class ExtensionManager(
|
||||
*
|
||||
* @param extension The extension to be updated.
|
||||
*/
|
||||
fun updateExtension(extension: Extension.Installed): Flow<InstallStep> {
|
||||
fun updateExtension(extension: Extension.Loaded): Flow<InstallStep> {
|
||||
val availableExt = availableExtensionMapFlow.value[extension.pkgName] ?: return emptyFlow()
|
||||
val isUpdateForPrivatelyInstalled = !extension.isShared
|
||||
return installer.downloadAndInstall(availableExt.apkUrl, availableExt, isUpdateForPrivatelyInstalled)
|
||||
@@ -277,7 +281,7 @@ class ExtensionManager(
|
||||
*
|
||||
* @param extension The extension to uninstall.
|
||||
*/
|
||||
fun uninstallExtension(extension: Extension) {
|
||||
fun uninstallExtension(extension: Extension.Installed) {
|
||||
installer.uninstallApk(extension.pkgName)
|
||||
}
|
||||
|
||||
@@ -287,16 +291,19 @@ class ExtensionManager(
|
||||
*
|
||||
* @param extension the extension to trust
|
||||
*/
|
||||
suspend fun trust(extension: Extension.Untrusted) {
|
||||
untrustedExtensionMapFlow.value[extension.pkgName] ?: return
|
||||
suspend fun trust(extension: Extension.NotLoaded) {
|
||||
val reason = extension.reason as? Extension.NotLoaded.Reason.Untrusted ?: return
|
||||
notLoadedExtensionMapFlow.value[extension.pkgName] ?: return
|
||||
|
||||
trustExtension.trust(extension.pkgName, extension.versionCode, extension.signatureHash)
|
||||
trustExtension.trust(extension.pkgName, extension.versionCode, reason.signatureHash)
|
||||
|
||||
untrustedExtensionMapFlow.value -= extension.pkgName
|
||||
notLoadedExtensionMapFlow.value -= extension.pkgName
|
||||
|
||||
ExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName)
|
||||
.let { it as? LoadResult.Success }
|
||||
?.let { registerNewExtension(it.extension) }
|
||||
when (val reloaded = ExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName)) {
|
||||
is Extension.Loaded -> registerExtension(reloaded)
|
||||
is Extension.NotLoaded -> notLoadedExtensionMapFlow.value += reloaded
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,18 +311,8 @@ class ExtensionManager(
|
||||
*
|
||||
* @param extension The extension to be registered.
|
||||
*/
|
||||
private fun registerNewExtension(extension: Extension.Installed) {
|
||||
installedExtensionMapFlow.value += extension
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given updated extension in this and the source managers previously removing
|
||||
* the outdated ones.
|
||||
*
|
||||
* @param extension The extension to be registered.
|
||||
*/
|
||||
private fun registerUpdatedExtension(extension: Extension.Installed) {
|
||||
installedExtensionMapFlow.value += extension
|
||||
private fun registerExtension(extension: Extension.Loaded) {
|
||||
loadedExtensionMapFlow.value += extension
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -325,8 +322,8 @@ class ExtensionManager(
|
||||
* @param pkgName The package name of the uninstalled application.
|
||||
*/
|
||||
private fun unregisterExtension(pkgName: String) {
|
||||
installedExtensionMapFlow.value -= pkgName
|
||||
untrustedExtensionMapFlow.value -= pkgName
|
||||
loadedExtensionMapFlow.value -= pkgName
|
||||
notLoadedExtensionMapFlow.value -= pkgName
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -334,19 +331,15 @@ class ExtensionManager(
|
||||
*/
|
||||
private inner class InstallationListener : ExtensionInstallReceiver.Listener {
|
||||
|
||||
override fun onExtensionInstalled(extension: Extension.Installed) {
|
||||
registerNewExtension(extension.withUpdateCheck())
|
||||
override fun onExtensionLoaded(extension: Extension.Loaded) {
|
||||
registerExtension(extension.withUpdateCheck())
|
||||
notLoadedExtensionMapFlow.value -= extension.pkgName
|
||||
updatePendingUpdatesCount()
|
||||
}
|
||||
|
||||
override fun onExtensionUpdated(extension: Extension.Installed) {
|
||||
registerUpdatedExtension(extension.withUpdateCheck())
|
||||
updatePendingUpdatesCount()
|
||||
}
|
||||
|
||||
override fun onExtensionUntrusted(extension: Extension.Untrusted) {
|
||||
installedExtensionMapFlow.value -= extension.pkgName
|
||||
untrustedExtensionMapFlow.value += extension
|
||||
override fun onExtensionNotLoaded(extension: Extension.NotLoaded) {
|
||||
loadedExtensionMapFlow.value -= extension.pkgName
|
||||
notLoadedExtensionMapFlow.value += extension
|
||||
updatePendingUpdatesCount()
|
||||
}
|
||||
|
||||
@@ -360,7 +353,7 @@ class ExtensionManager(
|
||||
/**
|
||||
* Extension method to set the update field of an installed extension.
|
||||
*/
|
||||
private fun Extension.Installed.withUpdateCheck(): Extension.Installed {
|
||||
private fun Extension.Loaded.withUpdateCheck(): Extension.Loaded {
|
||||
return if (updateExists()) {
|
||||
copy(hasUpdate = true)
|
||||
} else {
|
||||
@@ -368,7 +361,7 @@ class ExtensionManager(
|
||||
}
|
||||
}
|
||||
|
||||
private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean {
|
||||
private fun Extension.Loaded.updateExists(availableExtension: Extension.Available? = null): Boolean {
|
||||
val availableExt = availableExtension
|
||||
?: availableExtensionMapFlow.value[pkgName]
|
||||
?: return false
|
||||
@@ -377,7 +370,7 @@ class ExtensionManager(
|
||||
}
|
||||
|
||||
private fun updatePendingUpdatesCount() {
|
||||
val pendingUpdateCount = installedExtensionMapFlow.value.values.count { it.hasUpdate }
|
||||
val pendingUpdateCount = loadedExtensionMapFlow.value.values.count { it.hasUpdate }
|
||||
preferences.extensionUpdatesCount.set(pendingUpdateCount)
|
||||
if (pendingUpdateCount == 0) {
|
||||
extensionUpdateNotifier.dismiss()
|
||||
|
||||
@@ -5,7 +5,6 @@ import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import mihon.domain.extension.interactor.UpdateExtensionStores
|
||||
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||
@@ -28,12 +27,11 @@ class ExtensionApi(
|
||||
|
||||
val extensions = findExtensions()
|
||||
|
||||
val installedExtensions = ExtensionLoader.loadExtensions(context)
|
||||
.filterIsInstance<LoadResult.Success>()
|
||||
.map { it.extension }
|
||||
val loadedExtensions = ExtensionLoader.loadExtensions(context)
|
||||
.filterIsInstance<Extension.Loaded>()
|
||||
|
||||
val extensionsWithUpdate = mutableListOf<Extension.Installed>()
|
||||
for (installedExt in installedExtensions) {
|
||||
val extensionsWithUpdate = mutableListOf<Extension.Loaded>()
|
||||
for (installedExt in loadedExtensions) {
|
||||
val pkgName = installedExt.pkgName
|
||||
val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
|
||||
val hasUpdatedVer = availableExt.versionCode > installedExt.versionCode
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.model
|
||||
|
||||
sealed interface LoadResult {
|
||||
data class Success(val extension: Extension.Installed) : LoadResult
|
||||
data class Untrusted(val extension: Extension.Untrusted) : LoadResult
|
||||
data object Error : LoadResult
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -51,19 +50,19 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : Broadc
|
||||
if (isReplacing(intent)) return
|
||||
|
||||
scope.launch {
|
||||
when (val result = getExtensionFromIntent(context, intent)) {
|
||||
is LoadResult.Success -> listener.onExtensionInstalled(result.extension)
|
||||
is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
|
||||
else -> {}
|
||||
when (val extension = getExtensionFromIntent(context, intent)) {
|
||||
is Extension.Loaded -> listener.onExtensionLoaded(extension)
|
||||
is Extension.NotLoaded -> listener.onExtensionNotLoaded(extension)
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Intent.ACTION_PACKAGE_REPLACED, ACTION_EXTENSION_REPLACED -> {
|
||||
scope.launch {
|
||||
when (val result = getExtensionFromIntent(context, intent)) {
|
||||
is LoadResult.Success -> listener.onExtensionUpdated(result.extension)
|
||||
is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
|
||||
else -> {}
|
||||
when (val extension = getExtensionFromIntent(context, intent)) {
|
||||
is Extension.Loaded -> listener.onExtensionLoaded(extension)
|
||||
is Extension.NotLoaded -> listener.onExtensionNotLoaded(extension)
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,11 +92,11 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : Broadc
|
||||
* @param context The application context.
|
||||
* @param intent The intent containing the package name of the extension.
|
||||
*/
|
||||
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult {
|
||||
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): Extension.Installed? {
|
||||
val pkgName = getPackageNameFromIntent(intent)
|
||||
if (pkgName == null) {
|
||||
logcat(LogPriority.WARN) { "Package name not found" }
|
||||
return LoadResult.Error
|
||||
return null
|
||||
}
|
||||
return ExtensionLoader.loadExtensionFromPkgName(context, pkgName)
|
||||
}
|
||||
@@ -113,9 +112,8 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : Broadc
|
||||
* Listener that receives extension installation events.
|
||||
*/
|
||||
interface Listener {
|
||||
fun onExtensionInstalled(extension: Extension.Installed)
|
||||
fun onExtensionUpdated(extension: Extension.Installed)
|
||||
fun onExtensionUntrusted(extension: Extension.Untrusted)
|
||||
fun onExtensionLoaded(extension: Extension.Loaded)
|
||||
fun onExtensionNotLoaded(extension: Extension.NotLoaded)
|
||||
fun onPackageUninstalled(pkgName: String)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.os.Build
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import eu.kanade.domain.extension.interactor.TrustExtension
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import eu.kanade.tachiyomi.util.lang.Hash
|
||||
@@ -112,7 +111,7 @@ internal object ExtensionLoader {
|
||||
*
|
||||
* @param context The application context.
|
||||
*/
|
||||
fun loadExtensions(context: Context): List<LoadResult> {
|
||||
fun loadExtensions(context: Context): List<Extension.Installed> {
|
||||
val pkgManager = context.packageManager
|
||||
|
||||
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
@@ -138,7 +137,7 @@ internal object ExtensionLoader {
|
||||
|
||||
val path = it.absolutePath
|
||||
pkgManager.getPackageArchiveInfo(path, PACKAGE_FLAGS)
|
||||
?.apply { applicationInfo!!.fixBasePaths(path) }
|
||||
?.also { pkg -> pkg.applicationInfo?.fixBasePaths(path) }
|
||||
}
|
||||
?.filter { isPackageAnExtension(it) }
|
||||
?.map { ExtensionInfo(packageInfo = it, isShared = false) }
|
||||
@@ -160,7 +159,7 @@ internal object ExtensionLoader {
|
||||
// Load each extension concurrently and wait for completion
|
||||
return runBlocking(Dispatchers.IO) {
|
||||
val deferred = extPkgs.map {
|
||||
async { loadExtension(context, it) }
|
||||
async { loadExtensionCatching(context, it) }
|
||||
}
|
||||
deferred.awaitAll()
|
||||
}
|
||||
@@ -170,13 +169,13 @@ internal object ExtensionLoader {
|
||||
* Attempts to load an extension from the given package name. It checks if the extension
|
||||
* contains the required feature flag before trying to load it.
|
||||
*/
|
||||
suspend fun loadExtensionFromPkgName(context: Context, pkgName: String): LoadResult {
|
||||
suspend fun loadExtensionFromPkgName(context: Context, pkgName: String): Extension.Installed? {
|
||||
val extensionPackage = getExtensionInfoFromPkgName(context, pkgName)
|
||||
if (extensionPackage == null) {
|
||||
logcat(LogPriority.ERROR) { "Extension package is not found ($pkgName)" }
|
||||
return LoadResult.Error
|
||||
return null
|
||||
}
|
||||
return loadExtension(context, extensionPackage)
|
||||
return loadExtensionCatching(context, extensionPackage)
|
||||
}
|
||||
|
||||
fun getExtensionPackageInfoFromPkgName(context: Context, pkgName: String): PackageInfo? {
|
||||
@@ -189,7 +188,7 @@ internal object ExtensionLoader {
|
||||
context.packageManager.getPackageArchiveInfo(privateExtensionFile.absolutePath, PACKAGE_FLAGS)
|
||||
?.takeIf { isPackageAnExtension(it) }
|
||||
?.let {
|
||||
it.applicationInfo!!.fixBasePaths(privateExtensionFile.absolutePath)
|
||||
it.applicationInfo?.fixBasePaths(privateExtensionFile.absolutePath)
|
||||
ExtensionInfo(
|
||||
packageInfo = it,
|
||||
isShared = false,
|
||||
@@ -215,13 +214,36 @@ internal object ExtensionLoader {
|
||||
return selectExtensionPackage(sharedPkg, privatePkg)
|
||||
}
|
||||
|
||||
/**
|
||||
* [loadExtension] reports the failures it knows how to name, but an apk can be malformed in
|
||||
* ways it doesn't check for. Keep anything unforeseen to the extension that caused it instead of
|
||||
* letting it take down the load of every other extension.
|
||||
*/
|
||||
private suspend fun loadExtensionCatching(context: Context, extensionInfo: ExtensionInfo): Extension.Installed {
|
||||
return try {
|
||||
loadExtension(context, extensionInfo)
|
||||
} catch (e: Throwable) {
|
||||
val pkgInfo = extensionInfo.packageInfo
|
||||
logcat(LogPriority.ERROR, e) { "Extension load error: ${pkgInfo.packageName}" }
|
||||
Extension.NotLoaded(
|
||||
name = pkgInfo.packageName,
|
||||
pkgName = pkgInfo.packageName,
|
||||
versionName = pkgInfo.versionName.orEmpty(),
|
||||
versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo),
|
||||
isShared = extensionInfo.isShared,
|
||||
contentWarning = ContentWarning.SAFE,
|
||||
reason = Extension.NotLoaded.Reason.Failed(e.rootMessage, e.stackTraceToString()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an extension
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param extensionInfo The extension to load.
|
||||
*/
|
||||
private suspend fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult {
|
||||
private suspend fun loadExtension(context: Context, extensionInfo: ExtensionInfo): Extension.Installed {
|
||||
val trustExtension: TrustExtension = context.appGraph.trustExtension
|
||||
val sourcePreferences = context.appGraph.sourcePreferences
|
||||
val enabledContentWarnings = sourcePreferences.enabledContentWarnings.get()
|
||||
@@ -229,21 +251,54 @@ internal object ExtensionLoader {
|
||||
|
||||
val pkgManager = context.packageManager
|
||||
val pkgInfo = extensionInfo.packageInfo
|
||||
val appInfo = pkgInfo.applicationInfo!!
|
||||
val appInfo = pkgInfo.applicationInfo
|
||||
val metaData = appInfo?.metaData
|
||||
val pkgName = pkgInfo.packageName
|
||||
|
||||
val extName = appInfo.metaData.getString(METADATA_NAME)
|
||||
?: pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
|
||||
val extName = metaData?.getString(METADATA_NAME)
|
||||
?: appInfo?.let { pkgManager.getApplicationLabel(it).toString().substringAfter("Tachiyomi: ") }
|
||||
?: pkgName
|
||||
val versionName = pkgInfo.versionName
|
||||
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
|
||||
val contentWarning = when {
|
||||
metaData == null -> ContentWarning.SAFE
|
||||
metaData.containsKey(METADATA_CONTENT_WARNING) -> {
|
||||
when (metaData.getInt(METADATA_CONTENT_WARNING)) {
|
||||
1 -> ContentWarning.MIXED
|
||||
2 -> ContentWarning.NSFW
|
||||
else -> ContentWarning.SAFE
|
||||
}
|
||||
}
|
||||
metaData.getInt(METADATA_NSFW) == 1 -> ContentWarning.NSFW
|
||||
else -> ContentWarning.SAFE
|
||||
}
|
||||
|
||||
fun notLoaded(
|
||||
reason: Extension.NotLoaded.Reason,
|
||||
libVersion: Double? = null,
|
||||
) = Extension.NotLoaded(
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName.orEmpty(),
|
||||
versionCode = versionCode,
|
||||
isShared = extensionInfo.isShared,
|
||||
contentWarning = contentWarning,
|
||||
libVersion = libVersion,
|
||||
reason = reason,
|
||||
)
|
||||
|
||||
if (appInfo == null || metaData == null) {
|
||||
logcat(LogPriority.WARN) { "Missing application info for extension $extName" }
|
||||
return notLoaded(Extension.NotLoaded.Reason.Malformed)
|
||||
}
|
||||
|
||||
if (versionName.isNullOrEmpty()) {
|
||||
logcat(LogPriority.WARN) { "Missing versionName for extension $extName" }
|
||||
return LoadResult.Error
|
||||
return notLoaded(Extension.NotLoaded.Reason.Malformed)
|
||||
}
|
||||
|
||||
// Validate lib version
|
||||
val libVersion = appInfo.metaData.getFloat(METADATA_EXTENSION_LIB)
|
||||
val libVersion = metaData.getFloat(METADATA_EXTENSION_LIB)
|
||||
.takeUnless { it == 0.0f }
|
||||
?.toString()
|
||||
?.toDouble()
|
||||
@@ -252,50 +307,37 @@ internal object ExtensionLoader {
|
||||
logcat(LogPriority.WARN) {
|
||||
"Lib version is $libVersion, while only version(s) ${SUPPORTED_LIB_VERSIONS.joinToString()} are supported"
|
||||
}
|
||||
return LoadResult.Error
|
||||
return notLoaded(Extension.NotLoaded.Reason.UnsupportedLibVersion, libVersion)
|
||||
}
|
||||
|
||||
val signatures = getSignatures(pkgInfo)
|
||||
if (signatures.isNullOrEmpty()) {
|
||||
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
|
||||
return LoadResult.Error
|
||||
return notLoaded(Extension.NotLoaded.Reason.Unsigned, libVersion)
|
||||
} else if (!trustExtension.isTrusted(pkgInfo, signatures)) {
|
||||
val extension = Extension.Untrusted(
|
||||
extName,
|
||||
pkgName,
|
||||
versionName,
|
||||
versionCode,
|
||||
libVersion,
|
||||
signatures.last(),
|
||||
)
|
||||
logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" }
|
||||
return LoadResult.Untrusted(extension)
|
||||
return notLoaded(Extension.NotLoaded.Reason.Untrusted(signatures.last()), libVersion)
|
||||
}
|
||||
|
||||
val contentWarning = when {
|
||||
appInfo.metaData.containsKey(METADATA_CONTENT_WARNING) -> {
|
||||
when (appInfo.metaData.getInt(METADATA_CONTENT_WARNING)) {
|
||||
1 -> ContentWarning.MIXED
|
||||
2 -> ContentWarning.NSFW
|
||||
else -> ContentWarning.SAFE
|
||||
}
|
||||
}
|
||||
appInfo.metaData.getInt(METADATA_NSFW) == 1 -> ContentWarning.NSFW
|
||||
else -> ContentWarning.SAFE
|
||||
}
|
||||
if (applyContentWarningsToInstalled && contentWarning !in enabledContentWarnings) {
|
||||
logcat(LogPriority.WARN) { "Extension $pkgName with $contentWarning not allowed" }
|
||||
return LoadResult.Error
|
||||
return notLoaded(Extension.NotLoaded.Reason.Filtered, libVersion)
|
||||
}
|
||||
|
||||
val classLoader = try {
|
||||
DelegateLastClassLoaderCompat(appInfo.sourceDir, null, context.classLoader)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($pkgName)" }
|
||||
return LoadResult.Error
|
||||
return notLoaded(Extension.NotLoaded.Reason.Failed(e.rootMessage, e.stackTraceToString()), libVersion)
|
||||
}
|
||||
|
||||
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
||||
val sourceClasses = metaData.getString(METADATA_SOURCE_CLASS)
|
||||
if (sourceClasses.isNullOrBlank()) {
|
||||
logcat(LogPriority.WARN) { "Missing source class for extension $extName" }
|
||||
return notLoaded(Extension.NotLoaded.Reason.Malformed, libVersion)
|
||||
}
|
||||
|
||||
val sources = sourceClasses
|
||||
.split(";")
|
||||
.map {
|
||||
val sourceClass = it.trim()
|
||||
@@ -314,7 +356,10 @@ internal object ExtensionLoader {
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($it)" }
|
||||
return LoadResult.Error
|
||||
return notLoaded(
|
||||
Extension.NotLoaded.Reason.Failed(e.rootMessage, e.stackTraceToString()),
|
||||
libVersion,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +370,7 @@ internal object ExtensionLoader {
|
||||
else -> "all"
|
||||
}
|
||||
|
||||
val extension = Extension.Installed(
|
||||
return Extension.Loaded(
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName,
|
||||
@@ -334,11 +379,10 @@ internal object ExtensionLoader {
|
||||
lang = lang,
|
||||
contentWarning = contentWarning,
|
||||
sources = sources,
|
||||
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
||||
icon = appInfo.loadIcon(pkgManager),
|
||||
pkgFactory = metaData.getString(METADATA_SOURCE_FACTORY),
|
||||
icon = runCatching { appInfo.loadIcon(pkgManager) }.getOrNull(),
|
||||
isShared = extensionInfo.isShared,
|
||||
)
|
||||
return LoadResult.Success(extension)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -380,11 +424,11 @@ internal object ExtensionLoader {
|
||||
*/
|
||||
private fun getSignatures(pkgInfo: PackageInfo): List<String>? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val signingInfo = pkgInfo.signingInfo!!
|
||||
if (signingInfo.hasMultipleSigners()) {
|
||||
signingInfo.apkContentsSigners
|
||||
} else {
|
||||
signingInfo.signingCertificateHistory
|
||||
val signingInfo = pkgInfo.signingInfo
|
||||
when {
|
||||
signingInfo == null -> null
|
||||
signingInfo.hasMultipleSigners() -> signingInfo.apkContentsSigners
|
||||
else -> signingInfo.signingCertificateHistory
|
||||
}
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
@@ -412,3 +456,12 @@ internal object ExtensionLoader {
|
||||
val isShared: Boolean,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The message of the deepest cause, which is the one that actually says what went wrong.
|
||||
*/
|
||||
private val Throwable.rootMessage: String
|
||||
get() {
|
||||
val root = generateSequence(this) { it.cause }.last()
|
||||
return listOfNotNull(root::class.simpleName, root.message).joinToString(": ")
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class AndroidSourceManager(
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
extensionManager.installedExtensionsFlow
|
||||
extensionManager.loadedExtensionsFlow
|
||||
.collectLatest { extensions ->
|
||||
val mutableMap = ConcurrentHashMap<Long, Source>(
|
||||
mapOf(LocalSource.ID to localSource),
|
||||
|
||||
@@ -32,7 +32,7 @@ fun extensionsTab(
|
||||
val context = LocalContext.current
|
||||
|
||||
val updatesCount by extensionsViewModel.updatesCount.collectAsStateWithLifecycle()
|
||||
var privateExtensionToUninstall by remember { mutableStateOf<Extension?>(null) }
|
||||
var privateExtensionToUninstall by remember { mutableStateOf<Extension.Installed?>(null) }
|
||||
|
||||
return TabContent(
|
||||
titleRes = MR.strings.label_extensions,
|
||||
@@ -62,7 +62,7 @@ fun extensionsTab(
|
||||
onLongClickItem = { extension ->
|
||||
when (extension) {
|
||||
is Extension.Available -> extensionsViewModel.installExtension(extension)
|
||||
else -> {
|
||||
is Extension.Installed -> {
|
||||
if (context.isPackageInstalled(extension.pkgName)) {
|
||||
extensionsViewModel.uninstallExtension(extension)
|
||||
} else {
|
||||
|
||||
@@ -74,17 +74,21 @@ class ExtensionsViewModel(
|
||||
.map { searchQueryPredicate(it ?: "") },
|
||||
currentDownloads,
|
||||
getExtensions.subscribe(),
|
||||
) { predicate, downloads, (_updates, _installed, _available, _untrusted) ->
|
||||
) { predicate, downloads, (_updates, _loaded, _available, _notLoaded) ->
|
||||
buildMap {
|
||||
val updates = _updates.filter(predicate).map(extensionMapper(downloads))
|
||||
if (updates.isNotEmpty()) {
|
||||
put(ExtensionUiModel.Header.Resource(MR.strings.ext_updates_pending), updates)
|
||||
}
|
||||
|
||||
val installed = _installed.filter(predicate).map(extensionMapper(downloads))
|
||||
val untrusted = _untrusted.filter(predicate).map(extensionMapper(downloads))
|
||||
if (installed.isNotEmpty() || untrusted.isNotEmpty()) {
|
||||
put(ExtensionUiModel.Header.Resource(MR.strings.ext_installed), installed + untrusted)
|
||||
val notLoaded = _notLoaded.filter(predicate).map(extensionMapper(downloads))
|
||||
if (notLoaded.isNotEmpty()) {
|
||||
put(ExtensionUiModel.Header.Resource(MR.strings.ext_not_loaded), notLoaded)
|
||||
}
|
||||
|
||||
val loaded = _loaded.filter(predicate).map(extensionMapper(downloads))
|
||||
if (loaded.isNotEmpty()) {
|
||||
put(ExtensionUiModel.Header.Resource(MR.strings.ext_installed), loaded)
|
||||
}
|
||||
|
||||
val languagesWithExtensions = _available
|
||||
@@ -138,7 +142,7 @@ class ExtensionsViewModel(
|
||||
if (extension.name.contains(subquery, ignoreCase = true)) return@any true
|
||||
|
||||
when (extension) {
|
||||
is Extension.Installed -> extension.sources.any { source ->
|
||||
is Extension.Loaded -> extension.sources.any { source ->
|
||||
source.name.contains(subquery, ignoreCase = true) ||
|
||||
(source as? HttpSource)?.getHomeUrl()?.contains(subquery, ignoreCase = true) == true ||
|
||||
source.id == subquery.toLongOrNull()
|
||||
@@ -164,7 +168,7 @@ class ExtensionsViewModel(
|
||||
viewModelScope.launchIO {
|
||||
state.value.items.values.flatten()
|
||||
.map { it.extension }
|
||||
.filterIsInstance<Extension.Installed>()
|
||||
.filterIsInstance<Extension.Loaded>()
|
||||
.filter { it.hasUpdate }
|
||||
.forEach(::updateExtension)
|
||||
}
|
||||
@@ -176,7 +180,7 @@ class ExtensionsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateExtension(extension: Extension.Installed) {
|
||||
fun updateExtension(extension: Extension.Loaded) {
|
||||
viewModelScope.launchIO {
|
||||
extensionManager.updateExtension(extension).collectToInstallUpdate(extension)
|
||||
}
|
||||
@@ -202,7 +206,7 @@ class ExtensionsViewModel(
|
||||
.onCompletion { removeDownloadState(extension) }
|
||||
.collect()
|
||||
|
||||
fun uninstallExtension(extension: Extension) {
|
||||
fun uninstallExtension(extension: Extension.Installed) {
|
||||
extensionManager.uninstallExtension(extension)
|
||||
}
|
||||
|
||||
@@ -219,7 +223,7 @@ class ExtensionsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun trustExtension(extension: Extension.Untrusted) {
|
||||
fun trustExtension(extension: Extension.NotLoaded) {
|
||||
viewModelScope.launch {
|
||||
extensionManager.trust(extension)
|
||||
}
|
||||
|
||||
+3
-3
@@ -56,7 +56,7 @@ class ExtensionDetailsViewModel(
|
||||
fun create(pkgName: String): ExtensionDetailsViewModel
|
||||
}
|
||||
|
||||
val state: StateFlow<State> = extensionManager.installedExtensionsFlow
|
||||
val state: StateFlow<State> = extensionManager.loadedExtensionsFlow
|
||||
.map { it.firstOrNull { extension -> extension.pkgName == pkgName } }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { extension ->
|
||||
@@ -73,7 +73,7 @@ class ExtensionDetailsViewModel(
|
||||
private val successState: State.Success?
|
||||
get() = state.value as? State.Success
|
||||
|
||||
private fun subscribeToSources(extension: Extension.Installed): Flow<List<ExtensionSourceItem>> {
|
||||
private fun subscribeToSources(extension: Extension.Loaded): Flow<List<ExtensionSourceItem>> {
|
||||
return getExtensionSources.subscribe(extension)
|
||||
.map {
|
||||
it.sortedWith(
|
||||
@@ -142,7 +142,7 @@ class ExtensionDetailsViewModel(
|
||||
|
||||
@Immutable
|
||||
data class Success(
|
||||
val extension: Extension.Installed,
|
||||
val extension: Extension.Loaded,
|
||||
val isIncognito: Boolean,
|
||||
val sources: List<ExtensionSourceItem>,
|
||||
) : State
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@ abstract class SearchViewModel(
|
||||
return enabledSources
|
||||
}
|
||||
|
||||
return extensionManager.getInstalledExtensions()
|
||||
return extensionManager.getLoadedExtensions()
|
||||
.filter { it.pkgName == filter }
|
||||
.flatMap { it.sources }
|
||||
.filter { it in enabledSources }
|
||||
|
||||
@@ -6,6 +6,7 @@ import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
@@ -65,7 +66,7 @@ class CrashLogUtil(
|
||||
private suspend fun getExtensionsInfo(): String? {
|
||||
val availableExtensions = extensionManager.availableExtensionsFlow.value.associateBy { it.pkgName }
|
||||
|
||||
val extensionInfoList = extensionManager.getInstalledExtensions()
|
||||
val outdatedInfoList = extensionManager.getLoadedExtensions()
|
||||
.sortedBy { it.name }
|
||||
.mapNotNull {
|
||||
val availableExtension = availableExtensions[it.pkgName]
|
||||
@@ -80,6 +81,24 @@ class CrashLogUtil(
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
val notLoadedInfoList = extensionManager.getNotLoadedExtensions()
|
||||
.sortedBy { it.name }
|
||||
.map { extension ->
|
||||
buildString {
|
||||
appendLine("- ${extension.name}")
|
||||
appendLine(" Installed: ${extension.versionName} (lib ${extension.libVersion ?: "?"})")
|
||||
append(" Not loaded: ${extension.reason.description}")
|
||||
|
||||
val reason = extension.reason
|
||||
if (reason is Extension.NotLoaded.Reason.Failed) {
|
||||
appendLine()
|
||||
append(reason.stackTrace.trimEnd().prependIndent(" "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val extensionInfoList = outdatedInfoList + notLoadedInfoList
|
||||
|
||||
return if (extensionInfoList.isNotEmpty()) {
|
||||
(listOf("Problematic extensions:") + extensionInfoList)
|
||||
.joinToString("\n")
|
||||
@@ -88,3 +107,13 @@ class CrashLogUtil(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Extension.NotLoaded.Reason.description: String
|
||||
get() = when (this) {
|
||||
is Extension.NotLoaded.Reason.Untrusted -> "Untrusted"
|
||||
Extension.NotLoaded.Reason.Filtered -> "Filtered by content warning"
|
||||
Extension.NotLoaded.Reason.Unsigned -> "Unsigned"
|
||||
Extension.NotLoaded.Reason.UnsupportedLibVersion -> "Unsupported lib version"
|
||||
Extension.NotLoaded.Reason.Malformed -> "Malformed"
|
||||
is Extension.NotLoaded.Reason.Failed -> "Failed ($message)"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user