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.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.source.online.all.EHentaiMetadata import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.YamlHttpSource import eu.kanade.tachiyomi.source.online.all.NHentai import eu.kanade.tachiyomi.source.online.all.PervEden 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 exh.* import org.yaml.snakeyaml.Yaml import rx.Observable import timber.log.Timber import uy.kohesive.injekt.injectLazy import java.io.File open class SourceManager(private val context: Context) { private val prefs: PreferencesHelper by injectLazy() private val sourcesMap = mutableMapOf() init { //Recreate sources when they change val prefEntries = arrayOf( prefs.enableExhentai(), prefs.imageQuality(), prefs.useHentaiAtHome(), prefs.useJapaneseTitle(), prefs.ehSearchSize(), prefs.thumbnailRows() ).map { it.asObservable() } Observable.merge(prefEntries).skip(prefEntries.size - 1).subscribe { sourcesMap.clear() createSources() } } open fun get(sourceKey: Long): Source? { return sourcesMap[sourceKey] } fun getOnlineSources() = sourcesMap.values.filterIsInstance() fun getCatalogueSources() = sourcesMap.values.filterIsInstance() private fun createSources() { createExtensionSources().forEach { registerSource(it) } createYamlSources().forEach { registerSource(it) } createInternalSources().forEach { registerSource(it) } //EH createEHSources().forEach { registerSource(it) } } private fun registerSource(source: Source, overwrite: Boolean = false) { if (overwrite || !sourcesMap.containsKey(source.id)) { sourcesMap.put(source.id, source) } } private fun createInternalSources(): List = listOf( LocalSource(context), Batoto(), Mangahere(), Mangafox(), Kissmanga(), Readmanga(), Mintmanga(), Mangachan(), Readmangatoday(), Mangasee(), WieManga() ) private fun createEHSources(): List { val exSrcs = mutableListOf( EHentai(EH_SOURCE_ID, false, context), EHentaiMetadata(EH_METADATA_SOURCE_ID, false, context) ) if(prefs.enableExhentai().getOrDefault()) { exSrcs += EHentai(EXH_SOURCE_ID, true, context) exSrcs += EHentaiMetadata(EXH_METADATA_SOURCE_ID, true, context) } exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, "en") exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, "it") exSrcs += NHentai(context) return exSrcs } private fun createYamlSources(): List { val sources = mutableListOf() 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 { 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() 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 { // 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) 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 } }