komikku/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt

432 lines
16 KiB
Kotlin
Raw Normal View History

package eu.kanade.tachiyomi.extension
import android.content.Context
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.util.system.toast
import exh.log.xLogD
2019-04-14 18:48:59 +02:00
import exh.source.BlacklistedSources
import exh.source.EH_SOURCE_ID
import exh.source.EXH_SOURCE_ID
import exh.source.MERGED_SOURCE_ID
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import logcat.LogPriority
import tachiyomi.core.preference.plusAssign
Move more things around (cherry picked from commit 1730dd6af1f5a9e6f8752a2350b1bd153262a751) # Conflicts: # app/src/main/java/eu/kanade/data/source/SourcePagingSource.kt # app/src/main/java/eu/kanade/domain/chapter/interactor/SetReadStatus.kt # app/src/main/java/eu/kanade/domain/library/service/LibraryPreferences.kt # app/src/main/java/eu/kanade/domain/manga/interactor/GetManga.kt # app/src/main/java/eu/kanade/presentation/more/settings/PreferenceItem.kt # app/src/main/java/eu/kanade/presentation/more/settings/screen/AboutScreen.kt # app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAdvancedScreen.kt # app/src/main/java/eu/kanade/tachiyomi/App.kt # app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt # app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt # app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt # app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt # app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt # app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateDialog.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcesScreenModel.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreenModel.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryScreenModel.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryTab.kt # app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt # app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt # app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt # app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt # app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt # app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewerAdapter.kt # app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt # app/src/main/java/eu/kanade/tachiyomi/util/CrashLogUtil.kt # data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt
2023-01-28 04:31:12 +01:00
import tachiyomi.core.util.lang.launchNow
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.source.model.StubSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
/**
* The manager of extensions installed as another apk which extend the available sources. It handles
* the retrieval of remotely available extensions as well as installing, updating and removing them.
* To avoid malicious distribution, every extension must be signed and it will only be loaded if its
* signature is trusted, otherwise the user will be prompted with a warning to trust it before being
* loaded.
*
* @param context The application context.
* @param preferences The application preferences.
*/
class ExtensionManager(
2020-02-27 00:03:34 +01:00
private val context: Context,
private val preferences: SourcePreferences = Injekt.get(),
) {
var isInitialized = false
private set
/**
* API where all the available extensions can be found.
*/
private val api = ExtensionGithubApi()
/**
* The installer which installs, updates and uninstalls the extensions.
*/
private val installer by lazy { ExtensionInstaller(context) }
2020-04-04 03:39:55 +02:00
private val iconMap = mutableMapOf<String, Drawable>()
private val _installedExtensionsFlow = MutableStateFlow(emptyList<Extension.Installed>())
val installedExtensionsFlow = _installedExtensionsFlow.asStateFlow()
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages().isSet()
fun getAppIconForSource(sourceId: Long): Drawable? {
val pkgName = _installedExtensionsFlow.value.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
2020-04-04 03:39:55 +02:00
if (pkgName != null) {
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) }
}
2020-05-22 23:22:24 +02:00
// SY -->
return when (sourceId) {
EH_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_ehentai_source)
2023-05-23 01:38:02 +02:00
EXH_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_exhentai_source)
MERGED_SOURCE_ID -> ContextCompat.getDrawable(context, R.mipmap.ic_merged_source)
else -> null
}
// SY <--
}
private val _availableExtensionsFlow = MutableStateFlow(emptyList<Extension.Available>())
// SY -->
@OptIn(DelicateCoroutinesApi::class)
val availableExtensionsFlow = _availableExtensionsFlow
.map { it.filterNotBlacklisted() }
.stateIn(GlobalScope, SharingStarted.Eagerly, emptyList())
// SY <--
private var availableExtensionsSourcesData: Map<Long, StubSource> = emptyMap()
private fun setupAvailableExtensionsSourcesDataMap(extensions: List<Extension.Available>) {
if (extensions.isEmpty()) return
availableExtensionsSourcesData = extensions
.flatMap { ext -> ext.sources.map { it.toStubSource() } }
.associateBy { it.id }
}
fun getSourceData(id: Long) = availableExtensionsSourcesData[id]
private val _untrustedExtensionsFlow = MutableStateFlow(emptyList<Extension.Untrusted>())
val untrustedExtensionsFlow = _untrustedExtensionsFlow.asStateFlow()
init {
initExtensions()
ExtensionInstallReceiver(InstallationListener()).register(context)
}
/**
* Loads and registers the installed extensions.
*/
private fun initExtensions() {
val extensions = ExtensionLoader.loadExtensions(context)
_installedExtensionsFlow.value = extensions
2020-04-25 20:24:45 +02:00
.filterIsInstance<LoadResult.Success>()
.map { it.extension }
_untrustedExtensionsFlow.value = extensions
2020-04-25 20:24:45 +02:00
.filterIsInstance<LoadResult.Untrusted>()
.map { it.extension }
// SY -->
2020-01-06 17:43:11 +01:00
.filterNotBlacklisted()
// SY <--
isInitialized = true
}
2019-04-14 18:48:59 +02:00
// EXH -->
2020-12-26 20:22:55 +01:00
private fun <T : Extension> Iterable<T>.filterNotBlacklisted(): List<T> {
2020-11-30 20:35:31 +01:00
val blacklistEnabled = preferences.enableSourceBlacklist().get()
2021-03-12 04:39:46 +01:00
return filterNot { extension ->
extension.isBlacklisted(blacklistEnabled)
.also {
2021-03-31 20:29:27 +02:00
if (it) this@ExtensionManager.xLogD("Removing blacklisted extension: (name: %s, pkgName: %s)!", extension.name, extension.pkgName)
2021-03-12 04:39:46 +01:00
}
2019-04-14 18:48:59 +02:00
}
}
2021-03-10 23:20:42 +01:00
private fun Extension.isBlacklisted(blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get()): Boolean {
2019-04-14 18:48:59 +02:00
return pkgName in BlacklistedSources.BLACKLISTED_EXTENSIONS && blacklistEnabled
}
// EXH <--
/**
* Finds the available extensions in the [api] and updates [availableExtensions].
*/
suspend fun findAvailableExtensions() {
val extensions: List<Extension.Available> = try {
api.findExtensions()
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
withUIContext { context.toast(R.string.extension_api_error) }
emptyList()
}
enableAdditionalSubLanguages(extensions)
_availableExtensionsFlow.value = extensions
updatedInstalledExtensionsStatuses(extensions)
setupAvailableExtensionsSourcesDataMap(extensions)
}
/**
* Enables the additional sub-languages in the app first run. This addresses
* the issue where users still need to enable some specific languages even when
* the device language is inside that major group. As an example, if a user
* has a zh device language, the app will also enable zh-Hans and zh-Hant.
*
* If the user have already changed the enabledLanguages preference value once,
* the new languages will not be added to respect the user enabled choices.
*/
private fun enableAdditionalSubLanguages(extensions: List<Extension.Available>) {
if (subLanguagesEnabledOnFirstRun || extensions.isEmpty()) {
return
}
// Use the source lang as some aren't present on the extension level.
val availableLanguages = extensions
.flatMap(Extension.Available::sources)
.distinctBy(Extension.Available.Source::lang)
.map(Extension.Available.Source::lang)
val deviceLanguage = Locale.getDefault().language
val defaultLanguages = preferences.enabledLanguages().defaultValue()
val languagesToEnable = availableLanguages.filter {
it != deviceLanguage && it.startsWith(deviceLanguage)
}
preferences.enabledLanguages().set(defaultLanguages + languagesToEnable)
subLanguagesEnabledOnFirstRun = true
}
/**
* Sets the update field of the installed extensions with the given [availableExtensions].
*
* @param availableExtensions The list of extensions given by the [api].
*/
private fun updatedInstalledExtensionsStatuses(availableExtensions: List<Extension.Available>) {
if (availableExtensions.isEmpty()) {
preferences.extensionUpdatesCount().set(0)
return
}
val mutInstalledExtensions = _installedExtensionsFlow.value.toMutableList()
var changed = false
for ((index, installedExt) in mutInstalledExtensions.withIndex()) {
val pkgName = installedExt.pkgName
// SY -->
val availableExt = _availableExtensionsFlow.value.find { it.pkgName == pkgName }
// SY <--
if (!installedExt.isUnofficial && availableExt == null && !installedExt.isObsolete) {
mutInstalledExtensions[index] = installedExt.copy(isObsolete = true)
changed = true
// SY -->
} else if (installedExt.isBlacklisted() && !installedExt.isRedundant) {
mutInstalledExtensions[index] = installedExt.copy(isRedundant = true)
changed = true
// SY <--
} else if (availableExt != null) {
// SY -->
val hasUpdate = installedExt.updateExists(availableExt)
if (installedExt.hasUpdate != hasUpdate) {
mutInstalledExtensions[index] = installedExt.copy(hasUpdate = hasUpdate, isRepoSource = availableExt.isRepoSource, repoUrl = availableExt.repoUrl)
changed = true
} else if (availableExt.isRepoSource) {
mutInstalledExtensions[index] = installedExt.copy(isRepoSource = true, repoUrl = availableExt.repoUrl)
changed = true
}
// SY <--
}
}
if (changed) {
_installedExtensionsFlow.value = mutInstalledExtensions
}
updatePendingUpdatesCount()
}
/**
* Returns an observable of the installation process for the given extension. It will complete
* once the extension is installed or throws an error. The process will be canceled if
* unsubscribed before its completion.
*
* @param extension The extension to be installed.
*/
fun installExtension(extension: Extension.Available): Flow<InstallStep> {
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
}
/**
* Returns an observable of the installation process for the given extension. It will complete
* once the extension is updated or throws an error. The process will be canceled if
* unsubscribed before its completion.
*
* @param extension The extension to be updated.
*/
fun updateExtension(extension: Extension.Installed): Flow<InstallStep> {
val availableExt = _availableExtensionsFlow.value.find { it.pkgName == extension.pkgName }
?: return emptyFlow()
return installExtension(availableExt)
}
fun cancelInstallUpdateExtension(extension: Extension) {
installer.cancelInstall(extension.pkgName)
}
/**
* Sets to "installing" status of an extension installation.
*
* @param downloadId The id of the download.
*/
fun setInstalling(downloadId: Long) {
installer.updateInstallStep(downloadId, InstallStep.Installing)
}
fun updateInstallStep(downloadId: Long, step: InstallStep) {
installer.updateInstallStep(downloadId, step)
}
/**
* Uninstalls the extension that matches the given package name.
*
* @param extension The extension to uninstall.
*/
fun uninstallExtension(extension: Extension) {
installer.uninstallApk(extension.pkgName)
}
/**
* Adds the given signature to the list of trusted signatures. It also loads in background the
* extensions that match this signature.
*
* @param signature The signature to whitelist.
*/
fun trustSignature(signature: String) {
val untrustedSignatures = _untrustedExtensionsFlow.value.map { it.signatureHash }.toSet()
if (signature !in untrustedSignatures) return
ExtensionLoader.trustedSignatures += signature
preferences.trustedSignatures() += signature
val nowTrustedExtensions = _untrustedExtensionsFlow.value.filter { it.signatureHash == signature }
_untrustedExtensionsFlow.value -= nowTrustedExtensions
launchNow {
nowTrustedExtensions
2020-04-25 20:24:45 +02:00
.map { extension ->
async { ExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName) }.await()
2020-04-25 20:24:45 +02:00
}
.filterIsInstance<LoadResult.Success>()
.forEach { registerNewExtension(it.extension) }
}
}
/**
* Registers the given extension in this and the source managers.
*
* @param extension The extension to be registered.
*/
private fun registerNewExtension(extension: Extension.Installed) {
// SY -->
if (extension.isBlacklisted()) {
xLogD("Removing blacklisted extension: (name: String, pkgName: %s)!", extension.name, extension.pkgName)
2019-04-14 18:48:59 +02:00
return
}
// SY <--
2019-04-14 18:48:59 +02:00
_installedExtensionsFlow.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) {
// SY -->
if (extension.isBlacklisted()) {
xLogD("Removing blacklisted extension: (name: %s, pkgName: %s)!", extension.name, extension.pkgName)
2019-04-14 18:48:59 +02:00
return
}
// SY <--
2019-04-14 18:48:59 +02:00
val mutInstalledExtensions = _installedExtensionsFlow.value.toMutableList()
val oldExtension = mutInstalledExtensions.find { it.pkgName == extension.pkgName }
if (oldExtension != null) {
mutInstalledExtensions -= oldExtension
}
mutInstalledExtensions += extension
_installedExtensionsFlow.value = mutInstalledExtensions
}
/**
* Unregisters the extension in this and the source managers given its package name. Note this
* method is called for every uninstalled application in the system.
*
* @param pkgName The package name of the uninstalled application.
*/
private fun unregisterExtension(pkgName: String) {
val installedExtension = _installedExtensionsFlow.value.find { it.pkgName == pkgName }
if (installedExtension != null) {
_installedExtensionsFlow.value -= installedExtension
}
val untrustedExtension = _untrustedExtensionsFlow.value.find { it.pkgName == pkgName }
if (untrustedExtension != null) {
_untrustedExtensionsFlow.value -= untrustedExtension
}
}
/**
* Listener which receives events of the extensions being installed, updated or removed.
*/
private inner class InstallationListener : ExtensionInstallReceiver.Listener {
override fun onExtensionInstalled(extension: Extension.Installed) {
registerNewExtension(extension.withUpdateCheck())
updatePendingUpdatesCount()
}
override fun onExtensionUpdated(extension: Extension.Installed) {
registerUpdatedExtension(extension.withUpdateCheck())
updatePendingUpdatesCount()
}
override fun onExtensionUntrusted(extension: Extension.Untrusted) {
_untrustedExtensionsFlow.value += extension
}
override fun onPackageUninstalled(pkgName: String) {
unregisterExtension(pkgName)
updatePendingUpdatesCount()
}
}
/**
* Extension method to set the update field of an installed extension.
*/
private fun Extension.Installed.withUpdateCheck(): Extension.Installed {
return if (updateExists()) {
copy(hasUpdate = true)
} else {
this
}
}
private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean {
val availableExt = availableExtension ?: _availableExtensionsFlow.value.find { it.pkgName == pkgName }
if ((isUnofficial && availableExt?.isRepoSource != true) || availableExt == null) return false
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
}
private fun updatePendingUpdatesCount() {
preferences.extensionUpdatesCount().set(_installedExtensionsFlow.value.count { it.hasUpdate })
}
}