Fix the crash caused by Kotlinx Serialization on KitKat devices. (#1698)
Fix the crash in MangaPlus on KitKat devices
This commit is contained in:
parent
89a0b734f5
commit
23a9d3d7c4
|
@ -6,12 +6,14 @@ ext {
|
||||||
appName = 'Tachiyomi: MANGA Plus by SHUEISHA'
|
appName = 'Tachiyomi: MANGA Plus by SHUEISHA'
|
||||||
pkgNameSuffix = 'all.mangaplus'
|
pkgNameSuffix = 'all.mangaplus'
|
||||||
extClass = '.MangaPlusFactory'
|
extClass = '.MangaPlusFactory'
|
||||||
extVersionCode = 2
|
extVersionCode = 3
|
||||||
libVersion = '1.2'
|
libVersion = '1.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.13.0'
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.13.0'
|
||||||
|
compileOnly 'com.google.code.gson:gson:2.8.2'
|
||||||
|
compileOnly project(':duktape-stub')
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.mangaplus
|
package eu.kanade.tachiyomi.extension.all.mangaplus
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.squareup.duktape.Duktape
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
|
@ -30,6 +33,12 @@ abstract class MangaPlus(override val lang: String,
|
||||||
.addInterceptor { imageIntercept(it) }
|
.addInterceptor { imageIntercept(it) }
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
private val protobufJs: String by lazy {
|
||||||
|
client.newCall(GET(PROTOBUFJS_CDN, headers)).execute().body()!!.string()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val gson: Gson by lazy { Gson() }
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
return GET("$baseUrl/title_list/ranking", headers)
|
return GET("$baseUrl/title_list/ranking", headers)
|
||||||
}
|
}
|
||||||
|
@ -240,8 +249,8 @@ abstract class MangaPlus(override val lang: String,
|
||||||
private fun decodeImage(encryptionKey: String, image: ByteArray): ByteArray {
|
private fun decodeImage(encryptionKey: String, image: ByteArray): ByteArray {
|
||||||
val keyStream = HEX_GROUP
|
val keyStream = HEX_GROUP
|
||||||
.findAll(encryptionKey)
|
.findAll(encryptionKey)
|
||||||
.map { it.groupValues[1].toInt(16) }
|
|
||||||
.toList()
|
.toList()
|
||||||
|
.map { it.groupValues[1].toInt(16) }
|
||||||
|
|
||||||
val content = image
|
val content = image
|
||||||
.map { it.toInt() }
|
.map { it.toInt() }
|
||||||
|
@ -256,7 +265,33 @@ abstract class MangaPlus(override val lang: String,
|
||||||
return ByteArray(content.size) { pos -> content[pos].toByte() }
|
return ByteArray(content.size) { pos -> content[pos].toByte() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Response.asProto(): MangaPlusResponse = ProtoBuf.load(MangaPlusSerializer, body()!!.bytes())
|
private fun Response.asProto(): MangaPlusResponse {
|
||||||
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT)
|
||||||
|
return ProtoBuf.load(MangaPlusSerializer, body()!!.bytes())
|
||||||
|
|
||||||
|
// Apparently, the version used of Kotlinx Serialization lib causes a crash
|
||||||
|
// on KitKat devices (see #1678). So, if the device is running KitKat or lower,
|
||||||
|
// we use the old method of parsing their API -- using ProtobufJS + Duktape + Gson.
|
||||||
|
|
||||||
|
val bytes = body()!!.bytes()
|
||||||
|
val messageBytes = "var BYTE_ARR = new Uint8Array([${bytes.joinToString()}]);"
|
||||||
|
|
||||||
|
val res = Duktape.create().use {
|
||||||
|
it.set("helper", DuktapeHelper::class.java, object : DuktapeHelper {
|
||||||
|
override fun getProtobuf(): String = protobufJs
|
||||||
|
})
|
||||||
|
it.evaluate(messageBytes + DECODE_SCRIPT) as String
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Json.parse method of the Kotlinx Serialization causes the app to crash too,
|
||||||
|
// so unfortunately we have to use Gson to deserialize.
|
||||||
|
return gson.fromJson(res, MangaPlusResponse::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface DuktapeHelper {
|
||||||
|
@Suppress("unused")
|
||||||
|
fun getProtobuf(): String
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val WEB_URL = "https://mangaplus.shueisha.co.jp"
|
private const val WEB_URL = "https://mangaplus.shueisha.co.jp"
|
||||||
|
@ -264,5 +299,7 @@ abstract class MangaPlus(override val lang: String,
|
||||||
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36"
|
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36"
|
||||||
|
|
||||||
private val HEX_GROUP = "(.{1,2})".toRegex()
|
private val HEX_GROUP = "(.{1,2})".toRegex()
|
||||||
|
|
||||||
|
private const val PROTOBUFJS_CDN = "https://cdn.rawgit.com/dcodeIO/protobuf.js/6.8.8/dist/light/protobuf.min.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.mangaplus
|
package eu.kanade.tachiyomi.extension.all.mangaplus
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
import kotlinx.serialization.Optional
|
import kotlinx.serialization.Optional
|
||||||
import kotlinx.serialization.SerialId
|
import kotlinx.serialization.SerialId
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
@ -80,8 +81,13 @@ data class Title(
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class Language(val id: Int) {
|
enum class Language(val id: Int) {
|
||||||
@SerialId(0) ENGLISH(0),
|
@SerialId(0)
|
||||||
@SerialId(1) SPANISH(1)
|
@SerializedName("0")
|
||||||
|
ENGLISH(0),
|
||||||
|
|
||||||
|
@SerialId(1)
|
||||||
|
@SerializedName("1")
|
||||||
|
SPANISH(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -115,3 +121,160 @@ data class MangaPage(
|
||||||
@SerialId(3) val height: Int,
|
@SerialId(3) val height: Int,
|
||||||
@Optional @SerialId(5) val encryptionKey: String? = null
|
@Optional @SerialId(5) val encryptionKey: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Used for the deserialization on KitKat devices.
|
||||||
|
const val DECODE_SCRIPT: String = """
|
||||||
|
Duktape.modSearch = function(id) {
|
||||||
|
if (id == "protobufjs")
|
||||||
|
return helper.getProtobuf();
|
||||||
|
throw new Error("Cannot find module: " + id);
|
||||||
|
}
|
||||||
|
|
||||||
|
var protobuf = require("protobufjs");
|
||||||
|
|
||||||
|
var Root = protobuf.Root;
|
||||||
|
var Type = protobuf.Type;
|
||||||
|
var Field = protobuf.Field;
|
||||||
|
var Enum = protobuf.Enum;
|
||||||
|
var OneOf = protobuf.OneOf;
|
||||||
|
|
||||||
|
var Response = new Type("Response")
|
||||||
|
.add(
|
||||||
|
new OneOf("data")
|
||||||
|
.add(new Field("success", 1, "SuccessResult"))
|
||||||
|
.add(new Field("error", 2, "ErrorResult"))
|
||||||
|
);
|
||||||
|
|
||||||
|
var ErrorResult = new Type("ErrorResult")
|
||||||
|
.add(new Field("action", 1, "Action"))
|
||||||
|
.add(new Field("englishPopup", 2, "Popup"))
|
||||||
|
.add(new Field("spanishPopup", 3, "Popup"));
|
||||||
|
|
||||||
|
var Action = new Enum("Action")
|
||||||
|
.add("DEFAULT", 0)
|
||||||
|
.add("UNAUTHORIZED", 1)
|
||||||
|
.add("MAINTAINENCE", 2)
|
||||||
|
.add("GEOIP_BLOCKING", 3);
|
||||||
|
|
||||||
|
var Popup = new Type("Popup")
|
||||||
|
.add(new Field("subject", 1, "string"))
|
||||||
|
.add(new Field("body", 2, "string"));
|
||||||
|
|
||||||
|
var SuccessResult = new Type("SuccessResult")
|
||||||
|
.add(new Field("isFeaturedUpdated", 1, "bool"))
|
||||||
|
.add(
|
||||||
|
new OneOf("data")
|
||||||
|
.add(new Field("allTitlesView", 5, "AllTitlesView"))
|
||||||
|
.add(new Field("titleRankingView", 6, "TitleRankingView"))
|
||||||
|
.add(new Field("titleDetailView", 8, "TitleDetailView"))
|
||||||
|
.add(new Field("mangaViewer", 10, "MangaViewer"))
|
||||||
|
.add(new Field("webHomeView", 11, "WebHomeView"))
|
||||||
|
);
|
||||||
|
|
||||||
|
var TitleRankingView = new Type("TitleRankingView")
|
||||||
|
.add(new Field("titles", 1, "Title", "repeated"));
|
||||||
|
|
||||||
|
var AllTitlesView = new Type("AllTitlesView")
|
||||||
|
.add(new Field("titles", 1, "Title", "repeated"));
|
||||||
|
|
||||||
|
var WebHomeView = new Type("WebHomeView")
|
||||||
|
.add(new Field("groups", 2, "UpdatedTitleGroup", "repeated"));
|
||||||
|
|
||||||
|
var TitleDetailView = new Type("TitleDetailView")
|
||||||
|
.add(new Field("title", 1, "Title"))
|
||||||
|
.add(new Field("titleImageUrl", 2, "string"))
|
||||||
|
.add(new Field("overview", 3, "string"))
|
||||||
|
.add(new Field("backgroundImageUrl", 4, "string"))
|
||||||
|
.add(new Field("nextTimeStamp", 5, "uint32"))
|
||||||
|
.add(new Field("updateTiming", 6, "UpdateTiming"))
|
||||||
|
.add(new Field("viewingPeriodDescription", 7, "string"))
|
||||||
|
.add(new Field("firstChapterList", 9, "Chapter", "repeated"))
|
||||||
|
.add(new Field("lastChapterList", 10, "Chapter", "repeated"))
|
||||||
|
.add(new Field("isSimulReleased", 14, "bool"))
|
||||||
|
.add(new Field("chaptersDescending", 17, "bool"));
|
||||||
|
|
||||||
|
var UpdateTiming = new Enum("UpdateTiming")
|
||||||
|
.add("NOT_REGULARLY", 0)
|
||||||
|
.add("MONDAY", 1)
|
||||||
|
.add("TUESDAY", 2)
|
||||||
|
.add("WEDNESDAY", 3)
|
||||||
|
.add("THURSDAY", 4)
|
||||||
|
.add("FRIDAY", 5)
|
||||||
|
.add("SATURDAY", 6)
|
||||||
|
.add("SUNDAY", 7)
|
||||||
|
.add("DAY", 8);
|
||||||
|
|
||||||
|
var MangaViewer = new Type("MangaViewer")
|
||||||
|
.add(new Field("pages", 1, "Page", "repeated"));
|
||||||
|
|
||||||
|
var Title = new Type("Title")
|
||||||
|
.add(new Field("titleId", 1, "uint32"))
|
||||||
|
.add(new Field("name", 2, "string"))
|
||||||
|
.add(new Field("author", 3, "string"))
|
||||||
|
.add(new Field("portraitImageUrl", 4, "string"))
|
||||||
|
.add(new Field("landscapeImageUrl", 5, "string"))
|
||||||
|
.add(new Field("viewCount", 6, "uint32"))
|
||||||
|
.add(new Field("language", 7, "Language", {"default": 0}));
|
||||||
|
|
||||||
|
var Language = new Enum("Language")
|
||||||
|
.add("ENGLISH", 0)
|
||||||
|
.add("SPANISH", 1);
|
||||||
|
|
||||||
|
var UpdatedTitleGroup = new Type("UpdatedTitleGroup")
|
||||||
|
.add(new Field("groupName", 1, "string"))
|
||||||
|
.add(new Field("titles", 2, "UpdatedTitle", "repeated"));
|
||||||
|
|
||||||
|
var UpdatedTitle = new Type("UpdatedTitle")
|
||||||
|
.add(new Field("title", 1, "Title"))
|
||||||
|
.add(new Field("chapterId", 2, "uint32"))
|
||||||
|
.add(new Field("chapterName", 3, "string"))
|
||||||
|
.add(new Field("chapterSubtitle", 4, "string"));
|
||||||
|
|
||||||
|
var Chapter = new Type("Chapter")
|
||||||
|
.add(new Field("titleId", 1, "uint32"))
|
||||||
|
.add(new Field("chapterId", 2, "uint32"))
|
||||||
|
.add(new Field("name", 3, "string"))
|
||||||
|
.add(new Field("subTitle", 4, "string", "optional"))
|
||||||
|
.add(new Field("startTimeStamp", 6, "uint32"))
|
||||||
|
.add(new Field("endTimeStamp", 7, "uint32"));
|
||||||
|
|
||||||
|
var Page = new Type("Page")
|
||||||
|
.add(new Field("page", 1, "MangaPage"));
|
||||||
|
|
||||||
|
var MangaPage = new Type("MangaPage")
|
||||||
|
.add(new Field("imageUrl", 1, "string"))
|
||||||
|
.add(new Field("width", 2, "uint32"))
|
||||||
|
.add(new Field("height", 3, "uint32"))
|
||||||
|
.add(new Field("encryptionKey", 5, "string", "optional"));
|
||||||
|
|
||||||
|
var root = new Root()
|
||||||
|
.define("mangaplus")
|
||||||
|
.add(Response)
|
||||||
|
.add(ErrorResult)
|
||||||
|
.add(Action)
|
||||||
|
.add(Popup)
|
||||||
|
.add(SuccessResult)
|
||||||
|
.add(TitleRankingView)
|
||||||
|
.add(AllTitlesView)
|
||||||
|
.add(WebHomeView)
|
||||||
|
.add(TitleDetailView)
|
||||||
|
.add(UpdateTiming)
|
||||||
|
.add(MangaViewer)
|
||||||
|
.add(Title)
|
||||||
|
.add(Language)
|
||||||
|
.add(UpdatedTitleGroup)
|
||||||
|
.add(UpdatedTitle)
|
||||||
|
.add(Chapter)
|
||||||
|
.add(Page)
|
||||||
|
.add(MangaPage);
|
||||||
|
|
||||||
|
function decode(arr) {
|
||||||
|
var Response = root.lookupType("Response");
|
||||||
|
var message = Response.decode(arr);
|
||||||
|
return Response.toObject(message, {defaults: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
return JSON.stringify(decode(BYTE_ARR)).replace(/\,\{\}/g, "");
|
||||||
|
})();
|
||||||
|
"""
|
||||||
|
|
Loading…
Reference in New Issue