mirror of
https://github.com/tachiyomiorg/tachiyomi-extensions-inspector.git
synced 2024-12-25 16:21:50 +01:00
prepare to install apk from any source
This commit is contained in:
parent
47fcf7eb97
commit
760d1116a1
@ -7,6 +7,7 @@ package ir.armor.tachidesk.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import com.googlecode.d2j.dex.Dex2jar
|
import com.googlecode.d2j.dex.Dex2jar
|
||||||
import com.googlecode.d2j.reader.MultiDexFileReader
|
import com.googlecode.d2j.reader.MultiDexFileReader
|
||||||
import com.googlecode.dex2jar.tools.BaksmaliBaseDexExceptionHandler
|
import com.googlecode.dex2jar.tools.BaksmaliBaseDexExceptionHandler
|
||||||
@ -26,6 +27,7 @@ import mu.KotlinLogging
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
import org.jetbrains.exposed.sql.insert
|
import org.jetbrains.exposed.sql.insert
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
@ -87,24 +89,51 @@ object Extension {
|
|||||||
return classToLoad.getDeclaredConstructor().newInstance()
|
return classToLoad.getDeclaredConstructor().newInstance()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class InstallableAPK(
|
||||||
|
val apkFilePath: String,
|
||||||
|
val pkgName: String
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun installExtension(pkgName: String): Int {
|
suspend fun installExtension(pkgName: String): Int {
|
||||||
logger.debug("Installing $pkgName")
|
logger.debug("Installing $pkgName")
|
||||||
val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName }
|
val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName }
|
||||||
val fileNameWithoutType = extensionRecord.apkName.substringBefore(".apk")
|
|
||||||
|
return installAPK {
|
||||||
|
val apkURL = ExtensionGithubApi.getApkUrl(extensionRecord)
|
||||||
|
val apkName = Uri.parse(apkURL).lastPathSegment!!
|
||||||
|
val apkSavePath = "${ApplicationDirs.extensionsRoot}/$apkName"
|
||||||
|
// download apk file
|
||||||
|
downloadAPKFile(apkURL, apkSavePath)
|
||||||
|
|
||||||
|
apkSavePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun installAPK(fetcher: suspend () -> String): Int {
|
||||||
|
val apkFilePath = fetcher()
|
||||||
|
val apkName = Uri.parse(apkFilePath).lastPathSegment!!
|
||||||
|
|
||||||
|
// TODO: handle the whole apk signature, and trusting bossiness
|
||||||
|
|
||||||
|
val extensionRecord: ResultRow = transaction {
|
||||||
|
ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()
|
||||||
|
} ?: {
|
||||||
|
ExtensionTable.insert {
|
||||||
|
it[this.apkName] = apkName
|
||||||
|
}
|
||||||
|
ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!!
|
||||||
|
}()
|
||||||
|
|
||||||
|
val extensionId = extensionRecord[ExtensionTable.id]
|
||||||
|
|
||||||
|
// check if we don't have the extension already installed
|
||||||
|
if (!extensionRecord[ExtensionTable.isInstalled]) {
|
||||||
|
val fileNameWithoutType = apkName.substringBefore(".apk")
|
||||||
|
|
||||||
val dirPathWithoutType = "${ApplicationDirs.extensionsRoot}/$fileNameWithoutType"
|
val dirPathWithoutType = "${ApplicationDirs.extensionsRoot}/$fileNameWithoutType"
|
||||||
|
|
||||||
// check if we don't have the dex file already downloaded
|
|
||||||
val jarPath = "${ApplicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
|
||||||
if (!File(jarPath).exists()) {
|
|
||||||
val apkToDownload = ExtensionGithubApi.getApkUrl(extensionRecord)
|
|
||||||
|
|
||||||
val apkFilePath = "$dirPathWithoutType.apk"
|
|
||||||
val jarFilePath = "$dirPathWithoutType.jar"
|
val jarFilePath = "$dirPathWithoutType.jar"
|
||||||
val dexFilePath = "$dirPathWithoutType.dex"
|
val dexFilePath = "$dirPathWithoutType.dex"
|
||||||
|
|
||||||
// download apk file
|
|
||||||
downloadAPKFile(apkToDownload, apkFilePath)
|
|
||||||
|
|
||||||
val className: String = APKExtractor.extractDexAndReadClassname(apkFilePath, dexFilePath)
|
val className: String = APKExtractor.extractDexAndReadClassname(apkFilePath, dexFilePath)
|
||||||
logger.debug("Main class for extension is $className")
|
logger.debug("Main class for extension is $className")
|
||||||
|
|
||||||
@ -117,18 +146,14 @@ object Extension {
|
|||||||
// update sources of the extension
|
// update sources of the extension
|
||||||
val instance = loadExtensionInstance(jarFilePath, className)
|
val instance = loadExtensionInstance(jarFilePath, className)
|
||||||
|
|
||||||
val extensionId = transaction {
|
|
||||||
ExtensionTable.select { ExtensionTable.pkgName eq extensionRecord.pkgName }.firstOrNull()!![ExtensionTable.id]
|
|
||||||
}
|
|
||||||
|
|
||||||
when (instance) {
|
when (instance) {
|
||||||
is HttpSource -> { // single source
|
is HttpSource -> { // single source
|
||||||
transaction {
|
transaction {
|
||||||
if (SourceTable.select { SourceTable.id eq instance.id }.count() == 0L) {
|
if (SourceTable.select { SourceTable.id eq instance.id }.count() == 0L) {
|
||||||
SourceTable.insert {
|
SourceTable.insert {
|
||||||
it[this.id] = instance.id
|
it[id] = instance.id
|
||||||
it[name] = instance.name
|
it[name] = instance.name
|
||||||
it[this.lang] = instance.lang
|
it[lang] = instance.lang
|
||||||
it[extension] = extensionId
|
it[extension] = extensionId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -141,9 +166,9 @@ object Extension {
|
|||||||
val httpSource = source as HttpSource
|
val httpSource = source as HttpSource
|
||||||
if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) {
|
if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) {
|
||||||
SourceTable.insert {
|
SourceTable.insert {
|
||||||
it[this.id] = httpSource.id
|
it[id] = httpSource.id
|
||||||
it[name] = httpSource.name
|
it[name] = httpSource.name
|
||||||
it[this.lang] = httpSource.lang
|
it[lang] = httpSource.lang
|
||||||
it[extension] = extensionId
|
it[extension] = extensionId
|
||||||
it[partOfFactorySource] = true
|
it[partOfFactorySource] = true
|
||||||
}
|
}
|
||||||
@ -159,24 +184,24 @@ object Extension {
|
|||||||
|
|
||||||
// update extension info
|
// update extension info
|
||||||
transaction {
|
transaction {
|
||||||
ExtensionTable.update({ ExtensionTable.pkgName eq extensionRecord.pkgName }) {
|
ExtensionTable.update({ ExtensionTable.apkName eq apkName }) {
|
||||||
it[isInstalled] = true
|
it[isInstalled] = true
|
||||||
it[classFQName] = className
|
it[classFQName] = className
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 201 // we downloaded successfully
|
return 201 // we installed successfully
|
||||||
} else {
|
} else {
|
||||||
return 302
|
return 302 // extension was already installed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val network: NetworkHelper by injectLazy()
|
private val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
private suspend fun downloadAPKFile(url: String, apkPath: String) {
|
private suspend fun downloadAPKFile(url: String, savePath: String) {
|
||||||
val request = Request.Builder().url(url).build()
|
val request = Request.Builder().url(url).build()
|
||||||
val response = network.client.newCall(request).await()
|
val response = network.client.newCall(request).await()
|
||||||
|
|
||||||
val downloadedFile = File(apkPath)
|
val downloadedFile = File(savePath)
|
||||||
downloadedFile.sink().buffer().use { sink ->
|
downloadedFile.sink().buffer().use { sink ->
|
||||||
response.body!!.source().use { source ->
|
response.body!!.source().use { source ->
|
||||||
sink.writeAll(source)
|
sink.writeAll(source)
|
||||||
|
@ -48,14 +48,14 @@ object ExtensionsList {
|
|||||||
fun extensionTableAsDataClass() = transaction {
|
fun extensionTableAsDataClass() = transaction {
|
||||||
ExtensionTable.selectAll().map {
|
ExtensionTable.selectAll().map {
|
||||||
ExtensionDataClass(
|
ExtensionDataClass(
|
||||||
|
it[ExtensionTable.apkName],
|
||||||
|
getExtensionIconUrl(it[ExtensionTable.apkName]),
|
||||||
it[ExtensionTable.name],
|
it[ExtensionTable.name],
|
||||||
it[ExtensionTable.pkgName],
|
it[ExtensionTable.pkgName],
|
||||||
it[ExtensionTable.versionName],
|
it[ExtensionTable.versionName],
|
||||||
it[ExtensionTable.versionCode],
|
it[ExtensionTable.versionCode],
|
||||||
it[ExtensionTable.lang],
|
it[ExtensionTable.lang],
|
||||||
it[ExtensionTable.isNsfw],
|
it[ExtensionTable.isNsfw],
|
||||||
it[ExtensionTable.apkName],
|
|
||||||
getExtensionIconUrl(it[ExtensionTable.apkName]),
|
|
||||||
it[ExtensionTable.isInstalled],
|
it[ExtensionTable.isInstalled],
|
||||||
it[ExtensionTable.hasUpdate],
|
it[ExtensionTable.hasUpdate],
|
||||||
it[ExtensionTable.isObsolete],
|
it[ExtensionTable.isObsolete],
|
||||||
|
@ -10,17 +10,21 @@ package ir.armor.tachidesk.model.database
|
|||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
|
||||||
object ExtensionTable : IntIdTable() {
|
object ExtensionTable : IntIdTable() {
|
||||||
val name = varchar("name", 128)
|
|
||||||
val pkgName = varchar("pkg_name", 128)
|
|
||||||
val versionName = varchar("version_name", 16)
|
|
||||||
val versionCode = integer("version_code")
|
|
||||||
val lang = varchar("lang", 10)
|
|
||||||
val isNsfw = bool("is_nsfw")
|
|
||||||
val apkName = varchar("apk_name", 1024)
|
val apkName = varchar("apk_name", 1024)
|
||||||
|
|
||||||
|
// default is the local source icon from tachiyomi
|
||||||
val iconUrl = varchar("icon_url", 2048)
|
val iconUrl = varchar("icon_url", 2048)
|
||||||
|
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
|
||||||
|
|
||||||
|
val name = varchar("name", 128).nullable().default(null)
|
||||||
|
val pkgName = varchar("pkg_name", 128).nullable().default(null)
|
||||||
|
val versionName = varchar("version_name", 16).nullable().default(null)
|
||||||
|
val versionCode = integer("version_code").default(0)
|
||||||
|
val lang = varchar("lang", 10).nullable().default(null)
|
||||||
|
val isNsfw = bool("is_nsfw").nullable().default(null)
|
||||||
|
|
||||||
val isInstalled = bool("is_installed").default(false)
|
val isInstalled = bool("is_installed").default(false)
|
||||||
val hasUpdate = bool("has_update").default(false)
|
val hasUpdate = bool("has_update").default(false)
|
||||||
val isObsolete = bool("is_obsolete").default(false)
|
val isObsolete = bool("is_obsolete").default(false)
|
||||||
val classFQName = varchar("class_name", 256).default("") // fully qualified name
|
val classFQName = varchar("class_name", 1024).default("") // fully qualified name
|
||||||
}
|
}
|
||||||
|
@ -8,14 +8,14 @@ package ir.armor.tachidesk.model.dataclass
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
data class ExtensionDataClass(
|
data class ExtensionDataClass(
|
||||||
val name: String,
|
|
||||||
val pkgName: String,
|
|
||||||
val versionName: String,
|
|
||||||
val versionCode: Int,
|
|
||||||
val lang: String,
|
|
||||||
val isNsfw: Boolean,
|
|
||||||
val apkName: String,
|
val apkName: String,
|
||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
|
val name: String?,
|
||||||
|
val pkgName: String?,
|
||||||
|
val versionName: String?,
|
||||||
|
val versionCode: Int?,
|
||||||
|
val lang: String?,
|
||||||
|
val isNsfw: Boolean?,
|
||||||
val installed: Boolean,
|
val installed: Boolean,
|
||||||
val hasUpdate: Boolean,
|
val hasUpdate: Boolean,
|
||||||
val obsolete: Boolean,
|
val obsolete: Boolean,
|
||||||
|
Loading…
Reference in New Issue
Block a user