From 3eaa38247ba7c895816fd1544e062b9992425167 Mon Sep 17 00:00:00 2001 From: tom5079 Date: Tue, 4 Jan 2022 19:10:58 +0900 Subject: [PATCH] 5.2.2 Fixed app crashing --- README.md | 2 +- app/build.gradle | 2 +- app/release/output-metadata.json | 2 +- app/src/main/java/xyz/quaver/pupil/Pupil.kt | 105 +++++++++++++++--- .../java/xyz/quaver/pupil/hitomi/common.kt | 74 +++++++----- .../xyz/quaver/pupil/hitomi/galleryblock.kt | 2 +- .../java/xyz/quaver/pupil/hitomi/search.kt | 10 +- 7 files changed, 143 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index ec18663d..4f2f5d64 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ *Pupil, Hitomi.la viewer for Android* ![](https://img.shields.io/github/downloads/tom5079/Pupil/total) -[![](https://img.shields.io/github/downloads/tom5079/Pupil/5.2.1/Pupil-v5.2.1.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.2.1/Pupil-v5.2.1.apk) +[![](https://img.shields.io/github/downloads/tom5079/Pupil/5.2.2/Pupil-v5.2.2.apk?color=%234fc3f7&label=DOWNLOAD%20APP&style=for-the-badge)](https://github.com/tom5079/Pupil/releases/download/5.2.2/Pupil-v5.2.2.apk) [![](https://discordapp.com/api/guilds/610452916612104194/embed.png?style=banner2)](https://discord.gg/Stj4b5v) # Features diff --git a/app/build.gradle b/app/build.gradle index b57e58fc..1a4fb2cf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,7 +38,7 @@ android { minSdkVersion 16 targetSdkVersion 30 versionCode 69 - versionName "5.2.1" + versionName "5.2.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true } diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 78766588..c6b04b32 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,7 +11,7 @@ "type": "SINGLE", "filters": [], "versionCode": 69, - "versionName": "5.2.1", + "versionName": "5.2.2", "outputFile": "app-release.apk" } ] diff --git a/app/src/main/java/xyz/quaver/pupil/Pupil.kt b/app/src/main/java/xyz/quaver/pupil/Pupil.kt index db8b3203..f0cccdb1 100644 --- a/app/src/main/java/xyz/quaver/pupil/Pupil.kt +++ b/app/src/main/java/xyz/quaver/pupil/Pupil.kt @@ -25,6 +25,7 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context import android.content.Intent +import android.content.pm.ApplicationInfo import android.net.Uri import android.os.Build import android.util.Log @@ -40,14 +41,13 @@ import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.security.ProviderInstaller import com.google.android.material.snackbar.Snackbar import com.google.firebase.crashlytics.FirebaseCrashlytics -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.* import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.launch import okhttp3.* import xyz.quaver.io.FileX +import xyz.quaver.pupil.hitomi.evaluations import xyz.quaver.pupil.types.Tag import xyz.quaver.pupil.util.* import java.io.File @@ -81,13 +81,77 @@ val client: OkHttpClient @SuppressLint("StaticFieldLeak") lateinit var webView: WebView -val _webViewFlow = MutableSharedFlow>( +val _webViewFlow = MutableSharedFlow>( extraBufferCapacity = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST ) val webViewFlow = _webViewFlow.asSharedFlow() var webViewReady = false + private set +var webViewFailed = false + private set +private var reloadJob: Job? = null +fun reloadWebView() { + if (reloadJob?.isActive == true) return + + reloadJob = CoroutineScope(Dispatchers.IO).launch { + if (evaluations.isEmpty()) { + webViewReady = false + webViewFailed = false + + while (evaluations.isNotEmpty()) yield() + + runCatching { + URL( + if (isDebugBuild) + "https://tom5079.github.io/Pupil/hitomi-dev.html" + else + "https://tom5079.github.io/Pupil/hitomi.html" + ).readText() + }.onFailure { + webViewFailed = true + }.getOrNull()?.let { html -> + launch(Dispatchers.Main) { + webView.loadDataWithBaseURL( + "https://hitomi.la/", + html, + "text/html", + null, + null + ) + } + } + } + } +} + +private var htmlVersion: String = "" +fun reloadWhenFailedOrUpdate() = CoroutineScope(Dispatchers.Default).launch { + while (true) { + if ( + webViewFailed || + runCatching { + URL( + if (isDebugBuild) + "https://tom5079.github.io/Pupil/hitomi-dev.html.ver" + else + "https://tom5079.github.io/Pupil/hitomi.html.ver" + ).readText() + }.getOrNull().let { version -> + (!version.isNullOrEmpty() && version != htmlVersion).also { + if (it) htmlVersion = version!! + } + } + ) { + reloadWebView() + } + + delay(if (webViewReady && !webViewFailed) 10000 else 1000) + } +} + +var isDebugBuild: Boolean = false private lateinit var userAgent: String class Pupil : Application() { @@ -100,6 +164,9 @@ class Pupil : Application() { @SuppressLint("SetJavaScriptEnabled") override fun onCreate() { instance = this + isDebugBuild = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0 + + WebView.setWebContentsDebuggingEnabled(true) webView = WebView(this).apply { with (settings) { @@ -113,6 +180,14 @@ class Pupil : Application() { override fun onPageFinished(view: WebView?, url: String?) { webViewReady = true } + + override fun onReceivedError( + view: WebView?, + request: WebResourceRequest?, + error: WebResourceError? + ) { + webViewFailed = true + } } addJavascriptInterface(object { @@ -120,23 +195,19 @@ class Pupil : Application() { fun onResult(uid: String, result: String) { _webViewFlow.tryEmit(uid to result) } - }, "Callback") - - CoroutineScope(Dispatchers.IO).launch { - val html = URL("https://tom5079.github.io/Pupil/hitomi.html").readText() - - launch(Dispatchers.Main) { - loadDataWithBaseURL( - "https://hitomi.la/", - html, - "text/html", - null, - null + @JavascriptInterface + fun onError(uid: String, message: String) { + _webViewFlow.tryEmit(uid to null) + Toast.makeText(this@Pupil, message, Toast.LENGTH_LONG).show() + FirebaseCrashlytics.getInstance().recordException( + Exception(message) ) } - } + }, "Callback") } + reloadWhenFailedOrUpdate() + AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) preferences = PreferenceManager.getDefaultSharedPreferences(this) diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt index 168d34d8..f573df8b 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/common.kt @@ -16,37 +16,46 @@ package xyz.quaver.pupil.hitomi -import android.annotation.SuppressLint -import android.util.Log import android.webkit.WebView -import android.webkit.WebViewClient -import kotlinx.coroutines.* +import android.widget.Toast +import com.google.common.collect.ConcurrentHashMultiset +import com.google.firebase.crashlytics.FirebaseCrashlytics +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.transformWhile +import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import xyz.quaver.json -import xyz.quaver.pupil.Pupil -import xyz.quaver.pupil.webView -import xyz.quaver.pupil.webViewFlow -import xyz.quaver.pupil.webViewReady -import xyz.quaver.readText -import java.net.URL -import java.nio.charset.Charset +import xyz.quaver.pupil.* import java.util.* +import java.util.concurrent.ConcurrentHashMap import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine const val protocol = "https:" -suspend inline fun WebView.evaluate(script: String): String = withContext(Dispatchers.Main) { - while (!webViewReady) yield() +val evaluations = Collections.newSetFromMap(ConcurrentHashMap()) + +suspend fun WebView.evaluate(script: String): String = withContext(Dispatchers.Main) { + if (webViewFailed) { + Toast.makeText(Pupil.instance, "Failed to load scripts. Please restart the app.", Toast.LENGTH_LONG).show() + } + + while (webViewFailed || !webViewReady) yield() + + val uid = UUID.randomUUID().toString() + + evaluations.add(uid) val result: String = suspendCoroutine { continuation -> evaluateJavascript(script) { + evaluations.remove(uid) continuation.resume(it) } } @@ -55,15 +64,24 @@ suspend inline fun WebView.evaluate(script: String): String = withContext(Dispat } @OptIn(ExperimentalCoroutinesApi::class) -suspend inline fun WebView.evaluatePromise(script: String, then: String = ".then(result => Callback.onResult(%uid, JSON.stringify(result)))"): String = withContext(Dispatchers.Main) { - while (!webViewReady) yield() +suspend fun WebView.evaluatePromise(script: String, then: String = ".then(result => Callback.onResult(%uid, JSON.stringify(result))).catch(err => Callback.onError(%uid, JSON.stringify(error)))"): String? = withContext(Dispatchers.Main) { + if (webViewFailed) { + Toast.makeText(Pupil.instance, "Failed to load the scripts. Please restart the app.", Toast.LENGTH_LONG).show() + } + + while (webViewFailed || !webViewReady) yield() val uid = UUID.randomUUID().toString() + evaluations.add(uid) + evaluateJavascript((script+then).replace("%uid", "'$uid'"), null) - val flow: Flow> = webViewFlow.transformWhile { (currentUid, result) -> - if (currentUid == uid) emit(currentUid to result) + val flow: Flow> = webViewFlow.transformWhile { (currentUid, result) -> + if (currentUid == uid) { + evaluations.remove(uid) + emit(currentUid to result) + } currentUid != uid } @@ -72,17 +90,9 @@ suspend inline fun WebView.evaluatePromise(script: String, then: String = ".then @Suppress("EXPERIMENTAL_API_USAGE") suspend fun getGalleryInfo(galleryID: Int): GalleryInfo { - val result = webView.evaluatePromise( - """ - new Promise((resolve, reject) => { - $.getScript('https://$domain/galleries/$galleryID.js', () => { - resolve(galleryinfo) - }); - }) - """.trimIndent() - ) + val result = webView.evaluatePromise("get_gallery_info($galleryID)") - return json.decodeFromString(result) + return json.decodeFromString(result!!) } //common.js @@ -105,6 +115,16 @@ suspend fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? """.trimIndent() ) + + FirebaseCrashlytics.getInstance().log( + """ + url_from_url_from_hash( + ${galleryID.toString().js}, + ${Json.encodeToString(image)}, + ${dir.js}, ${ext.js}, ${base.js} + ) + """.trimIndent() + ) return Json.decodeFromString(result) } diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt index 3232af9c..318de9f5 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/galleryblock.kt @@ -52,7 +52,7 @@ suspend fun getGalleryBlock(galleryID: Int) : GalleryBlock { }); """.trimIndent(), then = "" - ) + )!! val doc = Jsoup.parse(html) diff --git a/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt b/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt index 858df44e..df2562f1 100644 --- a/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt +++ b/app/src/main/java/xyz/quaver/pupil/hitomi/search.kt @@ -16,8 +16,6 @@ package xyz.quaver.pupil.hitomi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString @@ -29,7 +27,7 @@ const val extension = ".html" @OptIn(ExperimentalSerializationApi::class) suspend fun getGalleryIDsForQuery(query: String) : Set { - val result = webView.evaluatePromise("get_galleryids_for_query('$query')") + val result = webView.evaluatePromise("get_galleryids_for_query('$query')") ?: return emptySet() return Json.decodeFromString(result) } @@ -39,14 +37,14 @@ data class Suggestion(val s: String, val t: Int, val u: String, val n: String) @OptIn(ExperimentalSerializationApi::class) suspend fun getSuggestionsForQuery(query: String) : List { - val result = webView.evaluatePromise("get_suggestions_for_query('$query')") + val result = webView.evaluatePromise("get_suggestions_for_query('$query')") ?: return emptyList() - return Json.decodeFromString?>>(result)[0]!! + return Json.decodeFromString?>>(result)[0] ?: return emptyList() } @OptIn(ExperimentalSerializationApi::class) suspend fun getGalleryIDsFromNozomi(area: String?, tag: String, language: String) : Set { val jsArea = if (area == null) "null" else "'$area'" - return Json.decodeFromString(webView.evaluatePromise("""get_galleryids_from_nozomi($jsArea, '$tag', '$language')""")) + return Json.decodeFromString(webView.evaluatePromise("""get_galleryids_from_nozomi($jsArea, '$tag', '$language')""") ?: return emptySet()) } \ No newline at end of file