prepare to install apk from any source

This commit is contained in:
Aria Moradi 2021-04-03 13:20:14 +04:30
parent 47fcf7eb97
commit 760d1116a1
4 changed files with 67 additions and 38 deletions

View File

@ -7,6 +7,7 @@ package ir.armor.tachidesk.impl
* 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/. */
import android.net.Uri
import com.googlecode.d2j.dex.Dex2jar
import com.googlecode.d2j.reader.MultiDexFileReader
import com.googlecode.dex2jar.tools.BaksmaliBaseDexExceptionHandler
@ -26,6 +27,7 @@ import mu.KotlinLogging
import okhttp3.Request
import okio.buffer
import okio.sink
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
@ -87,24 +89,51 @@ object Extension {
return classToLoad.getDeclaredConstructor().newInstance()
}
data class InstallableAPK(
val apkFilePath: String,
val pkgName: String
)
suspend fun installExtension(pkgName: String): Int {
logger.debug("Installing $pkgName")
val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName }
val fileNameWithoutType = extensionRecord.apkName.substringBefore(".apk")
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)
return installAPK {
val apkURL = ExtensionGithubApi.getApkUrl(extensionRecord)
val apkName = Uri.parse(apkURL).lastPathSegment!!
val apkSavePath = "${ApplicationDirs.extensionsRoot}/$apkName"
// download apk file
downloadAPKFile(apkURL, apkSavePath)
val apkFilePath = "$dirPathWithoutType.apk"
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 jarFilePath = "$dirPathWithoutType.jar"
val dexFilePath = "$dirPathWithoutType.dex"
// download apk file
downloadAPKFile(apkToDownload, apkFilePath)
val className: String = APKExtractor.extractDexAndReadClassname(apkFilePath, dexFilePath)
logger.debug("Main class for extension is $className")
@ -117,18 +146,14 @@ object Extension {
// update sources of the extension
val instance = loadExtensionInstance(jarFilePath, className)
val extensionId = transaction {
ExtensionTable.select { ExtensionTable.pkgName eq extensionRecord.pkgName }.firstOrNull()!![ExtensionTable.id]
}
when (instance) {
is HttpSource -> { // single source
transaction {
if (SourceTable.select { SourceTable.id eq instance.id }.count() == 0L) {
SourceTable.insert {
it[this.id] = instance.id
it[id] = instance.id
it[name] = instance.name
it[this.lang] = instance.lang
it[lang] = instance.lang
it[extension] = extensionId
}
}
@ -141,9 +166,9 @@ object Extension {
val httpSource = source as HttpSource
if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) {
SourceTable.insert {
it[this.id] = httpSource.id
it[id] = httpSource.id
it[name] = httpSource.name
it[this.lang] = httpSource.lang
it[lang] = httpSource.lang
it[extension] = extensionId
it[partOfFactorySource] = true
}
@ -159,24 +184,24 @@ object Extension {
// update extension info
transaction {
ExtensionTable.update({ ExtensionTable.pkgName eq extensionRecord.pkgName }) {
ExtensionTable.update({ ExtensionTable.apkName eq apkName }) {
it[isInstalled] = true
it[classFQName] = className
}
}
return 201 // we downloaded successfully
return 201 // we installed successfully
} else {
return 302
return 302 // extension was already installed
}
}
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 response = network.client.newCall(request).await()
val downloadedFile = File(apkPath)
val downloadedFile = File(savePath)
downloadedFile.sink().buffer().use { sink ->
response.body!!.source().use { source ->
sink.writeAll(source)

View File

@ -48,14 +48,14 @@ object ExtensionsList {
fun extensionTableAsDataClass() = transaction {
ExtensionTable.selectAll().map {
ExtensionDataClass(
it[ExtensionTable.apkName],
getExtensionIconUrl(it[ExtensionTable.apkName]),
it[ExtensionTable.name],
it[ExtensionTable.pkgName],
it[ExtensionTable.versionName],
it[ExtensionTable.versionCode],
it[ExtensionTable.lang],
it[ExtensionTable.isNsfw],
it[ExtensionTable.apkName],
getExtensionIconUrl(it[ExtensionTable.apkName]),
it[ExtensionTable.isInstalled],
it[ExtensionTable.hasUpdate],
it[ExtensionTable.isObsolete],

View File

@ -10,17 +10,21 @@ package ir.armor.tachidesk.model.database
import org.jetbrains.exposed.dao.id.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)
// default is the local source icon from tachiyomi
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 hasUpdate = bool("has_update").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
}

View File

@ -8,14 +8,14 @@ package ir.armor.tachidesk.model.dataclass
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
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 iconUrl: String,
val name: String?,
val pkgName: String?,
val versionName: String?,
val versionCode: Int?,
val lang: String?,
val isNsfw: Boolean?,
val installed: Boolean,
val hasUpdate: Boolean,
val obsolete: Boolean,