Fix the crash caused by Kotlinx Serialization on KitKat devices. (#1698)

Fix the crash in MangaPlus on KitKat devices
This commit is contained in:
Alessandro Jean 2019-10-23 18:22:54 -03:00 committed by arkon
parent 89a0b734f5
commit 23a9d3d7c4
3 changed files with 207 additions and 5 deletions

View File

@ -6,12 +6,14 @@ ext {
appName = 'Tachiyomi: MANGA Plus by SHUEISHA'
pkgNameSuffix = 'all.mangaplus'
extClass = '.MangaPlusFactory'
extVersionCode = 2
extVersionCode = 3
libVersion = '1.2'
}
dependencies {
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"

View File

@ -1,5 +1,8 @@
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.asObservableSuccess
import eu.kanade.tachiyomi.source.model.*
@ -30,6 +33,12 @@ abstract class MangaPlus(override val lang: String,
.addInterceptor { imageIntercept(it) }
.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 {
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 {
val keyStream = HEX_GROUP
.findAll(encryptionKey)
.map { it.groupValues[1].toInt(16) }
.toList()
.map { it.groupValues[1].toInt(16) }
val content = image
.map { it.toInt() }
@ -256,7 +265,33 @@ abstract class MangaPlus(override val lang: String,
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 {
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 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"
}
}

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.extension.all.mangaplus
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Optional
import kotlinx.serialization.SerialId
import kotlinx.serialization.Serializable
@ -80,8 +81,13 @@ data class Title(
)
enum class Language(val id: Int) {
@SerialId(0) ENGLISH(0),
@SerialId(1) SPANISH(1)
@SerialId(0)
@SerializedName("0")
ENGLISH(0),
@SerialId(1)
@SerializedName("1")
SPANISH(1)
}
@Serializable
@ -115,3 +121,160 @@ data class MangaPage(
@SerialId(3) val height: Int,
@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, "");
})();
"""