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 * 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)

View File

@ -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],

View File

@ -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
} }

View File

@ -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,