2018-02-05 16:50:56 -05:00
|
|
|
package eu.kanade.tachiyomi.extension
|
|
|
|
|
|
|
|
import android.content.Context
|
2020-03-20 01:52:03 -04:00
|
|
|
import android.graphics.drawable.Drawable
|
2021-01-07 18:34:38 -05:00
|
|
|
import com.jakewharton.rxrelay.BehaviorRelay
|
2018-02-05 16:50:56 -05:00
|
|
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
2020-07-25 18:07:10 -04:00
|
|
|
import eu.kanade.tachiyomi.data.preference.plusAssign
|
2018-02-05 16:50:56 -05:00
|
|
|
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
|
2020-03-20 01:52:03 -04:00
|
|
|
import eu.kanade.tachiyomi.source.Source
|
2018-02-05 16:50:56 -05:00
|
|
|
import eu.kanade.tachiyomi.source.SourceManager
|
2020-02-02 22:22:54 -05:00
|
|
|
import eu.kanade.tachiyomi.util.lang.launchNow
|
2020-04-03 22:54:52 -04:00
|
|
|
import eu.kanade.tachiyomi.util.system.toast
|
2019-09-18 22:45:54 -04:00
|
|
|
import kotlinx.coroutines.async
|
2018-02-05 16:50:56 -05:00
|
|
|
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(
|
2020-02-26 18:03:34 -05:00
|
|
|
private val context: Context,
|
|
|
|
private val preferences: PreferencesHelper = Injekt.get()
|
2018-02-05 16:50:56 -05:00
|
|
|
) {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2021-01-07 18:34:38 -05:00
|
|
|
private val installedExtensionsRelay = BehaviorRelay.create<List<Extension.Installed>>()
|
2018-02-05 16:50:56 -05:00
|
|
|
|
2020-04-03 21:39:55 -04:00
|
|
|
private val iconMap = mutableMapOf<String, Drawable>()
|
|
|
|
|
2018-02-05 16:50:56 -05:00
|
|
|
/**
|
|
|
|
* List of the currently installed extensions.
|
|
|
|
*/
|
|
|
|
var installedExtensions = emptyList<Extension.Installed>()
|
|
|
|
private set(value) {
|
|
|
|
field = value
|
2021-01-07 18:34:38 -05:00
|
|
|
installedExtensionsRelay.call(value)
|
2018-02-05 16:50:56 -05:00
|
|
|
}
|
|
|
|
|
2020-03-20 01:52:03 -04:00
|
|
|
fun getAppIconForSource(source: Source): Drawable? {
|
|
|
|
val pkgName = installedExtensions.find { ext -> ext.sources.any { it.id == source.id } }?.pkgName
|
2020-04-03 21:39:55 -04:00
|
|
|
if (pkgName != null) {
|
|
|
|
return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) }
|
|
|
|
}
|
|
|
|
return null
|
2020-03-20 01:52:03 -04:00
|
|
|
}
|
|
|
|
|
2018-02-05 16:50:56 -05:00
|
|
|
/**
|
|
|
|
* Relay used to notify the available extensions.
|
|
|
|
*/
|
2021-01-07 18:34:38 -05:00
|
|
|
private val availableExtensionsRelay = BehaviorRelay.create<List<Extension.Available>>()
|
2018-02-05 16:50:56 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* List of the currently available extensions.
|
|
|
|
*/
|
|
|
|
var availableExtensions = emptyList<Extension.Available>()
|
|
|
|
private set(value) {
|
|
|
|
field = value
|
2021-01-07 18:34:38 -05:00
|
|
|
availableExtensionsRelay.call(value)
|
2020-01-12 18:27:04 -05:00
|
|
|
updatedInstalledExtensionsStatuses(value)
|
2018-02-05 16:50:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Relay used to notify the untrusted extensions.
|
|
|
|
*/
|
2021-01-07 18:34:38 -05:00
|
|
|
private val untrustedExtensionsRelay = BehaviorRelay.create<List<Extension.Untrusted>>()
|
2018-02-05 16:50:56 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* List of the currently untrusted extensions.
|
|
|
|
*/
|
|
|
|
var untrustedExtensions = emptyList<Extension.Untrusted>()
|
|
|
|
private set(value) {
|
|
|
|
field = value
|
2021-01-07 18:34:38 -05:00
|
|
|
untrustedExtensionsRelay.call(value)
|
2018-02-05 16:50:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The source manager where the sources of the extensions are added.
|
|
|
|
*/
|
|
|
|
private lateinit var sourceManager: SourceManager
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initializes this manager with the given source manager.
|
|
|
|
*/
|
|
|
|
fun init(sourceManager: SourceManager) {
|
|
|
|
this.sourceManager = sourceManager
|
|
|
|
initExtensions()
|
|
|
|
ExtensionInstallReceiver(InstallationListener()).register(context)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Loads and registers the installed extensions.
|
|
|
|
*/
|
|
|
|
private fun initExtensions() {
|
|
|
|
val extensions = ExtensionLoader.loadExtensions(context)
|
|
|
|
|
|
|
|
installedExtensions = extensions
|
2020-04-25 14:24:45 -04:00
|
|
|
.filterIsInstance<LoadResult.Success>()
|
|
|
|
.map { it.extension }
|
2018-02-05 16:50:56 -05:00
|
|
|
installedExtensions
|
2020-04-25 14:24:45 -04:00
|
|
|
.flatMap { it.sources }
|
2020-12-12 16:15:18 -05:00
|
|
|
.forEach { sourceManager.registerSource(it) }
|
2018-02-05 16:50:56 -05:00
|
|
|
|
|
|
|
untrustedExtensions = extensions
|
2020-04-25 14:24:45 -04:00
|
|
|
.filterIsInstance<LoadResult.Untrusted>()
|
|
|
|
.map { it.extension }
|
2018-02-05 16:50:56 -05:00
|
|
|
}
|
|
|
|
|
2021-01-07 18:34:38 -05:00
|
|
|
/**
|
|
|
|
* Returns the relay of the installed extensions as an observable.
|
|
|
|
*/
|
|
|
|
fun getInstalledExtensionsObservable(): Observable<List<Extension.Installed>> {
|
|
|
|
return installedExtensionsRelay.asObservable()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the relay of the available extensions as an observable.
|
|
|
|
*/
|
|
|
|
fun getAvailableExtensionsObservable(): Observable<List<Extension.Available>> {
|
|
|
|
return availableExtensionsRelay.asObservable()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the relay of the untrusted extensions as an observable.
|
|
|
|
*/
|
|
|
|
fun getUntrustedExtensionsObservable(): Observable<List<Extension.Untrusted>> {
|
|
|
|
return untrustedExtensionsRelay.asObservable()
|
|
|
|
}
|
|
|
|
|
2018-02-05 16:50:56 -05:00
|
|
|
/**
|
|
|
|
* Finds the available extensions in the [api] and updates [availableExtensions].
|
|
|
|
*/
|
|
|
|
fun findAvailableExtensions() {
|
2020-02-14 09:23:54 -05:00
|
|
|
launchNow {
|
2020-02-03 19:11:18 -05:00
|
|
|
val extensions: List<Extension.Available> = try {
|
2020-02-02 22:57:15 -05:00
|
|
|
api.findExtensions()
|
|
|
|
} catch (e: Exception) {
|
2020-04-03 22:54:52 -04:00
|
|
|
context.toast(e.message)
|
2020-02-02 22:57:15 -05:00
|
|
|
emptyList()
|
|
|
|
}
|
2020-02-03 19:11:18 -05:00
|
|
|
|
2020-02-14 09:23:54 -05:00
|
|
|
availableExtensions = extensions
|
2020-02-02 22:57:15 -05:00
|
|
|
}
|
2018-02-05 16:50:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the update field of the installed extensions with the given [availableExtensions].
|
|
|
|
*
|
|
|
|
* @param availableExtensions The list of extensions given by the [api].
|
|
|
|
*/
|
2020-01-12 18:27:04 -05:00
|
|
|
private fun updatedInstalledExtensionsStatuses(availableExtensions: List<Extension.Available>) {
|
2020-01-13 21:30:20 -05:00
|
|
|
if (availableExtensions.isEmpty()) {
|
2020-03-20 22:22:39 -04:00
|
|
|
preferences.extensionUpdatesCount().set(0)
|
2020-01-13 21:30:20 -05:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-02-05 16:50:56 -05:00
|
|
|
val mutInstalledExtensions = installedExtensions.toMutableList()
|
|
|
|
var changed = false
|
|
|
|
|
|
|
|
for ((index, installedExt) in mutInstalledExtensions.withIndex()) {
|
|
|
|
val pkgName = installedExt.pkgName
|
2020-01-12 18:27:04 -05:00
|
|
|
val availableExt = availableExtensions.find { it.pkgName == pkgName }
|
|
|
|
|
2020-01-13 21:30:20 -05:00
|
|
|
if (availableExt == null && !installedExt.isObsolete) {
|
|
|
|
mutInstalledExtensions[index] = installedExt.copy(isObsolete = true)
|
|
|
|
changed = true
|
|
|
|
} else if (availableExt != null) {
|
2020-01-12 18:27:04 -05:00
|
|
|
val hasUpdate = availableExt.versionCode > installedExt.versionCode
|
|
|
|
if (installedExt.hasUpdate != hasUpdate) {
|
|
|
|
mutInstalledExtensions[index] = installedExt.copy(hasUpdate = hasUpdate)
|
|
|
|
changed = true
|
|
|
|
}
|
2018-02-05 16:50:56 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (changed) {
|
|
|
|
installedExtensions = mutInstalledExtensions
|
|
|
|
}
|
2020-03-20 22:53:24 -04:00
|
|
|
updatePendingUpdatesCount()
|
2018-02-05 16:50:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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<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.
|
|
|
|
*/
|
2020-02-17 17:23:37 -05:00
|
|
|
fun updateExtension(extension: Extension.Installed): Observable<InstallStep> {
|
2018-02-05 16:50:56 -05:00
|
|
|
val availableExt = availableExtensions.find { it.pkgName == extension.pkgName }
|
2020-04-25 14:24:45 -04:00
|
|
|
?: return Observable.empty()
|
2018-02-05 16:50:56 -05:00
|
|
|
return installExtension(availableExt)
|
|
|
|
}
|
|
|
|
|
2018-03-02 12:10:10 -05:00
|
|
|
/**
|
|
|
|
* Sets the result of the installation of an extension.
|
|
|
|
*
|
|
|
|
* @param downloadId The id of the download.
|
|
|
|
* @param result Whether the extension was installed or not.
|
|
|
|
*/
|
|
|
|
fun setInstallationResult(downloadId: Long, result: Boolean) {
|
|
|
|
installer.setInstallationResult(downloadId, result)
|
|
|
|
}
|
|
|
|
|
2018-02-05 16:50:56 -05:00
|
|
|
/**
|
|
|
|
* 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
|
2020-07-25 18:07:10 -04:00
|
|
|
preferences.trustedSignatures() += signature
|
2018-02-05 16:50:56 -05:00
|
|
|
|
|
|
|
val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature }
|
|
|
|
untrustedExtensions -= nowTrustedExtensions
|
|
|
|
|
|
|
|
val ctx = context
|
|
|
|
launchNow {
|
|
|
|
nowTrustedExtensions
|
2020-04-25 14:24:45 -04:00
|
|
|
.map { extension ->
|
|
|
|
async { ExtensionLoader.loadExtensionFromPkgName(ctx, extension.pkgName) }
|
|
|
|
}
|
|
|
|
.map { it.await() }
|
|
|
|
.forEach { result ->
|
|
|
|
if (result is LoadResult.Success) {
|
|
|
|
registerNewExtension(result.extension)
|
2018-02-05 16:50:56 -05:00
|
|
|
}
|
2020-04-25 14:24:45 -04:00
|
|
|
}
|
2018-02-05 16:50:56 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
extension.sources.forEach { sourceManager.registerSource(it) }
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
extension.sources.forEach { sourceManager.unregisterSource(it) }
|
|
|
|
}
|
|
|
|
mutInstalledExtensions += extension
|
|
|
|
installedExtensions = mutInstalledExtensions
|
|
|
|
extension.sources.forEach { sourceManager.registerSource(it) }
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
installedExtension.sources.forEach { sourceManager.unregisterSource(it) }
|
|
|
|
}
|
|
|
|
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())
|
2020-03-20 22:53:24 -04:00
|
|
|
updatePendingUpdatesCount()
|
2018-02-05 16:50:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onExtensionUpdated(extension: Extension.Installed) {
|
|
|
|
registerUpdatedExtension(extension.withUpdateCheck())
|
2020-03-20 22:53:24 -04:00
|
|
|
updatePendingUpdatesCount()
|
2018-02-05 16:50:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onExtensionUntrusted(extension: Extension.Untrusted) {
|
|
|
|
untrustedExtensions += extension
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onPackageUninstalled(pkgName: String) {
|
|
|
|
unregisterExtension(pkgName)
|
2020-03-20 22:53:24 -04:00
|
|
|
updatePendingUpdatesCount()
|
2018-02-05 16:50:56 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 (availableExt != null && availableExt.versionCode > versionCode) {
|
|
|
|
return copy(hasUpdate = true)
|
|
|
|
}
|
|
|
|
return this
|
|
|
|
}
|
2020-03-20 22:53:24 -04:00
|
|
|
|
|
|
|
private fun updatePendingUpdatesCount() {
|
|
|
|
preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate })
|
|
|
|
}
|
2018-02-05 16:50:56 -05:00
|
|
|
}
|