package eu.kanade.tachiyomi.extension import android.content.Context import android.graphics.drawable.Drawable import com.jakewharton.rxrelay.BehaviorRelay import eu.kanade.domain.source.model.SourceData import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.source.Source import eu.kanade.tachiyomi.util.lang.launchNow import eu.kanade.tachiyomi.util.preference.plusAssign import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.toast import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import logcat.LogPriority import rx.Observable import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get /** * 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( private val context: Context, private val preferences: PreferencesHelper = Injekt.get(), ) { /** * 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) } /** * Relay used to notify the installed extensions. */ private val installedExtensionsRelay = BehaviorRelay.create>() private val iconMap = mutableMapOf() /** * List of the currently installed extensions. */ var installedExtensions = emptyList() private set(value) { field = value installedExtensionsFlow.value = field installedExtensionsRelay.call(value) } private val installedExtensionsFlow = MutableStateFlow(installedExtensions) fun getInstalledExtensionsFlow(): StateFlow> { return installedExtensionsFlow.asStateFlow() } fun getAppIconForSource(source: Source): Drawable? { return getAppIconForSource(source.id) } fun getAppIconForSource(sourceId: Long): Drawable? { val pkgName = installedExtensions.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName if (pkgName != null) { return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) } } return null } /** * List of the currently available extensions. */ var availableExtensions = emptyList() private set(value) { field = value availableExtensionsFlow.value = field updatedInstalledExtensionsStatuses(value) setupAvailableExtensionsSourcesDataMap(value) } private val availableExtensionsFlow = MutableStateFlow(availableExtensions) fun getAvailableExtensionsFlow(): StateFlow> { return availableExtensionsFlow.asStateFlow() } private var availableExtensionsSourcesData: Map = mapOf() private fun setupAvailableExtensionsSourcesDataMap(extensions: List) { if (extensions.isEmpty()) return availableExtensionsSourcesData = extensions .flatMap { ext -> ext.sources.map { it.toSourceData() } } .associateBy { it.id } } fun getSourceData(id: Long) = availableExtensionsSourcesData[id] /** * List of the currently untrusted extensions. */ var untrustedExtensions = emptyList() private set(value) { field = value untrustedExtensionsFlow.value = field } private val untrustedExtensionsFlow = MutableStateFlow(untrustedExtensions) fun getUntrustedExtensionsFlow(): StateFlow> { return untrustedExtensionsFlow.asStateFlow() } init { initExtensions() ExtensionInstallReceiver(InstallationListener()).register(context) } /** * Loads and registers the installed extensions. */ private fun initExtensions() { val extensions = ExtensionLoader.loadExtensions(context) installedExtensions = extensions .filterIsInstance() .map { it.extension } untrustedExtensions = extensions .filterIsInstance() .map { it.extension } } /** * Finds the available extensions in the [api] and updates [availableExtensions]. */ fun findAvailableExtensions() { launchNow { val extensions: List = try { api.findExtensions() } catch (e: Exception) { logcat(LogPriority.ERROR, e) context.toast(R.string.extension_api_error) emptyList() } availableExtensions = extensions } } /** * 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) { if (availableExtensions.isEmpty()) { preferences.extensionUpdatesCount().set(0) return } val mutInstalledExtensions = installedExtensions.toMutableList() var changed = false for ((index, installedExt) in mutInstalledExtensions.withIndex()) { val pkgName = installedExt.pkgName val availableExt = availableExtensions.find { it.pkgName == pkgName } if (availableExt == null && !installedExt.isObsolete) { mutInstalledExtensions[index] = installedExt.copy(isObsolete = true) changed = true } else if (availableExt != null) { val hasUpdate = !installedExt.isUnofficial && availableExt.versionCode > installedExt.versionCode if (installedExt.hasUpdate != hasUpdate) { mutInstalledExtensions[index] = installedExt.copy(hasUpdate = hasUpdate) changed = true } } } if (changed) { installedExtensions = 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): Observable { 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): Observable { val availableExt = availableExtensions.find { it.pkgName == extension.pkgName } ?: return Observable.empty() 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 pkgName The package name of the application to uninstall. */ fun uninstallExtension(pkgName: String) { installer.uninstallApk(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 = untrustedExtensions.map { it.signatureHash }.toSet() if (signature !in untrustedSignatures) return ExtensionLoader.trustedSignatures += signature preferences.trustedSignatures() += signature val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature } untrustedExtensions -= nowTrustedExtensions val ctx = context launchNow { nowTrustedExtensions .map { extension -> async { ExtensionLoader.loadExtensionFromPkgName(ctx, extension.pkgName) } } .map { it.await() } .forEach { result -> if (result is LoadResult.Success) { registerNewExtension(result.extension) } } } } /** * Registers the given extension in this and the source managers. * * @param extension The extension to be registered. */ private fun registerNewExtension(extension: Extension.Installed) { installedExtensions += 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) { val mutInstalledExtensions = installedExtensions.toMutableList() val oldExtension = mutInstalledExtensions.find { it.pkgName == extension.pkgName } if (oldExtension != null) { mutInstalledExtensions -= oldExtension } mutInstalledExtensions += extension installedExtensions = 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 = installedExtensions.find { it.pkgName == pkgName } if (installedExtension != null) { installedExtensions -= installedExtension } val untrustedExtension = untrustedExtensions.find { it.pkgName == pkgName } if (untrustedExtension != null) { untrustedExtensions -= 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) { untrustedExtensions += 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 { val availableExt = availableExtensions.find { it.pkgName == pkgName } if (isUnofficial.not() && availableExt != null && availableExt.versionCode > versionCode) { return copy(hasUpdate = true) } return this } private fun updatePendingUpdatesCount() { preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate }) } }