diff --git a/app/build.gradle.kts b/app/build.gradle.kts index edfb28c..ebdb0bc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,8 +1,17 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +val compileKotlin: KotlinCompile by tasks + + plugins { id("org.jetbrains.kotlin.jvm") version "1.4.21" application } + +compileKotlin.kotlinOptions { + jvmTarget = "1.8" +} + repositories { // gradlePluginPortal() // google() @@ -26,13 +35,26 @@ dependencies { implementation( "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version") implementation ("com.squareup.okio:okio:2.9.0") + + // retrofit + val retrofit_version = "2.9.0" + implementation ("com.squareup.retrofit2:retrofit:$retrofit_version") + implementation ("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0") + implementation("com.squareup.retrofit2:converter-gson:$retrofit_version") + implementation ("com.squareup.retrofit2:adapter-rxjava:$retrofit_version") + + + implementation("io.reactivex:rxjava:1.3.8") // implementation 'io.reactivex:rxandroid:1.2.1' // implementation ("com.jakewharton.rxrelay:rxrelay:1.2.0") // implementation ("com.github.pwittchen:reactivenetwork:0.13.0") - implementation("org.jsoup:jsoup:1.13.1") + implementation( "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0") implementation("com.google.code.gson:gson:2.8.6") + implementation ("com.github.salomonbrys.kotson:kotson:2.5.0") + + implementation("org.jsoup:jsoup:1.13.1") implementation("com.github.salomonbrys.kotson:kotson:2.5.0") implementation("com.squareup.duktape:duktape-android:1.3.0") diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/annoations/Nsfw.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/annoations/Nsfw.kt new file mode 100644 index 0000000..5cff119 --- /dev/null +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/annoations/Nsfw.kt @@ -0,0 +1,5 @@ +package eu.kanade.tachiyomi.annoations + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +annotation class Nsfw diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt new file mode 100644 index 0000000..c3aff69 --- /dev/null +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt @@ -0,0 +1,82 @@ +package eu.kanade.tachiyomi.extension.api + +//import android.content.Context +import com.github.salomonbrys.kotson.get +import com.github.salomonbrys.kotson.int +//import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.extension.model.Extension +import eu.kanade.tachiyomi.extension.model.LoadResult +import eu.kanade.tachiyomi.extension.util.ExtensionLoader +//import kotlinx.coroutines.Dispatchers +//import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +//import uy.kohesive.injekt.injectLazy +import java.util.Date + +internal class ExtensionGithubApi { + +// private val preferences: PreferencesHelper by injectLazy() + + suspend fun findExtensions(): List { + val service: ExtensionGithubService = ExtensionGithubService.create() + + val response = service.getRepo() + return parseResponse(response) + } + +// suspend fun checkForUpdates(): List { +// val extensions = fin dExtensions() +// +//// preferences.lastExtCheck().set(Date().time) +// +// val installedExtensions = ExtensionLoader.loadExtensions(context) +// .filterIsInstance() +// .map { it.extension } +// +// val extensionsWithUpdate = mutableListOf() +// for (installedExt in installedExtensions) { +// val pkgName = installedExt.pkgName +// val availableExt = extensions.find { it.pkgName == pkgName } ?: continue +// +// val hasUpdate = availableExt.versionCode > installedExt.versionCode +// if (hasUpdate) { +// extensionsWithUpdate.add(installedExt) +// } +// } +// +// return extensionsWithUpdate +// } + + private fun parseResponse(json: JsonArray): List { + return json + .filter { element -> + val versionName = element.jsonObject["version"]!!.jsonPrimitive.content + val libVersion = versionName.substringBeforeLast('.').toDouble() + libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX + } + .map { element -> + val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ") + val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content + val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content + val versionName = element.jsonObject["version"]!!.jsonPrimitive.content + val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int + val lang = element.jsonObject["lang"]!!.jsonPrimitive.content + val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1 + val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}" + + Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon) + } + } + + fun getApkUrl(extension: Extension.Available): String { + return "$REPO_URL_PREFIX/apk/${extension.apkName}" + } + + companion object { + const val BASE_URL = "https://raw.githubusercontent.com/" + const val REPO_URL_PREFIX = "${BASE_URL}inorichi/tachiyomi-extensions/repo/" + } +} diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/extension/api/ExtensionGithubService.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/extension/api/ExtensionGithubService.kt new file mode 100644 index 0000000..2c6c88e --- /dev/null +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/extension/api/ExtensionGithubService.kt @@ -0,0 +1,44 @@ +package eu.kanade.tachiyomi.extension.api + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import eu.kanade.tachiyomi.network.NetworkHelper +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import okhttp3.MediaType.Companion.toMediaType +import retrofit2.Retrofit +import retrofit2.http.GET +//import uy.kohesive.injekt.injectLazy + +/** + * Used to get the extension repo listing from GitHub. + */ +interface ExtensionGithubService { + + companion object { + private val client by lazy { + val network: NetworkHelper = NetworkHelper() + network.client.newBuilder() + .addNetworkInterceptor { chain -> + val originalResponse = chain.proceed(chain.request()) + originalResponse.newBuilder() + .header("Content-Encoding", "gzip") + .header("Content-Type", "application/json") + .build() + } + .build() + } + + fun create(): ExtensionGithubService { + val adapter = Retrofit.Builder() + .baseUrl(ExtensionGithubApi.BASE_URL) + .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) + .client(client) + .build() + + return adapter.create(ExtensionGithubService::class.java) + } + } + + @GET("${ExtensionGithubApi.REPO_URL_PREFIX}index.json.gz") + suspend fun getRepo(): JsonArray +} diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/extension/model/Extension.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/extension/model/Extension.kt new file mode 100644 index 0000000..480c3b0 --- /dev/null +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/extension/model/Extension.kt @@ -0,0 +1,47 @@ +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? + abstract val isNsfw: Boolean + + data class Installed( + override val name: String, + override val pkgName: String, + override val versionName: String, + override val versionCode: Int, + override val lang: String, + override val isNsfw: Boolean, + val sources: List, + val hasUpdate: Boolean = false, + val isObsolete: Boolean = false, + val isUnofficial: 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, + override val isNsfw: Boolean, + 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, + override val isNsfw: Boolean = false + ) : Extension() +} diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/extension/model/InstallStep.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/extension/model/InstallStep.kt new file mode 100644 index 0000000..43bb519 --- /dev/null +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/extension/model/InstallStep.kt @@ -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 + } +} diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/extension/model/LoadResult.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/extension/model/LoadResult.kt new file mode 100644 index 0000000..0cf470f --- /dev/null +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/extension/model/LoadResult.kt @@ -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) + } +} diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt new file mode 100644 index 0000000..dd384d0 --- /dev/null +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -0,0 +1,233 @@ +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.annoations.Nsfw +//import eu.kanade.tachiyomi.data.preference.PreferenceValues +//import eu.kanade.tachiyomi.data.preference.PreferencesHelper +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.lang.Hash +//import kotlinx.coroutines.async +//import kotlinx.coroutines.runBlocking +//import timber.log.Timber +//import uy.kohesive.injekt.injectLazy + +/** + * Class that handles the loading of the extensions installed in the system. + */ +//@SuppressLint("PackageManagerGetSignatures") +internal object ExtensionLoader { + +// private val preferences: PreferencesHelper by injectLazy() +// private val allowNsfwSource by lazy { +// preferences.allowNsfwSource().get() +// } + + private const val EXTENSION_FEATURE = "tachiyomi.extension" + private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class" + private const val METADATA_NSFW = "tachiyomi.extension.nsfw" + const val LIB_VERSION_MIN = 1.2 + const val LIB_VERSION_MAX = 1.2 + +// private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES + + // inorichi's key + private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" + /** + * List of the trusted signatures. + */ +// var trustedSignatures = mutableSetOf() + preferences.trustedSignatures().get() + officialSignature + + /** + * Return a list of all the installed extensions initialized concurrently. + * + * @param context The application context. + */ +// fun loadExtensions(context: Context): List { +// 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 = try { +// context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS) +// } catch (error: PackageManager.NameNotFoundException) { +// // Unlikely, but the package may have been uninstalled at this point +// return LoadResult.Error(error) +// } +// 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 = try { +// pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA) +// } catch (error: PackageManager.NameNotFoundException) { +// // Unlikely, but the package may have been uninstalled at this point +// return LoadResult.Error(error) +// } +// +// val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ") +// val versionName = pkgInfo.versionName +// val versionCode = pkgInfo.versionCode +// +// if (versionName.isNullOrEmpty()) { +// val exception = Exception("Missing versionName for extension $extName") +// Timber.w(exception) +// return LoadResult.Error(exception) +// } +// +// // Validate lib version +// val libVersion = versionName.substringBeforeLast('.').toDouble() +// if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) { +// val exception = Exception( +// "Lib version is $libVersion, 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 isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1 +// if (allowNsfwSource == PreferenceValues.NsfwAllowance.BLOCKED && isNsfw) { +// return LoadResult.Error("NSFW extension $pkgName not allowed") +// } +// +// 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 { +// when (val obj = Class.forName(it, false, classLoader).newInstance()) { +// is Source -> listOf(obj) +// is SourceFactory -> { +// if (isSourceNsfw(obj)) { +// emptyList() +// } else { +// 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) +// } +// } +// .filter { !isSourceNsfw(it) } +// +// val langs = sources.filterIsInstance() +// .map { it.lang } +// .toSet() +// val lang = when (langs.size) { +// 0 -> "" +// 1 -> langs.first() +// else -> "all" +// } +// +// val extension = Extension.Installed( +// extName, +// pkgName, +// versionName, +// versionCode, +// lang, +// isNsfw, +// sources, +// isUnofficial = signatureHash != officialSignature +// ) +// 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.isNotEmpty()) { +// Hash.sha256(signatures.first().toByteArray()) +// } else { +// null +// } +// } + + /** + * Checks whether a Source or SourceFactory is annotated with @Nsfw. + */ +// private fun isSourceNsfw(clazz: Any): Boolean { +// if (allowNsfwSource == PreferenceValues.NsfwAllowance.ALLOWED) { +// return false +// } +// +// if (clazz !is Source && clazz !is SourceFactory) { +// return false +// } +// +// // Annotations are proxied, hence this janky way of checking for them +// return clazz.javaClass.annotations +// .flatMap { it.javaClass.interfaces.map { it.simpleName } } +// .firstOrNull { it == Nsfw::class.java.simpleName } != null +// } +}