mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	Downloading extensions from Github Repo. (#1101)
Downloading extensions from Github Repo.
This commit is contained in:
		@@ -8,33 +8,49 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.DownloadManager
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.track.TrackManager
 | 
			
		||||
import eu.kanade.tachiyomi.extension.ExtensionManager
 | 
			
		||||
import eu.kanade.tachiyomi.network.NetworkHelper
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceManager
 | 
			
		||||
import uy.kohesive.injekt.api.InjektModule
 | 
			
		||||
import uy.kohesive.injekt.api.InjektRegistrar
 | 
			
		||||
import uy.kohesive.injekt.api.addSingletonFactory
 | 
			
		||||
import kotlinx.coroutines.experimental.async
 | 
			
		||||
import uy.kohesive.injekt.api.*
 | 
			
		||||
 | 
			
		||||
class AppModule(val app: Application) : InjektModule {
 | 
			
		||||
 | 
			
		||||
    override fun InjektRegistrar.registerInjectables() {
 | 
			
		||||
 | 
			
		||||
            addSingletonFactory { PreferencesHelper(app) }
 | 
			
		||||
        addSingleton(app)
 | 
			
		||||
 | 
			
		||||
            addSingletonFactory { DatabaseHelper(app) }
 | 
			
		||||
        addSingletonFactory { PreferencesHelper(app) }
 | 
			
		||||
 | 
			
		||||
            addSingletonFactory { ChapterCache(app) }
 | 
			
		||||
        addSingletonFactory { DatabaseHelper(app) }
 | 
			
		||||
 | 
			
		||||
            addSingletonFactory { CoverCache(app) }
 | 
			
		||||
        addSingletonFactory { ChapterCache(app) }
 | 
			
		||||
 | 
			
		||||
            addSingletonFactory { NetworkHelper(app) }
 | 
			
		||||
        addSingletonFactory { CoverCache(app) }
 | 
			
		||||
 | 
			
		||||
            addSingletonFactory { SourceManager(app) }
 | 
			
		||||
        addSingletonFactory { NetworkHelper(app) }
 | 
			
		||||
 | 
			
		||||
            addSingletonFactory { DownloadManager(app) }
 | 
			
		||||
        addSingletonFactory { SourceManager(app).also { get<ExtensionManager>().init(it) } }
 | 
			
		||||
 | 
			
		||||
            addSingletonFactory { TrackManager(app) }
 | 
			
		||||
        addSingletonFactory { ExtensionManager(app) }
 | 
			
		||||
 | 
			
		||||
            addSingletonFactory { Gson() }
 | 
			
		||||
        addSingletonFactory { DownloadManager(app) }
 | 
			
		||||
 | 
			
		||||
        addSingletonFactory { TrackManager(app) }
 | 
			
		||||
 | 
			
		||||
        addSingletonFactory { Gson() }
 | 
			
		||||
 | 
			
		||||
        // Asynchronously init expensive components for a faster cold start
 | 
			
		||||
 | 
			
		||||
        async { get<PreferencesHelper>() }
 | 
			
		||||
 | 
			
		||||
        async { get<NetworkHelper>() }
 | 
			
		||||
 | 
			
		||||
        async { get<SourceManager>() }
 | 
			
		||||
 | 
			
		||||
        async { get<DatabaseHelper>() }
 | 
			
		||||
 | 
			
		||||
        async { get<DownloadManager>() }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,48 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.preference
 | 
			
		||||
 | 
			
		||||
import android.support.v7.preference.PreferenceDataStore
 | 
			
		||||
 | 
			
		||||
class EmptyPreferenceDataStore : PreferenceDataStore() {
 | 
			
		||||
 | 
			
		||||
    override fun getBoolean(key: String?, defValue: Boolean): Boolean {
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putBoolean(key: String?, value: Boolean) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getInt(key: String?, defValue: Int): Int {
 | 
			
		||||
        return 0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putInt(key: String?, value: Int) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getLong(key: String?, defValue: Long): Long {
 | 
			
		||||
        return 0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putLong(key: String?, value: Long) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getFloat(key: String?, defValue: Float): Float {
 | 
			
		||||
        return 0f
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putFloat(key: String?, value: Float) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getString(key: String?, defValue: String?): String? {
 | 
			
		||||
        return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putString(key: String?, value: String?) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getStringSet(key: String?, defValues: Set<String>?): Set<String>? {
 | 
			
		||||
        return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putStringSet(key: String?, values: Set<String>?) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -109,10 +109,14 @@ object PreferenceKeys {
 | 
			
		||||
 | 
			
		||||
    const val downloadBadge = "display_download_badge"
 | 
			
		||||
 | 
			
		||||
    @Deprecated("Use the preferences of the source")
 | 
			
		||||
    fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
 | 
			
		||||
 | 
			
		||||
    @Deprecated("Use the preferences of the source")
 | 
			
		||||
    fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"
 | 
			
		||||
 | 
			
		||||
    fun sourceSharedPref(sourceId: Long) = "source_$sourceId"
 | 
			
		||||
 | 
			
		||||
    fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
 | 
			
		||||
 | 
			
		||||
    fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
 | 
			
		||||
 
 | 
			
		||||
@@ -169,4 +169,5 @@ class PreferencesHelper(val context: Context) {
 | 
			
		||||
 | 
			
		||||
    fun migrateFlags() = rxPrefs.getInteger("migrate_flags", Int.MAX_VALUE)
 | 
			
		||||
 | 
			
		||||
    fun trustedSignatures() = rxPrefs.getStringSet("trusted_signatures", emptySet())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,55 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.preference
 | 
			
		||||
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
import android.support.v7.preference.PreferenceDataStore
 | 
			
		||||
 | 
			
		||||
class SharedPreferencesDataStore(private val prefs: SharedPreferences) : PreferenceDataStore() {
 | 
			
		||||
 | 
			
		||||
    override fun getBoolean(key: String?, defValue: Boolean): Boolean {
 | 
			
		||||
        return prefs.getBoolean(key, defValue)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putBoolean(key: String?, value: Boolean) {
 | 
			
		||||
        prefs.edit().putBoolean(key, value).apply()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getInt(key: String?, defValue: Int): Int {
 | 
			
		||||
        return prefs.getInt(key, defValue)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putInt(key: String?, value: Int) {
 | 
			
		||||
        prefs.edit().putInt(key, value).apply()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getLong(key: String?, defValue: Long): Long {
 | 
			
		||||
        return prefs.getLong(key, defValue)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putLong(key: String?, value: Long) {
 | 
			
		||||
        prefs.edit().putLong(key, value).apply()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getFloat(key: String?, defValue: Float): Float {
 | 
			
		||||
        return prefs.getFloat(key, defValue)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putFloat(key: String?, value: Float) {
 | 
			
		||||
        prefs.edit().putFloat(key, value).apply()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getString(key: String?, defValue: String?): String? {
 | 
			
		||||
        return prefs.getString(key, defValue)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putString(key: String?, value: String?) {
 | 
			
		||||
        prefs.edit().putString(key, value).apply()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String> {
 | 
			
		||||
        return prefs.getStringSet(key, defValues)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun putStringSet(key: String?, values: MutableSet<String>?) {
 | 
			
		||||
        prefs.edit().putStringSet(key, values).apply()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,330 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import com.jakewharton.rxrelay.BehaviorRelay
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
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.SourceManager
 | 
			
		||||
import eu.kanade.tachiyomi.util.launchNow
 | 
			
		||||
import kotlinx.coroutines.experimental.async
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
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<List<Extension.Installed>>()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * List of the currently installed extensions.
 | 
			
		||||
     */
 | 
			
		||||
    var installedExtensions = emptyList<Extension.Installed>()
 | 
			
		||||
        private set(value) {
 | 
			
		||||
            field = value
 | 
			
		||||
            installedExtensionsRelay.call(value)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Relay used to notify the available extensions.
 | 
			
		||||
     */
 | 
			
		||||
    private val availableExtensionsRelay = BehaviorRelay.create<List<Extension.Available>>()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * List of the currently available extensions.
 | 
			
		||||
     */
 | 
			
		||||
    var availableExtensions = emptyList<Extension.Available>()
 | 
			
		||||
        private set(value) {
 | 
			
		||||
            field = value
 | 
			
		||||
            availableExtensionsRelay.call(value)
 | 
			
		||||
            setUpdateFieldOfInstalledExtensions(value)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Relay used to notify the untrusted extensions.
 | 
			
		||||
     */
 | 
			
		||||
    private val untrustedExtensionsRelay = BehaviorRelay.create<List<Extension.Untrusted>>()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * List of the currently untrusted extensions.
 | 
			
		||||
     */
 | 
			
		||||
    var untrustedExtensions = emptyList<Extension.Untrusted>()
 | 
			
		||||
        private set(value) {
 | 
			
		||||
            field = value
 | 
			
		||||
            untrustedExtensionsRelay.call(value)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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
 | 
			
		||||
                .filterIsInstance<LoadResult.Success>()
 | 
			
		||||
                .map { it.extension }
 | 
			
		||||
        installedExtensions
 | 
			
		||||
                .flatMap { it.sources }
 | 
			
		||||
                // overwrite is needed until the bundled sources are removed
 | 
			
		||||
                .forEach { sourceManager.registerSource(it, true) }
 | 
			
		||||
 | 
			
		||||
        untrustedExtensions = extensions
 | 
			
		||||
                .filterIsInstance<LoadResult.Untrusted>()
 | 
			
		||||
                .map { it.extension }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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>> {
 | 
			
		||||
        if (!availableExtensionsRelay.hasValue()) {
 | 
			
		||||
            findAvailableExtensions()
 | 
			
		||||
        }
 | 
			
		||||
        return availableExtensionsRelay.asObservable()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the relay of the untrusted extensions as an observable.
 | 
			
		||||
     */
 | 
			
		||||
    fun getUntrustedExtensionsObservable(): Observable<List<Extension.Untrusted>> {
 | 
			
		||||
        return untrustedExtensionsRelay.asObservable()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Finds the available extensions in the [api] and updates [availableExtensions].
 | 
			
		||||
     */
 | 
			
		||||
    fun findAvailableExtensions() {
 | 
			
		||||
        api.findExtensions()
 | 
			
		||||
                .onErrorReturn { emptyList() }
 | 
			
		||||
                .subscribeOn(Schedulers.io())
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe { availableExtensions = it }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the update field of the installed extensions with the given [availableExtensions].
 | 
			
		||||
     *
 | 
			
		||||
     * @param availableExtensions The list of extensions given by the [api].
 | 
			
		||||
     */
 | 
			
		||||
    private fun setUpdateFieldOfInstalledExtensions(availableExtensions: List<Extension.Available>) {
 | 
			
		||||
        val mutInstalledExtensions = installedExtensions.toMutableList()
 | 
			
		||||
        var changed = false
 | 
			
		||||
 | 
			
		||||
        for ((index, installedExt) in mutInstalledExtensions.withIndex()) {
 | 
			
		||||
            val pkgName = installedExt.pkgName
 | 
			
		||||
            val availableExt = availableExtensions.find { it.pkgName == pkgName } ?: continue
 | 
			
		||||
 | 
			
		||||
            val hasUpdate = availableExt.versionCode > installedExt.versionCode
 | 
			
		||||
            if (installedExt.hasUpdate != hasUpdate) {
 | 
			
		||||
                mutInstalledExtensions[index] = installedExt.copy(hasUpdate = hasUpdate)
 | 
			
		||||
                changed = true
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (changed) {
 | 
			
		||||
            installedExtensions = mutInstalledExtensions
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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.
 | 
			
		||||
     */
 | 
			
		||||
    fun updateExtension(extension: Extension.Installed): Observable<InstallStep>  {
 | 
			
		||||
        val availableExt = availableExtensions.find { it.pkgName == extension.pkgName }
 | 
			
		||||
                ?: return Observable.empty()
 | 
			
		||||
        return installExtension(availableExt)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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
 | 
			
		||||
        val preference = preferences.trustedSignatures()
 | 
			
		||||
        preference.set(preference.getOrDefault() + 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
 | 
			
		||||
        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())
 | 
			
		||||
            installer.onApkInstalled(extension.pkgName)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun onExtensionUpdated(extension: Extension.Installed) {
 | 
			
		||||
            registerUpdatedExtension(extension.withUpdateCheck())
 | 
			
		||||
            installer.onApkInstalled(extension.pkgName)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun onExtensionUntrusted(extension: Extension.Untrusted) {
 | 
			
		||||
            untrustedExtensions += extension
 | 
			
		||||
            installer.onApkInstalled(extension.pkgName)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun onPackageUninstalled(pkgName: String) {
 | 
			
		||||
            unregisterExtension(pkgName)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,55 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.api
 | 
			
		||||
 | 
			
		||||
import com.github.salomonbrys.kotson.fromJson
 | 
			
		||||
import com.github.salomonbrys.kotson.get
 | 
			
		||||
import com.github.salomonbrys.kotson.int
 | 
			
		||||
import com.github.salomonbrys.kotson.string
 | 
			
		||||
import com.google.gson.Gson
 | 
			
		||||
import com.google.gson.JsonArray
 | 
			
		||||
import eu.kanade.tachiyomi.extension.model.Extension
 | 
			
		||||
import eu.kanade.tachiyomi.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.network.NetworkHelper
 | 
			
		||||
import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
internal class ExtensionGithubApi {
 | 
			
		||||
 | 
			
		||||
    private val network: NetworkHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    private val client get() = network.client
 | 
			
		||||
 | 
			
		||||
    private val gson: Gson by injectLazy()
 | 
			
		||||
 | 
			
		||||
    private val repoUrl = "https://raw.githubusercontent.com/inorichi/tachiyomi-extensions/repo"
 | 
			
		||||
 | 
			
		||||
    fun findExtensions(): Observable<List<Extension.Available>> {
 | 
			
		||||
        val call = GET("$repoUrl/index.json")
 | 
			
		||||
 | 
			
		||||
        return client.newCall(call).asObservableSuccess()
 | 
			
		||||
                .map(::parseResponse)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun parseResponse(response: Response): List<Extension.Available> {
 | 
			
		||||
        val text = response.body()?.use { it.string() } ?: return emptyList()
 | 
			
		||||
 | 
			
		||||
        val json = gson.fromJson<JsonArray>(text)
 | 
			
		||||
 | 
			
		||||
        return json.map { element ->
 | 
			
		||||
            val name = element["name"].string.substringAfter("Tachiyomi: ")
 | 
			
		||||
            val pkgName = element["pkg"].string
 | 
			
		||||
            val apkName = element["apk"].string
 | 
			
		||||
            val versionName = element["version"].string
 | 
			
		||||
            val versionCode = element["code"].int
 | 
			
		||||
            val lang = element["lang"].string
 | 
			
		||||
            val icon = "$repoUrl/icon/${apkName.replace(".apk", ".png")}"
 | 
			
		||||
 | 
			
		||||
            Extension.Available(name, pkgName, versionName, versionCode, lang, apkName, icon)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getApkUrl(extension: Extension.Available): String {
 | 
			
		||||
        return "$repoUrl/apk/${extension.apkName}"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.model
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.Source
 | 
			
		||||
 | 
			
		||||
sealed class Extension {
 | 
			
		||||
 | 
			
		||||
    abstract val name: String
 | 
			
		||||
    abstract val pkgName: String
 | 
			
		||||
    abstract val versionName: String
 | 
			
		||||
    abstract val versionCode: Int
 | 
			
		||||
    abstract val lang: String?
 | 
			
		||||
 | 
			
		||||
    data class Installed(override val name: String,
 | 
			
		||||
                         override val pkgName: String,
 | 
			
		||||
                         override val versionName: String,
 | 
			
		||||
                         override val versionCode: Int,
 | 
			
		||||
                         val sources: List<Source>,
 | 
			
		||||
                         override val lang: String,
 | 
			
		||||
                         val hasUpdate: Boolean = false) : Extension()
 | 
			
		||||
 | 
			
		||||
    data class Available(override val name: String,
 | 
			
		||||
                         override val pkgName: String,
 | 
			
		||||
                         override val versionName: String,
 | 
			
		||||
                         override val versionCode: Int,
 | 
			
		||||
                         override val lang: String,
 | 
			
		||||
                         val apkName: String,
 | 
			
		||||
                         val iconUrl: String) : Extension()
 | 
			
		||||
 | 
			
		||||
    data class Untrusted(override val name: String,
 | 
			
		||||
                         override val pkgName: String,
 | 
			
		||||
                         override val versionName: String,
 | 
			
		||||
                         override val versionCode: Int,
 | 
			
		||||
                         val signatureHash: String,
 | 
			
		||||
                         override val lang: String? = null) : Extension()
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.model
 | 
			
		||||
 | 
			
		||||
enum class InstallStep {
 | 
			
		||||
    Pending, Downloading, Installing, Installed, Error;
 | 
			
		||||
 | 
			
		||||
    fun isCompleted(): Boolean {
 | 
			
		||||
        return this == Installed || this == Error
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,10 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.model
 | 
			
		||||
 | 
			
		||||
sealed class LoadResult {
 | 
			
		||||
 | 
			
		||||
    class Success(val extension: Extension.Installed) : LoadResult()
 | 
			
		||||
    class Untrusted(val extension: Extension.Untrusted) : LoadResult()
 | 
			
		||||
    class Error(val message: String? = null) : LoadResult() {
 | 
			
		||||
        constructor(exception: Throwable) : this(exception.message)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,114 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.util
 | 
			
		||||
 | 
			
		||||
import android.content.BroadcastReceiver
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.content.IntentFilter
 | 
			
		||||
import eu.kanade.tachiyomi.extension.model.Extension
 | 
			
		||||
import eu.kanade.tachiyomi.extension.model.LoadResult
 | 
			
		||||
import eu.kanade.tachiyomi.util.launchNow
 | 
			
		||||
import kotlinx.coroutines.experimental.async
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Broadcast receiver that listens for the system's packages installed, updated or removed, and only
 | 
			
		||||
 * notifies the given [listener] when the package is an extension.
 | 
			
		||||
 *
 | 
			
		||||
 * @param listener The listener that should be notified of extension installation events.
 | 
			
		||||
 */
 | 
			
		||||
internal class ExtensionInstallReceiver(private val listener: Listener) :
 | 
			
		||||
        BroadcastReceiver() {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Registers this broadcast receiver
 | 
			
		||||
     */
 | 
			
		||||
    fun register(context: Context) {
 | 
			
		||||
        context.registerReceiver(this, filter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the intent filter this receiver should subscribe to.
 | 
			
		||||
     */
 | 
			
		||||
    private val filter get() = IntentFilter().apply {
 | 
			
		||||
        addAction(Intent.ACTION_PACKAGE_ADDED)
 | 
			
		||||
        addAction(Intent.ACTION_PACKAGE_REPLACED)
 | 
			
		||||
        addAction(Intent.ACTION_PACKAGE_REMOVED)
 | 
			
		||||
        addDataScheme("package")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when one of the events of the [filter] is received. When the package is an extension,
 | 
			
		||||
     * it's loaded in background and it notifies the [listener] when finished.
 | 
			
		||||
     */
 | 
			
		||||
    override fun onReceive(context: Context, intent: Intent?) {
 | 
			
		||||
        if (intent == null) return
 | 
			
		||||
 | 
			
		||||
        when (intent.action) {
 | 
			
		||||
            Intent.ACTION_PACKAGE_ADDED -> {
 | 
			
		||||
                if (!isReplacing(intent)) launchNow {
 | 
			
		||||
                    val result = getExtensionFromIntent(context, intent)
 | 
			
		||||
                    when (result) {
 | 
			
		||||
                        is LoadResult.Success -> listener.onExtensionInstalled(result.extension)
 | 
			
		||||
                        is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Intent.ACTION_PACKAGE_REPLACED -> {
 | 
			
		||||
                launchNow {
 | 
			
		||||
                    val result = getExtensionFromIntent(context, intent)
 | 
			
		||||
                    when (result) {
 | 
			
		||||
                        is LoadResult.Success -> listener.onExtensionUpdated(result.extension)
 | 
			
		||||
                        // Not needed as a package can't be upgraded if the signature is different
 | 
			
		||||
                        is LoadResult.Untrusted -> {}
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Intent.ACTION_PACKAGE_REMOVED -> {
 | 
			
		||||
                if (!isReplacing(intent)) {
 | 
			
		||||
                    val pkgName = getPackageNameFromIntent(intent)
 | 
			
		||||
                    if (pkgName != null) {
 | 
			
		||||
                        listener.onPackageUninstalled(pkgName)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns true if this package is performing an update.
 | 
			
		||||
     *
 | 
			
		||||
     * @param intent The intent that triggered the event.
 | 
			
		||||
     */
 | 
			
		||||
    private fun isReplacing(intent: Intent): Boolean {
 | 
			
		||||
        return intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the extension triggered by the given intent.
 | 
			
		||||
     *
 | 
			
		||||
     * @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 {
 | 
			
		||||
        val pkgName = getPackageNameFromIntent(intent) ?:
 | 
			
		||||
                return LoadResult.Error("Package name not found")
 | 
			
		||||
        return async { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }.await()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the package name of the installed, updated or removed application.
 | 
			
		||||
     */
 | 
			
		||||
    private fun getPackageNameFromIntent(intent: Intent?): String? {
 | 
			
		||||
        return intent?.data?.encodedSchemeSpecificPart ?: return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Listener that receives extension installation events.
 | 
			
		||||
     */
 | 
			
		||||
    interface Listener {
 | 
			
		||||
        fun onExtensionInstalled(extension: Extension.Installed)
 | 
			
		||||
        fun onExtensionUpdated(extension: Extension.Installed)
 | 
			
		||||
        fun onExtensionUntrusted(extension: Extension.Untrusted)
 | 
			
		||||
        fun onPackageUninstalled(pkgName: String)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,221 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.util
 | 
			
		||||
 | 
			
		||||
import android.app.DownloadManager
 | 
			
		||||
import android.content.BroadcastReceiver
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.content.IntentFilter
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import com.jakewharton.rxrelay.PublishRelay
 | 
			
		||||
import eu.kanade.tachiyomi.extension.model.Extension
 | 
			
		||||
import eu.kanade.tachiyomi.extension.model.InstallStep
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The installer which installs, updates and uninstalls the extensions.
 | 
			
		||||
 *
 | 
			
		||||
 * @param context The application context.
 | 
			
		||||
 */
 | 
			
		||||
internal class ExtensionInstaller(private val context: Context) {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The system's download manager
 | 
			
		||||
     */
 | 
			
		||||
    private val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The broadcast receiver which listens to download completion events.
 | 
			
		||||
     */
 | 
			
		||||
    private val downloadReceiver = DownloadCompletionReceiver()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The currently requested downloads, with the package name (unique id) as key, and the id
 | 
			
		||||
     * returned by the download manager.
 | 
			
		||||
     */
 | 
			
		||||
    private val activeDownloads = hashMapOf<String, Long>()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Relay used to notify the installation step of every download.
 | 
			
		||||
     */
 | 
			
		||||
    private val downloadsRelay = PublishRelay.create<Pair<Long, InstallStep>>()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Adds the given extension to the downloads queue and returns an observable containing its
 | 
			
		||||
     * step in the installation process.
 | 
			
		||||
     *
 | 
			
		||||
     * @param url The url of the apk.
 | 
			
		||||
     * @param extension The extension to install.
 | 
			
		||||
     */
 | 
			
		||||
    fun downloadAndInstall(url: String, extension: Extension) = Observable.defer {
 | 
			
		||||
        val pkgName = extension.pkgName
 | 
			
		||||
 | 
			
		||||
        val oldDownload = activeDownloads[pkgName]
 | 
			
		||||
        if (oldDownload != null) {
 | 
			
		||||
            deleteDownload(pkgName)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Register the receiver after removing (and unregistering) the previous download
 | 
			
		||||
        downloadReceiver.register()
 | 
			
		||||
 | 
			
		||||
        val request = DownloadManager.Request(Uri.parse(url))
 | 
			
		||||
                .setTitle(extension.name)
 | 
			
		||||
                .setMimeType(APK_MIME)
 | 
			
		||||
                .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
 | 
			
		||||
 | 
			
		||||
        val id = downloadManager.enqueue(request)
 | 
			
		||||
        activeDownloads.put(pkgName, id)
 | 
			
		||||
 | 
			
		||||
        downloadsRelay.filter { it.first == id }
 | 
			
		||||
                .map { it.second }
 | 
			
		||||
                // Poll download status
 | 
			
		||||
                .mergeWith(pollStatus(id))
 | 
			
		||||
                // Force an error if the download takes more than 3 minutes
 | 
			
		||||
                .mergeWith(Observable.timer(3, TimeUnit.MINUTES).map { InstallStep.Error })
 | 
			
		||||
                // Stop when the application is installed or errors
 | 
			
		||||
                .takeUntil { it.isCompleted() }
 | 
			
		||||
                // Always notify on main thread
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                // Always remove the download when unsubscribed
 | 
			
		||||
                .doOnUnsubscribe { deleteDownload(pkgName) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns an observable that polls the given download id for its status every second, as the
 | 
			
		||||
     * manager doesn't have any notification system. It'll stop once the download finishes.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id The id of the download to poll.
 | 
			
		||||
     */
 | 
			
		||||
    private fun pollStatus(id: Long): Observable<InstallStep> {
 | 
			
		||||
        val query = DownloadManager.Query().setFilterById(id)
 | 
			
		||||
 | 
			
		||||
        return Observable.interval(0, 1, TimeUnit.SECONDS)
 | 
			
		||||
                // Get the current download status
 | 
			
		||||
                .map {
 | 
			
		||||
                    downloadManager.query(query).use { cursor ->
 | 
			
		||||
                        cursor.moveToFirst()
 | 
			
		||||
                        cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                // Ignore duplicate results
 | 
			
		||||
                .distinctUntilChanged()
 | 
			
		||||
                // Stop polling when the download fails or finishes
 | 
			
		||||
                .takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED }
 | 
			
		||||
                // Map to our model
 | 
			
		||||
                .flatMap { status ->
 | 
			
		||||
                    when (status) {
 | 
			
		||||
                        DownloadManager.STATUS_PENDING -> Observable.just(InstallStep.Pending)
 | 
			
		||||
                        DownloadManager.STATUS_RUNNING -> Observable.just(InstallStep.Downloading)
 | 
			
		||||
                        else -> Observable.empty()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Starts an intent to install the extension at the given uri.
 | 
			
		||||
     *
 | 
			
		||||
     * @param uri The uri of the extension to install.
 | 
			
		||||
     */
 | 
			
		||||
    fun installApk(uri: Uri) {
 | 
			
		||||
        val intent = Intent(Intent.ACTION_VIEW)
 | 
			
		||||
                .setDataAndType(uri, APK_MIME)
 | 
			
		||||
                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
 | 
			
		||||
 | 
			
		||||
        context.startActivity(intent)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Starts an intent to uninstall the extension by the given package name.
 | 
			
		||||
     *
 | 
			
		||||
     * @param pkgName The package name of the extension to uninstall
 | 
			
		||||
     */
 | 
			
		||||
    fun uninstallApk(pkgName: String) {
 | 
			
		||||
        val packageUri = Uri.parse("package:$pkgName")
 | 
			
		||||
        val uninstallIntent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri)
 | 
			
		||||
        context.startActivity(uninstallIntent)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when an extension is installed, allowing to update its installation step.
 | 
			
		||||
     *
 | 
			
		||||
     * @param pkgName The package name of the installed application.
 | 
			
		||||
     */
 | 
			
		||||
    fun onApkInstalled(pkgName: String) {
 | 
			
		||||
        val id = activeDownloads[pkgName] ?: return
 | 
			
		||||
        downloadsRelay.call(id to InstallStep.Installed)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Deletes the download for the given package name.
 | 
			
		||||
     *
 | 
			
		||||
     * @param pkgName The package name of the download to delete.
 | 
			
		||||
     */
 | 
			
		||||
    fun deleteDownload(pkgName: String) {
 | 
			
		||||
        val downloadId = activeDownloads.remove(pkgName)
 | 
			
		||||
        if (downloadId != null) {
 | 
			
		||||
            downloadManager.remove(downloadId)
 | 
			
		||||
        }
 | 
			
		||||
        if (activeDownloads.isEmpty()) {
 | 
			
		||||
            downloadReceiver.unregister()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Receiver that listens to download status events.
 | 
			
		||||
     */
 | 
			
		||||
    private inner class DownloadCompletionReceiver : BroadcastReceiver() {
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Whether this receiver is currently registered.
 | 
			
		||||
         */
 | 
			
		||||
        private var isRegistered = false
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Registers this receiver if it's not already.
 | 
			
		||||
         */
 | 
			
		||||
        fun register() {
 | 
			
		||||
            if (isRegistered) return
 | 
			
		||||
            isRegistered = true
 | 
			
		||||
 | 
			
		||||
            val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
 | 
			
		||||
            context.registerReceiver(this, filter)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Unregisters this receiver if it's not already.
 | 
			
		||||
         */
 | 
			
		||||
        fun unregister() {
 | 
			
		||||
            if (!isRegistered) return
 | 
			
		||||
            isRegistered = false
 | 
			
		||||
 | 
			
		||||
            context.unregisterReceiver(this)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Called when a download event is received. It looks for the download in the current active
 | 
			
		||||
         * downloads and notifies its installation step.
 | 
			
		||||
         */
 | 
			
		||||
        override fun onReceive(context: Context, intent: Intent?) {
 | 
			
		||||
            val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0) ?: return
 | 
			
		||||
 | 
			
		||||
            // Avoid events for downloads we didn't request
 | 
			
		||||
            if (id !in activeDownloads.values) return
 | 
			
		||||
 | 
			
		||||
            val uri = downloadManager.getUriForDownloadedFile(id)
 | 
			
		||||
            if (uri != null) {
 | 
			
		||||
                downloadsRelay.call(id to InstallStep.Installing)
 | 
			
		||||
                installApk(uri)
 | 
			
		||||
            } else {
 | 
			
		||||
                Timber.e("Couldn't locate downloaded APK")
 | 
			
		||||
                downloadsRelay.call(id to InstallStep.Error)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private companion object {
 | 
			
		||||
        const val APK_MIME = "application/vnd.android.package-archive"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,172 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.util
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.pm.PackageInfo
 | 
			
		||||
import android.content.pm.PackageManager
 | 
			
		||||
import dalvik.system.PathClassLoader
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
 | 
			
		||||
import eu.kanade.tachiyomi.extension.model.Extension
 | 
			
		||||
import eu.kanade.tachiyomi.extension.model.LoadResult
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.source.SourceFactory
 | 
			
		||||
import eu.kanade.tachiyomi.util.Hash
 | 
			
		||||
import kotlinx.coroutines.experimental.async
 | 
			
		||||
import kotlinx.coroutines.experimental.runBlocking
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class that handles the loading of the extensions installed in the system.
 | 
			
		||||
 */
 | 
			
		||||
@SuppressLint("PackageManagerGetSignatures")
 | 
			
		||||
internal object ExtensionLoader {
 | 
			
		||||
 | 
			
		||||
    private const val EXTENSION_FEATURE = "tachiyomi.extension"
 | 
			
		||||
    private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
 | 
			
		||||
    private const val LIB_VERSION_MIN = 1
 | 
			
		||||
    private const val LIB_VERSION_MAX = 1
 | 
			
		||||
 | 
			
		||||
    private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * List of the trusted signatures.
 | 
			
		||||
     */
 | 
			
		||||
    var trustedSignatures = mutableSetOf<String>() +
 | 
			
		||||
            Injekt.get<PreferencesHelper>().trustedSignatures().getOrDefault() +
 | 
			
		||||
            // inorichi's key
 | 
			
		||||
            "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Return a list of all the installed extensions initialized concurrently.
 | 
			
		||||
     *
 | 
			
		||||
     * @param context The application context.
 | 
			
		||||
     */
 | 
			
		||||
    fun loadExtensions(context: Context): List<LoadResult> {
 | 
			
		||||
        val pkgManager = context.packageManager
 | 
			
		||||
        val installedPkgs = pkgManager.getInstalledPackages(PACKAGE_FLAGS)
 | 
			
		||||
        val extPkgs = installedPkgs.filter { isPackageAnExtension(it) }
 | 
			
		||||
 | 
			
		||||
        if (extPkgs.isEmpty()) return emptyList()
 | 
			
		||||
 | 
			
		||||
        // Load each extension concurrently and wait for completion
 | 
			
		||||
        return runBlocking {
 | 
			
		||||
            val deferred = extPkgs.map {
 | 
			
		||||
                async { loadExtension(context, it.packageName, it) }
 | 
			
		||||
            }
 | 
			
		||||
            deferred.map { it.await() }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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.
 | 
			
		||||
     */
 | 
			
		||||
    fun loadExtensionFromPkgName(context: Context, pkgName: String): LoadResult {
 | 
			
		||||
        val pkgInfo = context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
 | 
			
		||||
        if (!isPackageAnExtension(pkgInfo)) {
 | 
			
		||||
            return LoadResult.Error("Tried to load a package that wasn't a extension")
 | 
			
		||||
        }
 | 
			
		||||
        return loadExtension(context, pkgName, pkgInfo)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Loads an extension given its package name.
 | 
			
		||||
     *
 | 
			
		||||
     * @param context The application context.
 | 
			
		||||
     * @param pkgName The package name of the extension to load.
 | 
			
		||||
     * @param pkgInfo The package info of the extension.
 | 
			
		||||
     */
 | 
			
		||||
    private fun loadExtension(context: Context, pkgName: String, pkgInfo: PackageInfo): LoadResult {
 | 
			
		||||
        val pkgManager = context.packageManager
 | 
			
		||||
 | 
			
		||||
        val appInfo = pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
 | 
			
		||||
 | 
			
		||||
        val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
 | 
			
		||||
        val versionName = pkgInfo.versionName
 | 
			
		||||
        val versionCode = pkgInfo.versionCode
 | 
			
		||||
 | 
			
		||||
        // Validate lib version
 | 
			
		||||
        val majorLibVersion = versionName.substringBefore('.').toInt()
 | 
			
		||||
        if (majorLibVersion < LIB_VERSION_MIN || majorLibVersion > LIB_VERSION_MAX) {
 | 
			
		||||
            val exception = Exception("Lib version is $majorLibVersion, while only versions " +
 | 
			
		||||
                    "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed")
 | 
			
		||||
            Timber.w(exception)
 | 
			
		||||
            return LoadResult.Error(exception)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val signatureHash = getSignatureHash(pkgInfo)
 | 
			
		||||
 | 
			
		||||
        if (signatureHash == null) {
 | 
			
		||||
            return LoadResult.Error("Package $pkgName isn't signed")
 | 
			
		||||
        } else if (signatureHash !in trustedSignatures) {
 | 
			
		||||
            val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, signatureHash)
 | 
			
		||||
            Timber.w("Extension $pkgName isn't trusted")
 | 
			
		||||
            return LoadResult.Untrusted(extension)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
 | 
			
		||||
 | 
			
		||||
        val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)
 | 
			
		||||
                .split(";")
 | 
			
		||||
                .map {
 | 
			
		||||
                    val sourceClass = it.trim()
 | 
			
		||||
                    if (sourceClass.startsWith("."))
 | 
			
		||||
                        pkgInfo.packageName + sourceClass
 | 
			
		||||
                    else
 | 
			
		||||
                        sourceClass
 | 
			
		||||
                }
 | 
			
		||||
                .flatMap {
 | 
			
		||||
                    try {
 | 
			
		||||
                        val obj = Class.forName(it, false, classLoader).newInstance()
 | 
			
		||||
                        when (obj) {
 | 
			
		||||
                            is Source -> listOf(obj)
 | 
			
		||||
                            is SourceFactory -> obj.createSources()
 | 
			
		||||
                            else -> throw Exception("Unknown source class type! ${obj.javaClass}")
 | 
			
		||||
                        }
 | 
			
		||||
                    } catch (e: Throwable) {
 | 
			
		||||
                        Timber.e(e, "Extension load error: $extName.")
 | 
			
		||||
                        return LoadResult.Error(e)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
        val langs = sources.filterIsInstance<CatalogueSource>()
 | 
			
		||||
                .map { it.lang }
 | 
			
		||||
                .toSet()
 | 
			
		||||
 | 
			
		||||
        val lang = when (langs.size) {
 | 
			
		||||
            0 -> ""
 | 
			
		||||
            1 -> langs.first()
 | 
			
		||||
            else -> "all"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val extension = Extension.Installed(extName, pkgName, versionName, versionCode, sources, lang)
 | 
			
		||||
        return LoadResult.Success(extension)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns true if the given package is an extension.
 | 
			
		||||
     *
 | 
			
		||||
     * @param pkgInfo The package info of the application.
 | 
			
		||||
     */
 | 
			
		||||
    private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean {
 | 
			
		||||
        return pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the signature hash of the package or null if it's not signed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param pkgInfo The package info of the application.
 | 
			
		||||
     */
 | 
			
		||||
    private fun getSignatureHash(pkgInfo: PackageInfo): String? {
 | 
			
		||||
        val signatures = pkgInfo.signatures
 | 
			
		||||
        return if (signatures != null && !signatures.isEmpty()) {
 | 
			
		||||
            Hash.sha256(signatures.first().toByteArray())
 | 
			
		||||
        } else {
 | 
			
		||||
            null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
package eu.kanade.tachiyomi.source
 | 
			
		||||
 | 
			
		||||
import android.support.v7.preference.PreferenceScreen
 | 
			
		||||
 | 
			
		||||
interface ConfigurableSource : Source {
 | 
			
		||||
 | 
			
		||||
    fun setupPreferenceScreen(screen: PreferenceScreen)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,30 +1,19 @@
 | 
			
		||||
package eu.kanade.tachiyomi.source
 | 
			
		||||
 | 
			
		||||
import android.Manifest.permission.READ_EXTERNAL_STORAGE
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.pm.ApplicationInfo
 | 
			
		||||
import android.content.pm.PackageManager
 | 
			
		||||
import android.os.Environment
 | 
			
		||||
import dalvik.system.PathClassLoader
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.YamlHttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.english.*
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.german.WieManga
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.russian.Mangachan
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.russian.Mintmanga
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.russian.Readmanga
 | 
			
		||||
import eu.kanade.tachiyomi.util.hasPermission
 | 
			
		||||
import org.yaml.snakeyaml.Yaml
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import java.io.File
 | 
			
		||||
 | 
			
		||||
open class SourceManager(private val context: Context) {
 | 
			
		||||
 | 
			
		||||
    private val sourcesMap = mutableMapOf<Long, Source>()
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        createSources()
 | 
			
		||||
        createInternalSources().forEach { registerSource(it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    open fun get(sourceKey: Long): Source? {
 | 
			
		||||
@@ -35,18 +24,16 @@ open class SourceManager(private val context: Context) {
 | 
			
		||||
 | 
			
		||||
    fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
 | 
			
		||||
 | 
			
		||||
    private fun createSources() {
 | 
			
		||||
        createExtensionSources().forEach { registerSource(it) }
 | 
			
		||||
        createYamlSources().forEach { registerSource(it) }
 | 
			
		||||
        createInternalSources().forEach { registerSource(it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun registerSource(source: Source, overwrite: Boolean = false) {
 | 
			
		||||
    internal fun registerSource(source: Source, overwrite: Boolean = false) {
 | 
			
		||||
        if (overwrite || !sourcesMap.containsKey(source.id)) {
 | 
			
		||||
            sourcesMap.put(source.id, source)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    internal fun unregisterSource(source: Source) {
 | 
			
		||||
        sourcesMap.remove(source.id)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun createInternalSources(): List<Source> = listOf(
 | 
			
		||||
            LocalSource(context),
 | 
			
		||||
            Batoto(),
 | 
			
		||||
@@ -60,92 +47,4 @@ open class SourceManager(private val context: Context) {
 | 
			
		||||
            Mangasee(),
 | 
			
		||||
            WieManga()
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    private fun createYamlSources(): List<Source> {
 | 
			
		||||
        val sources = mutableListOf<Source>()
 | 
			
		||||
 | 
			
		||||
        val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
 | 
			
		||||
                File.separator + context.getString(R.string.app_name), "parsers")
 | 
			
		||||
 | 
			
		||||
        if (parsersDir.exists() && context.hasPermission(READ_EXTERNAL_STORAGE)) {
 | 
			
		||||
            val yaml = Yaml()
 | 
			
		||||
            for (file in parsersDir.listFiles().filter { it.extension == "yml" }) {
 | 
			
		||||
                try {
 | 
			
		||||
                    val map = file.inputStream().use { yaml.loadAs(it, Map::class.java) }
 | 
			
		||||
                    sources.add(YamlHttpSource(map))
 | 
			
		||||
                } catch (e: Exception) {
 | 
			
		||||
                    Timber.e("Error loading source from file. Bad format?", e)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return sources
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun createExtensionSources(): List<Source> {
 | 
			
		||||
        val pkgManager = context.packageManager
 | 
			
		||||
        val flags = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
 | 
			
		||||
        val installedPkgs = pkgManager.getInstalledPackages(flags)
 | 
			
		||||
        val extPkgs = installedPkgs.filter { it.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE } }
 | 
			
		||||
 | 
			
		||||
        val sources = mutableListOf<Source>()
 | 
			
		||||
        for (pkgInfo in extPkgs) {
 | 
			
		||||
            val appInfo = pkgManager.getApplicationInfo(pkgInfo.packageName,
 | 
			
		||||
                    PackageManager.GET_META_DATA) ?: continue
 | 
			
		||||
 | 
			
		||||
            val extName = pkgManager.getApplicationLabel(appInfo).toString()
 | 
			
		||||
                    .substringAfter("Tachiyomi: ")
 | 
			
		||||
            val version = pkgInfo.versionName
 | 
			
		||||
            val sourceClasses = appInfo.metaData.getString(METADATA_SOURCE_CLASS)
 | 
			
		||||
                    .split(";")
 | 
			
		||||
                    .map {
 | 
			
		||||
                        val sourceClass = it.trim()
 | 
			
		||||
                        if(sourceClass.startsWith("."))
 | 
			
		||||
                            pkgInfo.packageName + sourceClass
 | 
			
		||||
                        else
 | 
			
		||||
                            sourceClass
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
            val extension = Extension(extName, appInfo, version, sourceClasses)
 | 
			
		||||
            try {
 | 
			
		||||
                sources += loadExtension(extension)
 | 
			
		||||
            } catch (e: Exception) {
 | 
			
		||||
                Timber.e("Extension load error: $extName.", e)
 | 
			
		||||
            } catch (e: LinkageError) {
 | 
			
		||||
                Timber.e("Extension load error: $extName.", e)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return sources
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun loadExtension(ext: Extension): List<Source> {
 | 
			
		||||
        // Validate lib version
 | 
			
		||||
        val majorLibVersion = ext.version.substringBefore('.').toInt()
 | 
			
		||||
        if (majorLibVersion < LIB_VERSION_MIN || majorLibVersion > LIB_VERSION_MAX) {
 | 
			
		||||
            throw Exception("Lib version is $majorLibVersion, while only versions "
 | 
			
		||||
                    + "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val classLoader = PathClassLoader(ext.appInfo.sourceDir, null, context.classLoader)
 | 
			
		||||
        return ext.sourceClasses.flatMap {
 | 
			
		||||
            val obj = Class.forName(it, false, classLoader).newInstance()
 | 
			
		||||
            when(obj) {
 | 
			
		||||
                is Source -> listOf(obj)
 | 
			
		||||
                is SourceFactory -> obj.createSources()
 | 
			
		||||
                else -> throw Exception("Unknown source class type!")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class Extension(val name: String,
 | 
			
		||||
                    val appInfo: ApplicationInfo,
 | 
			
		||||
                    val version: String,
 | 
			
		||||
                    val sourceClasses: List<String>)
 | 
			
		||||
 | 
			
		||||
    private companion object {
 | 
			
		||||
        const val EXTENSION_FEATURE = "tachiyomi.extension"
 | 
			
		||||
        const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
 | 
			
		||||
        const val LIB_VERSION_MIN = 1
 | 
			
		||||
        const val LIB_VERSION_MAX = 1
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package eu.kanade.tachiyomi.source.online
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.network.NetworkHelper
 | 
			
		||||
import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
			
		||||
@@ -28,10 +27,12 @@ abstract class HttpSource : CatalogueSource {
 | 
			
		||||
     */
 | 
			
		||||
    protected val network: NetworkHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Preferences helper.
 | 
			
		||||
     */
 | 
			
		||||
    protected val preferences: PreferencesHelper by injectLazy()
 | 
			
		||||
//    /**
 | 
			
		||||
//     * Preferences that a source may need.
 | 
			
		||||
//     */
 | 
			
		||||
//    val preferences: SharedPreferences by lazy {
 | 
			
		||||
//        Injekt.get<Application>().getSharedPreferences("source_$id", Context.MODE_PRIVATE)
 | 
			
		||||
//    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Base url of the website without the trailing slash, like: http://mysite.com
 | 
			
		||||
 
 | 
			
		||||
@@ -1,231 +0,0 @@
 | 
			
		||||
package eu.kanade.tachiyomi.source.online
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.network.POST
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.*
 | 
			
		||||
import eu.kanade.tachiyomi.util.asJsoup
 | 
			
		||||
import eu.kanade.tachiyomi.util.attrOrText
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import org.jsoup.Jsoup
 | 
			
		||||
import org.jsoup.nodes.Element
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
class YamlHttpSource(mappings: Map<*, *>) : HttpSource() {
 | 
			
		||||
 | 
			
		||||
    val map = YamlSourceNode(mappings)
 | 
			
		||||
 | 
			
		||||
    override val name: String
 | 
			
		||||
        get() = map.name
 | 
			
		||||
 | 
			
		||||
    override val baseUrl = map.host.let {
 | 
			
		||||
        if (it.endsWith("/")) it.dropLast(1) else it
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override val lang = map.lang.toLowerCase()
 | 
			
		||||
 | 
			
		||||
    override val supportsLatest = map.latestupdates != null
 | 
			
		||||
 | 
			
		||||
    override val client = when (map.client) {
 | 
			
		||||
        "cloudflare" -> network.cloudflareClient
 | 
			
		||||
        else -> network.client
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override val id = map.id.let {
 | 
			
		||||
        (it as? Int ?: (lang.toUpperCase().hashCode() + 31 * it.hashCode()) and 0x7fffffff).toLong()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Ugly, but needed after the changes
 | 
			
		||||
    var popularNextPage: String? = null
 | 
			
		||||
    var searchNextPage: String? = null
 | 
			
		||||
    var latestNextPage: String? = null
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaRequest(page: Int): Request {
 | 
			
		||||
        val url = if (page == 1) {
 | 
			
		||||
            popularNextPage = null
 | 
			
		||||
            map.popular.url
 | 
			
		||||
        } else {
 | 
			
		||||
            popularNextPage!!
 | 
			
		||||
        }
 | 
			
		||||
        return when (map.popular.method?.toLowerCase()) {
 | 
			
		||||
            "post" -> POST(url, headers, map.popular.createForm())
 | 
			
		||||
            else -> GET(url, headers)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaParse(response: Response): MangasPage {
 | 
			
		||||
        val document = response.asJsoup()
 | 
			
		||||
 | 
			
		||||
        val mangas = document.select(map.popular.manga_css).map { element ->
 | 
			
		||||
            SManga.create().apply {
 | 
			
		||||
                title = element.text()
 | 
			
		||||
                setUrlWithoutDomain(element.attr("href"))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        popularNextPage = map.popular.next_url_css?.let { selector ->
 | 
			
		||||
             document.select(selector).first()?.absUrl("href")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return MangasPage(mangas, popularNextPage != null)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
 | 
			
		||||
        val url = if (page == 1) {
 | 
			
		||||
            searchNextPage = null
 | 
			
		||||
            map.search.url.replace("\$query", query)
 | 
			
		||||
        } else {
 | 
			
		||||
            searchNextPage!!
 | 
			
		||||
        }
 | 
			
		||||
        return when (map.search.method?.toLowerCase()) {
 | 
			
		||||
            "post" -> POST(url, headers, map.search.createForm())
 | 
			
		||||
            else -> GET(url, headers)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaParse(response: Response): MangasPage {
 | 
			
		||||
        val document = response.asJsoup()
 | 
			
		||||
 | 
			
		||||
        val mangas = document.select(map.search.manga_css).map { element ->
 | 
			
		||||
            SManga.create().apply {
 | 
			
		||||
                title = element.text()
 | 
			
		||||
                setUrlWithoutDomain(element.attr("href"))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        searchNextPage = map.search.next_url_css?.let { selector ->
 | 
			
		||||
            document.select(selector).first()?.absUrl("href")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return MangasPage(mangas, searchNextPage != null)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun latestUpdatesRequest(page: Int): Request {
 | 
			
		||||
        val url = if (page == 1) {
 | 
			
		||||
            latestNextPage = null
 | 
			
		||||
            map.latestupdates!!.url
 | 
			
		||||
        } else {
 | 
			
		||||
            latestNextPage!!
 | 
			
		||||
        }
 | 
			
		||||
        return when (map.latestupdates!!.method?.toLowerCase()) {
 | 
			
		||||
            "post" -> POST(url, headers, map.latestupdates.createForm())
 | 
			
		||||
            else -> GET(url, headers)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun latestUpdatesParse(response: Response): MangasPage {
 | 
			
		||||
        val document = response.asJsoup()
 | 
			
		||||
 | 
			
		||||
        val mangas = document.select(map.latestupdates!!.manga_css).map { element ->
 | 
			
		||||
            SManga.create().apply {
 | 
			
		||||
                title = element.text()
 | 
			
		||||
                setUrlWithoutDomain(element.attr("href"))
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        popularNextPage = map.latestupdates.next_url_css?.let { selector ->
 | 
			
		||||
            document.select(selector).first()?.absUrl("href")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return MangasPage(mangas, popularNextPage != null)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun mangaDetailsParse(response: Response): SManga {
 | 
			
		||||
        val document = response.asJsoup()
 | 
			
		||||
 | 
			
		||||
        val manga = SManga.create()
 | 
			
		||||
        with(map.manga) {
 | 
			
		||||
            val pool = parts.get(document)
 | 
			
		||||
 | 
			
		||||
            manga.author = author?.process(document, pool)
 | 
			
		||||
            manga.artist = artist?.process(document, pool)
 | 
			
		||||
            manga.description = summary?.process(document, pool)
 | 
			
		||||
            manga.thumbnail_url = cover?.process(document, pool)
 | 
			
		||||
            manga.genre = genres?.process(document, pool)
 | 
			
		||||
            manga.status = status?.getStatus(document, pool) ?: SManga.UNKNOWN
 | 
			
		||||
        }
 | 
			
		||||
        return manga
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun chapterListParse(response: Response): List<SChapter> {
 | 
			
		||||
        val document = response.asJsoup()
 | 
			
		||||
 | 
			
		||||
        val chapters = mutableListOf<SChapter>()
 | 
			
		||||
        with(map.chapters) {
 | 
			
		||||
            val pool = emptyMap<String, Element>()
 | 
			
		||||
            val dateFormat = SimpleDateFormat(date?.format, Locale.ENGLISH)
 | 
			
		||||
 | 
			
		||||
            for (element in document.select(chapter_css)) {
 | 
			
		||||
                val chapter = SChapter.create()
 | 
			
		||||
                element.select(title).first().let {
 | 
			
		||||
                    chapter.name = it.text()
 | 
			
		||||
                    chapter.setUrlWithoutDomain(it.attr("href"))
 | 
			
		||||
                }
 | 
			
		||||
                val dateElement = element.select(date?.select).first()
 | 
			
		||||
                chapter.date_upload = date?.getDate(dateElement, pool, dateFormat)?.time ?: 0
 | 
			
		||||
                chapters.add(chapter)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return chapters
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun pageListParse(response: Response): List<Page> {
 | 
			
		||||
        val body = response.body()!!.string()
 | 
			
		||||
        val url = response.request().url().toString()
 | 
			
		||||
 | 
			
		||||
        val pages = mutableListOf<Page>()
 | 
			
		||||
 | 
			
		||||
        val document by lazy { Jsoup.parse(body, url) }
 | 
			
		||||
 | 
			
		||||
        with(map.pages) {
 | 
			
		||||
            // Capture a list of values where page urls will be resolved.
 | 
			
		||||
            val capturedPages = if (pages_regex != null)
 | 
			
		||||
                pages_regex!!.toRegex().findAll(body).map { it.value }.toList()
 | 
			
		||||
            else if (pages_css != null)
 | 
			
		||||
                document.select(pages_css).map { it.attrOrText(pages_attr!!) }
 | 
			
		||||
            else
 | 
			
		||||
                null
 | 
			
		||||
 | 
			
		||||
            // For each captured value, obtain the url and create a new page.
 | 
			
		||||
            capturedPages?.forEach { value ->
 | 
			
		||||
                // If the captured value isn't an url, we have to use replaces with the chapter url.
 | 
			
		||||
                val pageUrl = if (replace != null && replacement != null)
 | 
			
		||||
                    url.replace(replace!!.toRegex(), replacement!!.replace("\$value", value))
 | 
			
		||||
                else
 | 
			
		||||
                    value
 | 
			
		||||
 | 
			
		||||
                pages.add(Page(pages.size, pageUrl))
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Capture a list of images.
 | 
			
		||||
            val capturedImages = if (image_regex != null)
 | 
			
		||||
                image_regex!!.toRegex().findAll(body).map { it.groups[1]?.value }.toList()
 | 
			
		||||
            else if (image_css != null)
 | 
			
		||||
                document.select(image_css).map { it.absUrl(image_attr) }
 | 
			
		||||
            else
 | 
			
		||||
                null
 | 
			
		||||
 | 
			
		||||
            // Assign the image url to each page
 | 
			
		||||
            capturedImages?.forEachIndexed { i, url ->
 | 
			
		||||
                val page = pages.getOrElse(i) { Page(i, "").apply { pages.add(this) } }
 | 
			
		||||
                page.imageUrl = url
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return pages
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun imageUrlParse(response: Response): String {
 | 
			
		||||
        val body = response.body()!!.string()
 | 
			
		||||
        val url = response.request().url().toString()
 | 
			
		||||
 | 
			
		||||
        with(map.pages) {
 | 
			
		||||
            return if (image_regex != null)
 | 
			
		||||
                image_regex!!.toRegex().find(body)!!.groups[1]!!.value
 | 
			
		||||
            else if (image_css != null)
 | 
			
		||||
                Jsoup.parse(body, url).select(image_css).first().absUrl(image_attr)
 | 
			
		||||
            else
 | 
			
		||||
                throw Exception("image_regex and image_css are null")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,234 +0,0 @@
 | 
			
		||||
@file:Suppress("UNCHECKED_CAST")
 | 
			
		||||
 | 
			
		||||
package eu.kanade.tachiyomi.source.online
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import okhttp3.FormBody
 | 
			
		||||
import okhttp3.RequestBody
 | 
			
		||||
import org.jsoup.nodes.Document
 | 
			
		||||
import org.jsoup.nodes.Element
 | 
			
		||||
import java.text.ParseException
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
private fun toMap(map: Any?) = map as? Map<String, Any?>
 | 
			
		||||
 | 
			
		||||
class YamlSourceNode(uncheckedMap: Map<*, *>) {
 | 
			
		||||
 | 
			
		||||
    val map = toMap(uncheckedMap)!!
 | 
			
		||||
 | 
			
		||||
    val id: Any by map
 | 
			
		||||
 | 
			
		||||
    val name: String by map
 | 
			
		||||
 | 
			
		||||
    val host: String by map
 | 
			
		||||
 | 
			
		||||
    val lang: String by map
 | 
			
		||||
 | 
			
		||||
    val client: String?
 | 
			
		||||
        get() = map["client"] as? String
 | 
			
		||||
 | 
			
		||||
    val popular = PopularNode(toMap(map["popular"])!!)
 | 
			
		||||
 | 
			
		||||
    val latestupdates = toMap(map["latest_updates"])?.let { LatestUpdatesNode(it) }
 | 
			
		||||
 | 
			
		||||
    val search = SearchNode(toMap(map["search"])!!)
 | 
			
		||||
 | 
			
		||||
    val manga = MangaNode(toMap(map["manga"])!!)
 | 
			
		||||
 | 
			
		||||
    val chapters = ChaptersNode(toMap(map["chapters"])!!)
 | 
			
		||||
 | 
			
		||||
    val pages = PagesNode(toMap(map["pages"])!!)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface RequestableNode {
 | 
			
		||||
 | 
			
		||||
    val map: Map<String, Any?>
 | 
			
		||||
 | 
			
		||||
    val url: String
 | 
			
		||||
        get() = map["url"] as String
 | 
			
		||||
 | 
			
		||||
    val method: String?
 | 
			
		||||
        get() = map["method"] as? String
 | 
			
		||||
 | 
			
		||||
    val payload: Map<String, String>?
 | 
			
		||||
        get() = map["payload"] as? Map<String, String>
 | 
			
		||||
 | 
			
		||||
    fun createForm(): RequestBody {
 | 
			
		||||
        return FormBody.Builder().apply {
 | 
			
		||||
            payload?.let {
 | 
			
		||||
                for ((key, value) in it) {
 | 
			
		||||
                    add(key, value)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }.build()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class PopularNode(override val map: Map<String, Any?>): RequestableNode {
 | 
			
		||||
 | 
			
		||||
    val manga_css: String by map
 | 
			
		||||
 | 
			
		||||
    val next_url_css: String?
 | 
			
		||||
        get() = map["next_url_css"] as? String
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LatestUpdatesNode(override val map: Map<String, Any?>): RequestableNode {
 | 
			
		||||
 | 
			
		||||
    val manga_css: String by map
 | 
			
		||||
 | 
			
		||||
    val next_url_css: String?
 | 
			
		||||
        get() = map["next_url_css"] as? String
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SearchNode(override val map: Map<String, Any?>): RequestableNode {
 | 
			
		||||
 | 
			
		||||
    val manga_css: String by map
 | 
			
		||||
 | 
			
		||||
    val next_url_css: String?
 | 
			
		||||
        get() = map["next_url_css"] as? String
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class MangaNode(private val map: Map<String, Any?>) {
 | 
			
		||||
 | 
			
		||||
    val parts = CacheNode(toMap(map["parts"]) ?: emptyMap())
 | 
			
		||||
 | 
			
		||||
    val artist = toMap(map["artist"])?.let { SelectableNode(it) }
 | 
			
		||||
 | 
			
		||||
    val author = toMap(map["author"])?.let { SelectableNode(it) }
 | 
			
		||||
 | 
			
		||||
    val summary = toMap(map["summary"])?.let { SelectableNode(it) }
 | 
			
		||||
 | 
			
		||||
    val status = toMap(map["status"])?.let { StatusNode(it) }
 | 
			
		||||
 | 
			
		||||
    val genres = toMap(map["genres"])?.let { SelectableNode(it) }
 | 
			
		||||
 | 
			
		||||
    val cover = toMap(map["cover"])?.let { CoverNode(it) }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ChaptersNode(private val map: Map<String, Any?>) {
 | 
			
		||||
 | 
			
		||||
    val chapter_css: String by map
 | 
			
		||||
 | 
			
		||||
    val title: String by map
 | 
			
		||||
 | 
			
		||||
    val date = toMap(toMap(map["date"]))?.let { DateNode(it) }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class CacheNode(private val map: Map<String, Any?>) {
 | 
			
		||||
 | 
			
		||||
    fun get(document: Document) = map.mapValues { document.select(it.value as String).first() }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
open class SelectableNode(private val map: Map<String, Any?>) {
 | 
			
		||||
 | 
			
		||||
    val select: String by map
 | 
			
		||||
 | 
			
		||||
    val from: String?
 | 
			
		||||
        get() = map["from"] as? String
 | 
			
		||||
 | 
			
		||||
    open val attr: String?
 | 
			
		||||
        get() = map["attr"] as? String
 | 
			
		||||
 | 
			
		||||
    val capture: String?
 | 
			
		||||
        get() = map["capture"] as? String
 | 
			
		||||
 | 
			
		||||
    fun process(document: Element, cache: Map<String, Element>): String {
 | 
			
		||||
        val parent = from?.let { cache[it] } ?: document
 | 
			
		||||
        val node = parent.select(select).first()
 | 
			
		||||
        var text = attr?.let { node.attr(it) } ?: node.text()
 | 
			
		||||
        capture?.let {
 | 
			
		||||
            text = Regex(it).find(text)?.groupValues?.get(1) ?: text
 | 
			
		||||
        }
 | 
			
		||||
        return text
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class StatusNode(private val map: Map<String, Any?>) : SelectableNode(map) {
 | 
			
		||||
 | 
			
		||||
    val complete: String?
 | 
			
		||||
        get() = map["complete"] as? String
 | 
			
		||||
 | 
			
		||||
    val ongoing: String?
 | 
			
		||||
        get() = map["ongoing"] as? String
 | 
			
		||||
 | 
			
		||||
    val licensed: String?
 | 
			
		||||
        get() = map["licensed"] as? String
 | 
			
		||||
 | 
			
		||||
    fun getStatus(document: Element, cache: Map<String, Element>): Int {
 | 
			
		||||
        val text = process(document, cache)
 | 
			
		||||
        complete?.let {
 | 
			
		||||
            if (text.contains(it)) return SManga.COMPLETED
 | 
			
		||||
        }
 | 
			
		||||
        ongoing?.let {
 | 
			
		||||
            if (text.contains(it)) return SManga.ONGOING
 | 
			
		||||
        }
 | 
			
		||||
        licensed?.let {
 | 
			
		||||
            if (text.contains(it)) return SManga.LICENSED
 | 
			
		||||
        }
 | 
			
		||||
        return SManga.UNKNOWN
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class CoverNode(private val map: Map<String, Any?>) : SelectableNode(map) {
 | 
			
		||||
 | 
			
		||||
    override val attr: String?
 | 
			
		||||
        get() = map["attr"] as? String ?: "src"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class DateNode(private val map: Map<String, Any?>) : SelectableNode(map) {
 | 
			
		||||
 | 
			
		||||
    val format: String by map
 | 
			
		||||
 | 
			
		||||
    fun getDate(document: Element, cache: Map<String, Element>, formatter: SimpleDateFormat): Date {
 | 
			
		||||
        val text = process(document, cache)
 | 
			
		||||
        try {
 | 
			
		||||
            return formatter.parse(text)
 | 
			
		||||
        } catch (exception: ParseException) {}
 | 
			
		||||
 | 
			
		||||
        for (i in 0..7) {
 | 
			
		||||
            (map["day$i"] as? List<String>)?.let {
 | 
			
		||||
                it.find { it.toRegex().containsMatchIn(text) }?.let {
 | 
			
		||||
                    return Calendar.getInstance().apply { add(Calendar.DATE, -i) }.time
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Date(0)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class PagesNode(private val map: Map<String, Any?>) {
 | 
			
		||||
 | 
			
		||||
    val pages_regex: String?
 | 
			
		||||
        get() = map["pages_regex"] as? String
 | 
			
		||||
 | 
			
		||||
    val pages_css: String?
 | 
			
		||||
        get() = map["pages_css"] as? String
 | 
			
		||||
 | 
			
		||||
    val pages_attr: String?
 | 
			
		||||
        get() = map["pages_attr"] as? String ?: "value"
 | 
			
		||||
 | 
			
		||||
    val replace: String?
 | 
			
		||||
        get() = map["url_replace"] as? String
 | 
			
		||||
 | 
			
		||||
    val replacement: String?
 | 
			
		||||
        get() = map["url_replacement"] as? String
 | 
			
		||||
 | 
			
		||||
    val image_regex: String?
 | 
			
		||||
        get() = map["image_regex"] as? String
 | 
			
		||||
 | 
			
		||||
    val image_css: String?
 | 
			
		||||
        get() = map["image_css"] as? String
 | 
			
		||||
 | 
			
		||||
    val image_attr: String
 | 
			
		||||
        get() = map["image_attr"] as? String ?: "src"
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package eu.kanade.tachiyomi.source.online.english
 | 
			
		||||
 | 
			
		||||
import android.text.Html
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 | 
			
		||||
import eu.kanade.tachiyomi.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.network.POST
 | 
			
		||||
import eu.kanade.tachiyomi.network.asObservable
 | 
			
		||||
@@ -9,14 +10,11 @@ import eu.kanade.tachiyomi.source.online.LoginSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
 | 
			
		||||
import eu.kanade.tachiyomi.util.asJsoup
 | 
			
		||||
import eu.kanade.tachiyomi.util.selectText
 | 
			
		||||
import okhttp3.FormBody
 | 
			
		||||
import okhttp3.HttpUrl
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import okhttp3.*
 | 
			
		||||
import org.jsoup.nodes.Document
 | 
			
		||||
import org.jsoup.nodes.Element
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.net.URI
 | 
			
		||||
import java.text.ParseException
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
@@ -25,6 +23,9 @@ import java.util.regex.Pattern
 | 
			
		||||
 | 
			
		||||
class Batoto : ParsedHttpSource(), LoginSource {
 | 
			
		||||
 | 
			
		||||
    // TODO remove
 | 
			
		||||
    private val preferences: PreferencesHelper by injectLazy()
 | 
			
		||||
 | 
			
		||||
    override val id: Long = 1
 | 
			
		||||
 | 
			
		||||
    override val name = "Batoto"
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
 | 
			
		||||
                val top = child.bottom + params.bottomMargin
 | 
			
		||||
                val bottom = top + divider.intrinsicHeight
 | 
			
		||||
                val left = parent.paddingLeft + holder.margin
 | 
			
		||||
                val right = parent.paddingRight + holder.margin
 | 
			
		||||
                val right = parent.width - parent.paddingRight - holder.margin
 | 
			
		||||
 | 
			
		||||
                divider.setBounds(left, top, right, bottom)
 | 
			
		||||
                divider.draw(c)
 | 
			
		||||
@@ -41,4 +41,4 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
 | 
			
		||||
        outRect.set(0, 0, 0, divider.intrinsicHeight)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.extension
 | 
			
		||||
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.util.getResourceColor
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Adapter that holds the catalogue cards.
 | 
			
		||||
 *
 | 
			
		||||
 * @param controller instance of [ExtensionController].
 | 
			
		||||
 */
 | 
			
		||||
class ExtensionAdapter(val controller: ExtensionController) :
 | 
			
		||||
        FlexibleAdapter<IFlexible<*>>(null, controller, true) {
 | 
			
		||||
 | 
			
		||||
    val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card)
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        setDisplayHeadersAtStartUp(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Listener for browse item clicks.
 | 
			
		||||
     */
 | 
			
		||||
    val buttonClickListener: ExtensionAdapter.OnButtonClickListener = controller
 | 
			
		||||
 | 
			
		||||
    interface OnButtonClickListener {
 | 
			
		||||
        fun onButtonClick(position: Int)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,132 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.extension
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.LinearLayoutManager
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import com.jakewharton.rxbinding.support.v4.widget.refreshes
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.extension.model.Extension
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
 | 
			
		||||
import kotlinx.android.synthetic.main.extension_controller.*
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Controller to manage the catalogues available in the app.
 | 
			
		||||
 */
 | 
			
		||||
open class ExtensionController : NucleusController<ExtensionPresenter>(),
 | 
			
		||||
        ExtensionAdapter.OnButtonClickListener,
 | 
			
		||||
        FlexibleAdapter.OnItemClickListener,
 | 
			
		||||
        FlexibleAdapter.OnItemLongClickListener,
 | 
			
		||||
        ExtensionTrustDialog.Listener {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Adapter containing the list of manga from the catalogue.
 | 
			
		||||
     */
 | 
			
		||||
    private var adapter: FlexibleAdapter<IFlexible<*>>? = null
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        setHasOptionsMenu(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getTitle(): String? {
 | 
			
		||||
        return applicationContext?.getString(R.string.label_extensions)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createPresenter(): ExtensionPresenter {
 | 
			
		||||
        return ExtensionPresenter()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
 | 
			
		||||
        return inflater.inflate(R.layout.extension_controller, container, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View) {
 | 
			
		||||
        super.onViewCreated(view)
 | 
			
		||||
 | 
			
		||||
        ext_swipe_refresh.isRefreshing = true
 | 
			
		||||
        ext_swipe_refresh.refreshes().subscribeUntilDestroy {
 | 
			
		||||
            presenter.findAvailableExtensions()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Initialize adapter, scroll listener and recycler views
 | 
			
		||||
        adapter = ExtensionAdapter(this)
 | 
			
		||||
        // Create recycler and set adapter.
 | 
			
		||||
        ext_recycler.layoutManager = LinearLayoutManager(view.context)
 | 
			
		||||
        ext_recycler.adapter = adapter
 | 
			
		||||
        ext_recycler.addItemDecoration(ExtensionDividerItemDecoration(view.context))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView(view: View) {
 | 
			
		||||
        adapter = null
 | 
			
		||||
        super.onDestroyView(view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onButtonClick(position: Int) {
 | 
			
		||||
        val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
 | 
			
		||||
        when (extension) {
 | 
			
		||||
            is Extension.Installed -> {
 | 
			
		||||
                if (!extension.hasUpdate) {
 | 
			
		||||
                    openDetails(extension)
 | 
			
		||||
                } else {
 | 
			
		||||
                    presenter.updateExtension(extension)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            is Extension.Available -> {
 | 
			
		||||
                presenter.installExtension(extension)
 | 
			
		||||
            }
 | 
			
		||||
            is Extension.Untrusted -> {
 | 
			
		||||
                openTrustDialog(extension)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onItemClick(position: Int): Boolean {
 | 
			
		||||
        val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false
 | 
			
		||||
        if (extension is Extension.Installed) {
 | 
			
		||||
            openDetails(extension)
 | 
			
		||||
        } else if (extension is Extension.Untrusted) {
 | 
			
		||||
            openTrustDialog(extension)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onItemLongClick(position: Int) {
 | 
			
		||||
        val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
 | 
			
		||||
        if (extension is Extension.Installed || extension is Extension.Untrusted) {
 | 
			
		||||
            uninstallExtension(extension.pkgName)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun openDetails(extension: Extension.Installed) {
 | 
			
		||||
        val controller = ExtensionDetailsController(extension.pkgName)
 | 
			
		||||
        router.pushController(controller.withFadeTransaction())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun openTrustDialog(extension: Extension.Untrusted) {
 | 
			
		||||
        ExtensionTrustDialog(this, extension.signatureHash, extension.pkgName)
 | 
			
		||||
                .showDialog(router)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setExtensions(extensions: List<ExtensionItem>) {
 | 
			
		||||
        ext_swipe_refresh?.isRefreshing = false
 | 
			
		||||
        adapter?.updateDataSet(extensions)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun downloadUpdate(item: ExtensionItem) {
 | 
			
		||||
        adapter?.updateItem(item, item.installStep)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun trustSignature(signatureHash: String) {
 | 
			
		||||
        presenter.trustSignature(signatureHash)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun uninstallExtension(pkgName: String) {
 | 
			
		||||
        presenter.uninstallExtension(pkgName)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,190 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.extension
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v7.preference.*
 | 
			
		||||
import android.support.v7.preference.internal.AbstractMultiSelectListPreference
 | 
			
		||||
import android.support.v7.widget.DividerItemDecoration
 | 
			
		||||
import android.support.v7.widget.DividerItemDecoration.VERTICAL
 | 
			
		||||
import android.support.v7.widget.LinearLayoutManager
 | 
			
		||||
import android.util.TypedValue
 | 
			
		||||
import android.view.ContextThemeWrapper
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import com.jakewharton.rxbinding.view.clicks
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
 | 
			
		||||
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
 | 
			
		||||
import eu.kanade.tachiyomi.source.ConfigurableSource
 | 
			
		||||
import eu.kanade.tachiyomi.source.Source
 | 
			
		||||
import eu.kanade.tachiyomi.source.online.LoginSource
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.setting.preferenceCategory
 | 
			
		||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
 | 
			
		||||
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
 | 
			
		||||
import kotlinx.android.synthetic.main.extension_detail_controller.*
 | 
			
		||||
 | 
			
		||||
@SuppressLint("RestrictedApi")
 | 
			
		||||
class ExtensionDetailsController(bundle: Bundle? = null) :
 | 
			
		||||
        NucleusController<ExtensionDetailsPresenter>(bundle),
 | 
			
		||||
        PreferenceManager.OnDisplayPreferenceDialogListener,
 | 
			
		||||
        DialogPreference.TargetFragment,
 | 
			
		||||
        SourceLoginDialog.Listener {
 | 
			
		||||
 | 
			
		||||
    private var lastOpenPreferencePosition: Int? = null
 | 
			
		||||
 | 
			
		||||
    private var preferenceScreen: PreferenceScreen? = null
 | 
			
		||||
 | 
			
		||||
    constructor(pkgName: String) : this(Bundle().apply {
 | 
			
		||||
        putString(PKGNAME_KEY, pkgName)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
 | 
			
		||||
        return inflater.inflate(R.layout.extension_detail_controller, container, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createPresenter(): ExtensionDetailsPresenter {
 | 
			
		||||
        return ExtensionDetailsPresenter(args.getString(PKGNAME_KEY))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getTitle(): String? {
 | 
			
		||||
        return resources?.getString(R.string.label_extension_info)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @SuppressLint("PrivateResource")
 | 
			
		||||
    override fun onViewCreated(view: View) {
 | 
			
		||||
        super.onViewCreated(view)
 | 
			
		||||
 | 
			
		||||
        val extension = presenter.extension
 | 
			
		||||
        val context = view.context
 | 
			
		||||
 | 
			
		||||
        extension_title.text = extension.name
 | 
			
		||||
        extension_version.text = context.getString(R.string.ext_version_info, extension.versionName)
 | 
			
		||||
        extension_lang.text = context.getString(R.string.ext_language_info, extension.getLocalizedLang(context))
 | 
			
		||||
        extension_pkg.text = extension.pkgName
 | 
			
		||||
        extension.getApplicationIcon(context)?.let { extension_icon.setImageDrawable(it) }
 | 
			
		||||
        extension_uninstall_button.clicks().subscribeUntilDestroy {
 | 
			
		||||
            presenter.uninstallExtension()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val themedContext by lazy { getPreferenceThemeContext() }
 | 
			
		||||
        val manager = PreferenceManager(themedContext)
 | 
			
		||||
        manager.preferenceDataStore = EmptyPreferenceDataStore()
 | 
			
		||||
        manager.onDisplayPreferenceDialogListener = this
 | 
			
		||||
        val screen = manager.createPreferenceScreen(themedContext)
 | 
			
		||||
        preferenceScreen = screen
 | 
			
		||||
 | 
			
		||||
        val multiSource = extension.sources.size > 1
 | 
			
		||||
 | 
			
		||||
        for (source in extension.sources) {
 | 
			
		||||
            if (source is ConfigurableSource) {
 | 
			
		||||
                addPreferencesForSource(screen, source, multiSource)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        manager.setPreferences(screen)
 | 
			
		||||
 | 
			
		||||
        extension_prefs_recycler.layoutManager = LinearLayoutManager(context)
 | 
			
		||||
        extension_prefs_recycler.adapter = PreferenceGroupAdapter(screen)
 | 
			
		||||
        extension_prefs_recycler.addItemDecoration(DividerItemDecoration(context, VERTICAL))
 | 
			
		||||
 | 
			
		||||
        if (screen.preferenceCount == 0) {
 | 
			
		||||
            extension_prefs_empty_view.show(R.drawable.ic_no_settings,
 | 
			
		||||
                    R.string.ext_empty_preferences)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView(view: View) {
 | 
			
		||||
        preferenceScreen = null
 | 
			
		||||
        super.onDestroyView(view)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onExtensionUninstalled() {
 | 
			
		||||
        router.popCurrentController()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onSaveInstanceState(outState: Bundle) {
 | 
			
		||||
        lastOpenPreferencePosition?.let { outState.putInt(LASTOPENPREFERENCE_KEY, it) }
 | 
			
		||||
        super.onSaveInstanceState(outState)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
 | 
			
		||||
        super.onRestoreInstanceState(savedInstanceState)
 | 
			
		||||
        lastOpenPreferencePosition = savedInstanceState.get(LASTOPENPREFERENCE_KEY) as? Int
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun addPreferencesForSource(screen: PreferenceScreen, source: Source, multiSource: Boolean) {
 | 
			
		||||
        val context = screen.context
 | 
			
		||||
 | 
			
		||||
        // TODO 
 | 
			
		||||
        val dataStore = SharedPreferencesDataStore(/*if (source is HttpSource) {
 | 
			
		||||
            source.preferences
 | 
			
		||||
        } else {*/
 | 
			
		||||
            context.getSharedPreferences("source_${source.id}", Context.MODE_PRIVATE)
 | 
			
		||||
        /*}*/)
 | 
			
		||||
 | 
			
		||||
        if (source is ConfigurableSource) {
 | 
			
		||||
            if (multiSource) {
 | 
			
		||||
                screen.preferenceCategory {
 | 
			
		||||
                    title = source.toString()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val newScreen = screen.preferenceManager.createPreferenceScreen(context)
 | 
			
		||||
            source.setupPreferenceScreen(newScreen)
 | 
			
		||||
 | 
			
		||||
            for (i in 0 until newScreen.preferenceCount) {
 | 
			
		||||
                val pref = newScreen.getPreference(i)
 | 
			
		||||
                pref.preferenceDataStore = dataStore
 | 
			
		||||
                pref.order = Int.MAX_VALUE // reset to default order
 | 
			
		||||
                screen.addPreference(pref)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getPreferenceThemeContext(): Context {
 | 
			
		||||
        val tv = TypedValue()
 | 
			
		||||
        activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true)
 | 
			
		||||
        return ContextThemeWrapper(activity, tv.resourceId)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDisplayPreferenceDialog(preference: Preference) {
 | 
			
		||||
        if (!isAttached) return
 | 
			
		||||
 | 
			
		||||
        val screen = preference.parent!!
 | 
			
		||||
 | 
			
		||||
        lastOpenPreferencePosition = (0 until screen.preferenceCount).indexOfFirst {
 | 
			
		||||
            screen.getPreference(it) === preference
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val f = when (preference) {
 | 
			
		||||
            is EditTextPreference -> EditTextPreferenceDialogController
 | 
			
		||||
                    .newInstance(preference.getKey())
 | 
			
		||||
            is ListPreference -> ListPreferenceDialogController
 | 
			
		||||
                    .newInstance(preference.getKey())
 | 
			
		||||
            is AbstractMultiSelectListPreference -> MultiSelectListPreferenceDialogController
 | 
			
		||||
                    .newInstance(preference.getKey())
 | 
			
		||||
            else -> throw IllegalArgumentException("Tried to display dialog for unknown " +
 | 
			
		||||
                    "preference type. Did you forget to override onDisplayPreferenceDialog()?")
 | 
			
		||||
        }
 | 
			
		||||
        f.targetController = this
 | 
			
		||||
        f.showDialog(router)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun findPreference(key: CharSequence?): Preference {
 | 
			
		||||
        return preferenceScreen!!.getPreference(lastOpenPreferencePosition!!)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun loginDialogClosed(source: LoginSource) {
 | 
			
		||||
        val lastOpen = lastOpenPreferencePosition ?: return
 | 
			
		||||
        (preferenceScreen?.getPreference(lastOpen) as? LoginPreference)?.notifyChanged()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private companion object {
 | 
			
		||||
        const val PKGNAME_KEY = "pkg_name"
 | 
			
		||||
        const val LASTOPENPREFERENCE_KEY = "last_open_preference"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.extension
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import eu.kanade.tachiyomi.extension.ExtensionManager
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
 | 
			
		||||
class ExtensionDetailsPresenter(
 | 
			
		||||
        val pkgName: String,
 | 
			
		||||
        private val extensionManager: ExtensionManager = Injekt.get()
 | 
			
		||||
) : BasePresenter<ExtensionDetailsController>() {
 | 
			
		||||
 | 
			
		||||
    val extension = extensionManager.installedExtensions.first { it.pkgName == pkgName }
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedState)
 | 
			
		||||
 | 
			
		||||
        bindToUninstalledExtension()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun bindToUninstalledExtension() {
 | 
			
		||||
        extensionManager.getInstalledExtensionsObservable()
 | 
			
		||||
                .skip(1)
 | 
			
		||||
                .filter { extensions -> extensions.none { it.pkgName == pkgName } }
 | 
			
		||||
                .map { Unit }
 | 
			
		||||
                .take(1)
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribeFirst({ view, _ ->
 | 
			
		||||
                    view.onExtensionUninstalled()
 | 
			
		||||
                })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun uninstallExtension() {
 | 
			
		||||
        extensionManager.uninstallExtension(extension.pkgName)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,44 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.extension
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.Canvas
 | 
			
		||||
import android.graphics.Rect
 | 
			
		||||
import android.graphics.drawable.Drawable
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
 | 
			
		||||
class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
 | 
			
		||||
 | 
			
		||||
    private val divider: Drawable
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider))
 | 
			
		||||
        divider = a.getDrawable(0)
 | 
			
		||||
        a.recycle()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
 | 
			
		||||
        val childCount = parent.childCount
 | 
			
		||||
        for (i in 0 until childCount - 1) {
 | 
			
		||||
            val child = parent.getChildAt(i)
 | 
			
		||||
            val holder = parent.getChildViewHolder(child)
 | 
			
		||||
            if (holder is ExtensionHolder &&
 | 
			
		||||
                    parent.getChildViewHolder(parent.getChildAt(i + 1)) is ExtensionHolder) {
 | 
			
		||||
                val params = child.layoutParams as RecyclerView.LayoutParams
 | 
			
		||||
                val top = child.bottom + params.bottomMargin
 | 
			
		||||
                val bottom = top + divider.intrinsicHeight
 | 
			
		||||
                val left = parent.paddingLeft + holder.margin
 | 
			
		||||
                val right = parent.width - parent.paddingRight - holder.margin
 | 
			
		||||
 | 
			
		||||
                divider.setBounds(left, top, right, bottom)
 | 
			
		||||
                divider.draw(c)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
 | 
			
		||||
                                state: RecyclerView.State) {
 | 
			
		||||
        outRect.set(0, 0, 0, divider.intrinsicHeight)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.extension
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
 | 
			
		||||
import kotlinx.android.synthetic.main.extension_card_header.*
 | 
			
		||||
 | 
			
		||||
class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter<*>) :
 | 
			
		||||
        BaseFlexibleViewHolder(view, adapter, true) {
 | 
			
		||||
 | 
			
		||||
    @SuppressLint("SetTextI18n")
 | 
			
		||||
    fun bind(item: ExtensionGroupItem) {
 | 
			
		||||
        title.text = when {
 | 
			
		||||
            item.installed -> itemView.context.getString(R.string.ext_installed)
 | 
			
		||||
            else -> itemView.context.getString(R.string.ext_available)
 | 
			
		||||
        } + " (" + item.size + ")"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,50 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.extension
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Item that contains the language header.
 | 
			
		||||
 *
 | 
			
		||||
 * @param code The lang code.
 | 
			
		||||
 */
 | 
			
		||||
data class ExtensionGroupItem(val installed: Boolean, val size: Int) : AbstractHeaderItem<ExtensionGroupHolder>() {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the layout resource of this item.
 | 
			
		||||
     */
 | 
			
		||||
    override fun getLayoutRes(): Int {
 | 
			
		||||
        return R.layout.extension_card_header
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new view holder for this item.
 | 
			
		||||
     */
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): ExtensionGroupHolder {
 | 
			
		||||
        return ExtensionGroupHolder(view, adapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Binds this item to the given view holder.
 | 
			
		||||
     */
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: ExtensionGroupHolder,
 | 
			
		||||
                                position: Int, payloads: List<Any?>?) {
 | 
			
		||||
 | 
			
		||||
        holder.bind(this)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (this === other) return true
 | 
			
		||||
        if (other is ExtensionGroupItem) {
 | 
			
		||||
            return installed == other.installed
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return installed.hashCode()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,88 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.extension
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.glide.GlideApp
 | 
			
		||||
import eu.kanade.tachiyomi.extension.model.Extension
 | 
			
		||||
import eu.kanade.tachiyomi.extension.model.InstallStep
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
 | 
			
		||||
import io.github.mthli.slice.Slice
 | 
			
		||||
import kotlinx.android.synthetic.main.extension_card_item.*
 | 
			
		||||
 | 
			
		||||
class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
 | 
			
		||||
        BaseFlexibleViewHolder(view, adapter),
 | 
			
		||||
        SlicedHolder {
 | 
			
		||||
 | 
			
		||||
    override val slice = Slice(card).apply {
 | 
			
		||||
        setColor(adapter.cardBackground)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override val viewToSlice: View
 | 
			
		||||
        get() = card
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        ext_button.setOnClickListener {
 | 
			
		||||
            adapter.buttonClickListener.onButtonClick(adapterPosition)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun bind(item: ExtensionItem) {
 | 
			
		||||
        val extension = item.extension
 | 
			
		||||
        setCardEdges(item)
 | 
			
		||||
 | 
			
		||||
        // Set source name
 | 
			
		||||
        ext_title.text = extension.name
 | 
			
		||||
        version.text = extension.versionName
 | 
			
		||||
        lang.text = if (extension !is Extension.Untrusted) {
 | 
			
		||||
            extension.getLocalizedLang(itemView.context)
 | 
			
		||||
        } else {
 | 
			
		||||
            itemView.context.getString(R.string.ext_untrusted).toUpperCase()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        GlideApp.with(itemView.context).clear(image)
 | 
			
		||||
        if (extension is Extension.Available) {
 | 
			
		||||
            GlideApp.with(itemView.context)
 | 
			
		||||
                    .load(extension.iconUrl)
 | 
			
		||||
                    .into(image)
 | 
			
		||||
        } else {
 | 
			
		||||
            extension.getApplicationIcon(itemView.context)?.let { image.setImageDrawable(it) }
 | 
			
		||||
        }
 | 
			
		||||
        bindButton(item)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun bindButton(item: ExtensionItem) = with(ext_button) {
 | 
			
		||||
        isEnabled = true
 | 
			
		||||
        isClickable = true
 | 
			
		||||
        isActivated = false
 | 
			
		||||
 | 
			
		||||
        val extension = item.extension
 | 
			
		||||
 | 
			
		||||
        val installStep = item.installStep
 | 
			
		||||
        if (installStep != null) {
 | 
			
		||||
            setText(when (installStep) {
 | 
			
		||||
                InstallStep.Pending -> R.string.ext_pending
 | 
			
		||||
                InstallStep.Downloading -> R.string.ext_downloading
 | 
			
		||||
                InstallStep.Installing -> R.string.ext_installing
 | 
			
		||||
                InstallStep.Installed -> R.string.ext_installed
 | 
			
		||||
                InstallStep.Error -> R.string.action_retry
 | 
			
		||||
            })
 | 
			
		||||
            if (installStep != InstallStep.Error) {
 | 
			
		||||
                isEnabled = false
 | 
			
		||||
                isClickable = false
 | 
			
		||||
            }
 | 
			
		||||
        } else if (extension is Extension.Installed) {
 | 
			
		||||
            if (extension.hasUpdate) {
 | 
			
		||||
                isActivated = true
 | 
			
		||||
                setText(R.string.ext_update)
 | 
			
		||||
            } else {
 | 
			
		||||
                setText(R.string.ext_details)
 | 
			
		||||
            }
 | 
			
		||||
        } else if (extension is Extension.Untrusted) {
 | 
			
		||||
            setText(R.string.ext_trust)
 | 
			
		||||
        } else {
 | 
			
		||||
            setText(R.string.ext_install)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,59 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.extension
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.extension.model.Extension
 | 
			
		||||
import eu.kanade.tachiyomi.extension.model.InstallStep
 | 
			
		||||
import eu.kanade.tachiyomi.source.CatalogueSource
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Item that contains source information.
 | 
			
		||||
 *
 | 
			
		||||
 * @param source Instance of [CatalogueSource] containing source information.
 | 
			
		||||
 * @param header The header for this item.
 | 
			
		||||
 */
 | 
			
		||||
data class ExtensionItem(val extension: Extension,
 | 
			
		||||
                         val header: ExtensionGroupItem? = null,
 | 
			
		||||
                         val installStep: InstallStep? = null) :
 | 
			
		||||
        AbstractSectionableItem<ExtensionHolder, ExtensionGroupItem>(header) {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the layout resource of this item.
 | 
			
		||||
     */
 | 
			
		||||
    override fun getLayoutRes(): Int {
 | 
			
		||||
        return R.layout.extension_card_item
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Creates a new view holder for this item.
 | 
			
		||||
     */
 | 
			
		||||
    override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): ExtensionHolder {
 | 
			
		||||
        return ExtensionHolder(view, adapter as ExtensionAdapter)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Binds this item to the given view holder.
 | 
			
		||||
     */
 | 
			
		||||
    override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: ExtensionHolder,
 | 
			
		||||
                                position: Int, payloads: List<Any?>?) {
 | 
			
		||||
 | 
			
		||||
        if (payloads == null || payloads.isEmpty()) {
 | 
			
		||||
            holder.bind(this)
 | 
			
		||||
        } else {
 | 
			
		||||
            holder.bindButton(this)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (this === other) return true
 | 
			
		||||
        if (javaClass != other?.javaClass) return false
 | 
			
		||||
        return extension.pkgName == (other as ExtensionItem).extension.pkgName
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int {
 | 
			
		||||
        return extension.pkgName.hashCode()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,130 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.extension
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import eu.kanade.tachiyomi.extension.ExtensionManager
 | 
			
		||||
import eu.kanade.tachiyomi.extension.model.Extension
 | 
			
		||||
import eu.kanade.tachiyomi.extension.model.InstallStep
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
private typealias ExtensionTuple
 | 
			
		||||
        = Triple<List<Extension.Installed>, List<Extension.Untrusted>, List<Extension.Available>>
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Presenter of [ExtensionController].
 | 
			
		||||
 */
 | 
			
		||||
open class ExtensionPresenter(
 | 
			
		||||
        private val extensionManager: ExtensionManager = Injekt.get()
 | 
			
		||||
) : BasePresenter<ExtensionController>() {
 | 
			
		||||
 | 
			
		||||
    private var extensions = emptyList<ExtensionItem>()
 | 
			
		||||
 | 
			
		||||
    private var currentDownloads = hashMapOf<String, InstallStep>()
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedState)
 | 
			
		||||
 | 
			
		||||
        bindToExtensionsObservable()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun bindToExtensionsObservable(): Subscription {
 | 
			
		||||
        val installedObservable = extensionManager.getInstalledExtensionsObservable()
 | 
			
		||||
        val untrustedObservable = extensionManager.getUntrustedExtensionsObservable()
 | 
			
		||||
        val availableObservable = extensionManager.getAvailableExtensionsObservable()
 | 
			
		||||
                .startWith(emptyList<Extension.Available>())
 | 
			
		||||
 | 
			
		||||
        return Observable.combineLatest(installedObservable, untrustedObservable, availableObservable)
 | 
			
		||||
                { installed, untrusted, available -> Triple(installed, untrusted, available) }
 | 
			
		||||
                .debounce(100, TimeUnit.MILLISECONDS)
 | 
			
		||||
                .map(::toItems)
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribeLatestCache({ view, _ -> view.setExtensions(extensions) })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Synchronized
 | 
			
		||||
    private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
 | 
			
		||||
        val (installed, untrusted, available) = tuple
 | 
			
		||||
 | 
			
		||||
        val items = mutableListOf<ExtensionItem>()
 | 
			
		||||
 | 
			
		||||
        val installedSorted = installed.sortedWith(compareBy({ !it.hasUpdate }, { it.name }))
 | 
			
		||||
        val untrustedSorted = untrusted.sortedBy { it.name }
 | 
			
		||||
        val availableSorted = available
 | 
			
		||||
                // Filter out already installed extensions
 | 
			
		||||
                .filter { avail -> installed.none { it.pkgName == avail.pkgName }
 | 
			
		||||
                        && untrusted.none { it.pkgName == avail.pkgName } }
 | 
			
		||||
                .sortedBy { it.name }
 | 
			
		||||
 | 
			
		||||
        if (installedSorted.isNotEmpty() || untrustedSorted.isNotEmpty()) {
 | 
			
		||||
            val header = ExtensionGroupItem(true, installedSorted.size + untrustedSorted.size)
 | 
			
		||||
            items += installedSorted.map { extension ->
 | 
			
		||||
                ExtensionItem(extension, header, currentDownloads[extension.pkgName])
 | 
			
		||||
            }
 | 
			
		||||
            items += untrustedSorted.map { extension ->
 | 
			
		||||
                ExtensionItem(extension, header)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (availableSorted.isNotEmpty()) {
 | 
			
		||||
            val header = ExtensionGroupItem(false, availableSorted.size)
 | 
			
		||||
            items += availableSorted.map { extension ->
 | 
			
		||||
                ExtensionItem(extension, header, currentDownloads[extension.pkgName])
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.extensions = items
 | 
			
		||||
        return items
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Synchronized
 | 
			
		||||
    private fun updateInstallStep(extension: Extension, state: InstallStep): ExtensionItem? {
 | 
			
		||||
        val extensions = extensions.toMutableList()
 | 
			
		||||
        val position = extensions.indexOfFirst { it.extension.pkgName == extension.pkgName }
 | 
			
		||||
 | 
			
		||||
        return if (position != -1) {
 | 
			
		||||
            val item = extensions[position].copy(installStep = state)
 | 
			
		||||
            extensions[position] = item
 | 
			
		||||
 | 
			
		||||
            this.extensions = extensions
 | 
			
		||||
            item
 | 
			
		||||
        } else {
 | 
			
		||||
            null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun installExtension(extension: Extension.Available) {
 | 
			
		||||
        extensionManager.installExtension(extension).subscribeToInstallUpdate(extension)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateExtension(extension: Extension.Installed) {
 | 
			
		||||
        extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun Observable<InstallStep>.subscribeToInstallUpdate(extension: Extension) {
 | 
			
		||||
        this.doOnNext { currentDownloads.put(extension.pkgName, it) }
 | 
			
		||||
                .doOnUnsubscribe { currentDownloads.remove(extension.pkgName) }
 | 
			
		||||
                .map { state -> updateInstallStep(extension, state) }
 | 
			
		||||
                .subscribeWithView({ view, item ->
 | 
			
		||||
                    if (item != null) {
 | 
			
		||||
                        view.downloadUpdate(item)
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun uninstallExtension(pkgName: String) {
 | 
			
		||||
        extensionManager.uninstallExtension(pkgName)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun findAvailableExtensions() {
 | 
			
		||||
        extensionManager.findAvailableExtensions()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun trustSignature(signatureHash: String) {
 | 
			
		||||
        extensionManager.trustSignature(signatureHash)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,44 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.extension
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import com.bluelinelabs.conductor.Controller
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
 | 
			
		||||
 | 
			
		||||
class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
 | 
			
		||||
        where T : Controller, T: ExtensionTrustDialog.Listener {
 | 
			
		||||
 | 
			
		||||
    constructor(target: T, signatureHash: String, pkgName: String) : this(Bundle().apply {
 | 
			
		||||
        putString(SIGNATURE_KEY, signatureHash)
 | 
			
		||||
        putString(PKGNAME_KEY, pkgName)
 | 
			
		||||
    }) {
 | 
			
		||||
        targetController = target
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
 | 
			
		||||
        return MaterialDialog.Builder(activity!!)
 | 
			
		||||
                .title(R.string.untrusted_extension)
 | 
			
		||||
                .content(R.string.untrusted_extension_message)
 | 
			
		||||
                .positiveText(R.string.ext_trust)
 | 
			
		||||
                .negativeText(R.string.ext_uninstall)
 | 
			
		||||
                .onPositive { _, _ ->
 | 
			
		||||
                    (targetController as? Listener)?.trustSignature(args.getString(SIGNATURE_KEY))
 | 
			
		||||
                }
 | 
			
		||||
                .onNegative { _, _ ->
 | 
			
		||||
                    (targetController as? Listener)?.uninstallExtension(args.getString(PKGNAME_KEY))
 | 
			
		||||
                }
 | 
			
		||||
                .build()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private companion object {
 | 
			
		||||
        const val SIGNATURE_KEY = "signature_key"
 | 
			
		||||
        const val PKGNAME_KEY = "pkgname_key"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    interface Listener {
 | 
			
		||||
        fun trustSignature(signatureHash: String)
 | 
			
		||||
        fun uninstallExtension(pkgName: String)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,28 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.extension
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.pm.PackageManager
 | 
			
		||||
import android.graphics.drawable.Drawable
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.extension.model.Extension
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
fun Extension.getLocalizedLang(context: Context): String {
 | 
			
		||||
    return when (lang) {
 | 
			
		||||
        null -> ""
 | 
			
		||||
        "" -> context.getString(R.string.other_source)
 | 
			
		||||
        "all" -> context.getString(R.string.all_lang)
 | 
			
		||||
        else -> {
 | 
			
		||||
            val locale = Locale(lang)
 | 
			
		||||
            locale.getDisplayName(locale).capitalize()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Extension.getApplicationIcon(context: Context): Drawable? {
 | 
			
		||||
    return try {
 | 
			
		||||
        context.packageManager.getApplicationIcon(pkgName)
 | 
			
		||||
    } catch (e: PackageManager.NameNotFoundException) {
 | 
			
		||||
        null
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
 | 
			
		||||
import eu.kanade.tachiyomi.ui.base.controller.*
 | 
			
		||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.download.DownloadController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.extension.ExtensionController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.library.LibraryController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.manga.MangaController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
 | 
			
		||||
@@ -80,6 +81,7 @@ class MainActivity : BaseActivity() {
 | 
			
		||||
                    R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id)
 | 
			
		||||
                    R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id)
 | 
			
		||||
                    R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id)
 | 
			
		||||
                    R.id.nav_drawer_extensions -> setRoot(ExtensionController(), id)
 | 
			
		||||
                    R.id.nav_drawer_downloads -> {
 | 
			
		||||
                        router.pushController(DownloadController().withFadeTransaction())
 | 
			
		||||
                    }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,6 @@ import android.support.v4.os.EnvironmentCompat
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.InputStream
 | 
			
		||||
import java.net.URLConnection
 | 
			
		||||
import java.security.MessageDigest
 | 
			
		||||
import java.security.NoSuchAlgorithmException
 | 
			
		||||
 | 
			
		||||
object DiskUtil {
 | 
			
		||||
 | 
			
		||||
@@ -52,16 +50,7 @@ object DiskUtil {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun hashKeyForDisk(key: String): String {
 | 
			
		||||
        return try {
 | 
			
		||||
            val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
 | 
			
		||||
            val sb = StringBuilder()
 | 
			
		||||
            bytes.forEach { byte ->
 | 
			
		||||
                sb.append(Integer.toHexString(byte.toInt() and 0xFF or 0x100).substring(1, 3))
 | 
			
		||||
            }
 | 
			
		||||
            sb.toString()
 | 
			
		||||
        } catch (e: NoSuchAlgorithmException) {
 | 
			
		||||
            key.hashCode().toString()
 | 
			
		||||
        }
 | 
			
		||||
        return Hash.md5(key)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getDirectorySize(f: File): Long {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								app/src/main/java/eu/kanade/tachiyomi/util/Hash.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								app/src/main/java/eu/kanade/tachiyomi/util/Hash.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
package eu.kanade.tachiyomi.util
 | 
			
		||||
 | 
			
		||||
import java.security.MessageDigest
 | 
			
		||||
 | 
			
		||||
object Hash {
 | 
			
		||||
 | 
			
		||||
    private val chars = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
 | 
			
		||||
            'a', 'b', 'c', 'd', 'e', 'f')
 | 
			
		||||
 | 
			
		||||
    private val MD5 get() = MessageDigest.getInstance("MD5")
 | 
			
		||||
 | 
			
		||||
    private val SHA256 get() = MessageDigest.getInstance("SHA-256")
 | 
			
		||||
 | 
			
		||||
    fun sha256(bytes: ByteArray): String {
 | 
			
		||||
        return encodeHex(SHA256.digest(bytes))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun sha256(string: String): String {
 | 
			
		||||
        return sha256(string.toByteArray())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun md5(bytes: ByteArray): String {
 | 
			
		||||
        return encodeHex(MD5.digest(bytes))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun md5(string: String): String {
 | 
			
		||||
        return md5(string.toByteArray())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun encodeHex(data: ByteArray): String {
 | 
			
		||||
        val l = data.size
 | 
			
		||||
        val out = CharArray(l shl 1)
 | 
			
		||||
        var i = 0
 | 
			
		||||
        var j = 0
 | 
			
		||||
        while (i < l) {
 | 
			
		||||
            out[j++] = chars[(240 and data[i].toInt()).ushr(4)]
 | 
			
		||||
            out[j++] = chars[15 and data[i].toInt()]
 | 
			
		||||
            i++
 | 
			
		||||
        }
 | 
			
		||||
        return String(out)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -12,10 +12,15 @@ class IntListPreference @JvmOverloads constructor(context: Context, attrs: Attri
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getPersistedString(defaultReturnValue: String?): String? {
 | 
			
		||||
        if (sharedPreferences.contains(key)) {
 | 
			
		||||
            return getPersistedInt(0).toString()
 | 
			
		||||
        // When the underlying preference is using a PreferenceDataStore, there's no way (for now)
 | 
			
		||||
        // to check if a value is in the store, so we use a most likely unused value as workaround
 | 
			
		||||
        val defaultIntValue = Int.MIN_VALUE + 1
 | 
			
		||||
 | 
			
		||||
        val value = getPersistedInt(defaultIntValue)
 | 
			
		||||
        return if (value != defaultIntValue) {
 | 
			
		||||
            value.toString()
 | 
			
		||||
        } else {
 | 
			
		||||
            return defaultReturnValue
 | 
			
		||||
            defaultReturnValue
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user