From c8ddcda3848dbcef3927aa63ba19277e705dca16 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Fri, 19 Jan 2024 17:28:35 +0100 Subject: [PATCH] Refs, Dev portal improvements and on-device testing, Fix crashes on disabling v8 race conditions, edgecase where history could be null, issue on starting Grayjay with an url --- app/src/main/assets/devportal/dev_bridge.js | 3 + app/src/main/assets/devportal/index.html | 55 +++++++++++- .../platformplayer/activities/MainActivity.kt | 64 ++++++++------ .../developer/DeveloperEndpoints.kt | 87 +++++++++++++++++++ .../futo/platformplayer/engine/V8Plugin.kt | 14 ++- .../mainactivity/main/HomeFragment.kt | 7 +- .../others/PlatformLinkMovementMethod.kt | 48 +++++----- .../platformplayer/states/StateHistory.kt | 13 ++- .../platformplayer/states/StatePlatform.kt | 10 ++- .../stores/db/ManagedDBStore.kt | 5 +- .../views/behavior/NonScrollingTextView.kt | 47 +++++----- app/src/stable/assets/sources/rumble | 2 +- app/src/unstable/assets/sources/rumble | 2 +- 13 files changed, 275 insertions(+), 82 deletions(-) diff --git a/app/src/main/assets/devportal/dev_bridge.js b/app/src/main/assets/devportal/dev_bridge.js index 8186c8ae..b89f303d 100644 --- a/app/src/main/assets/devportal/dev_bridge.js +++ b/app/src/main/assets/devportal/dev_bridge.js @@ -233,6 +233,9 @@ function pluginRemoteProp(objID, propName) { function pluginRemoteCall(objID, methodName, args) { return JSON.parse(syncPOST("/plugin/remoteCall?id=" + objID + "&method=" + methodName, {}, JSON.stringify(args))); } +function pluginRemoteTest(methodName, args) { + return JSON.parse(syncPOST("/plugin/remoteTest?method=" + methodName, {}, JSON.stringify(args))); +} function pluginIsLoggedIn(cb, err) { fetch("/plugin/isLoggedIn", { diff --git a/app/src/main/assets/devportal/index.html b/app/src/main/assets/devportal/index.html index a9f41794..73acbbbb 100644 --- a/app/src/main/assets/devportal/index.html +++ b/app/src/main/assets/devportal/index.html @@ -385,8 +385,8 @@
- - + +
(Optional) @@ -416,6 +416,9 @@ + + Test Android + Test @@ -545,6 +548,7 @@ new Vue({ el: '#app', data: { + searchTestMethods: "", page: "Plugin", pastPluginUrls: [], settings: {}, @@ -860,6 +864,53 @@ "Error: " + ex; } }, + testSourceRemotely(req) { + const name = req.title; + const parameterVals = req.parameters.map(x=>{ + if(x.value && x.value.startsWith && x.value.startsWith("json:")) + return JSON.parse(x.value.substring(5)); + return x.value + }); + + if(name == "enable") { + if(parameterVals.length > 0) + parameterVals[0] = this.Plugin.currentPlugin; + else + parameterVals.push(this.Plugin.currentPlugin); + if(parameterVals.length > 1) + parameterVals[1] = __DEV_SETTINGS; + else + parameterVals.push(__DEV_SETTINGS); + } + + const func = source[name]; + if(!func) + alert("Test func not found"); + + try { + const remoteResult = pluginRemoteTest(name, parameterVals); + console.log("Result for " + req.title, remoteResult); + this.Testing.lastResult = "//Results [" + name + "]\n" + + JSON.stringify(remoteResult, null, 3); + this.Testing.lastResultError = ""; + } + catch(ex) { + if(ex.plugin_type == "CaptchaRequiredException") { + let shouldCaptcha = confirm("Do you want to request captcha?"); + if(shouldCaptcha) { + pluginCaptchaTestPlugin(ex.url, ex.body); + } + } + console.error("Failed to run test for " + req.title, ex); + this.Testing.lastResult = "" + if(ex.message) + this.Testing.lastResultError = "//Results [" + name + "]\n\n" + + "Error: " + ex.message + "\n\n" + ex.stack; + else + this.Testing.lastResultError = "//Results [" + name + "]\n\n" + + "Error: " + ex; + } + }, showTestResults(results) { }, diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index 4889df55..3ba92676 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -29,6 +29,7 @@ import androidx.fragment.app.FragmentContainerView import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.* +import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment @@ -141,7 +142,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { } try { - handleUrlAll(content) + runBlocking { + handleUrlAll(content) + } } catch (e: Throwable) { Logger.i(TAG, "Failed to handle URL.", e) UIDialogs.toast(this, "Failed to handle URL: ${e.message}") @@ -540,7 +543,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { Pair("grayjay") { req -> StateApp.instance.contextOrNull?.let { if(it is MainActivity) { - it.handleUrlAll(req.url.toString()); + runBlocking { + it.handleUrlAll(req.url.toString()); + } } }; } @@ -552,7 +557,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { try { if (targetData != null) { - handleUrlAll(targetData) + runBlocking { + handleUrlAll(targetData) + } } } catch(ex: Throwable) { @@ -560,7 +567,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { } } - fun handleUrlAll(url: String) { + suspend fun handleUrlAll(url: String) { val uri = Uri.parse(url) when (uri.scheme) { "grayjay" -> { @@ -644,31 +651,38 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { } } - fun handleUrl(url: String): Boolean { + suspend fun handleUrl(url: String): Boolean { Logger.i(TAG, "handleUrl(url=$url)") - if (StatePlatform.instance.hasEnabledVideoClient(url)) { - navigate(_fragVideoDetail, url); - _fragVideoDetail.maximizeVideoDetail(true); - return true; - } else if(StatePlatform.instance.hasEnabledChannelClient(url)) { - navigate(_fragMainChannel, url); + return withContext(Dispatchers.IO) { + Logger.i(TAG, "handleUrl(url=$url) on IO"); + if (StatePlatform.instance.hasEnabledVideoClient(url)) { + Logger.i(TAG, "handleUrl(url=$url) found video client"); + lifecycleScope.launch(Dispatchers.Main) { + navigate(_fragVideoDetail, url); - lifecycleScope.launch { - delay(100); - _fragVideoDetail.minimizeVideoDetail(); - }; - return true; + _fragVideoDetail.maximizeVideoDetail(true); + } + return@withContext true; + } else if (StatePlatform.instance.hasEnabledChannelClient(url)) { + Logger.i(TAG, "handleUrl(url=$url) found channel client"); + lifecycleScope.launch(Dispatchers.Main) { + navigate(_fragMainChannel, url); + delay(100); + _fragVideoDetail.minimizeVideoDetail(); + }; + return@withContext true; + } else if (StatePlatform.instance.hasEnabledPlaylistClient(url)) { + Logger.i(TAG, "handleUrl(url=$url) found playlist client"); + lifecycleScope.launch(Dispatchers.Main) { + navigate(_fragMainPlaylist, url); + delay(100); + _fragVideoDetail.minimizeVideoDetail(); + }; + return@withContext true; + } + return@withContext false; } - else if(StatePlatform.instance.hasEnabledPlaylistClient(url)) { - navigate(_fragMainPlaylist, url); - lifecycleScope.launch { - delay(100); - _fragVideoDetail.minimizeVideoDetail(); - }; - return true; - } - return false; } fun handleContent(file: String, mime: String? = null): Boolean { Logger.i(TAG, "handleContent(url=$file)"); diff --git a/app/src/main/java/com/futo/platformplayer/developer/DeveloperEndpoints.kt b/app/src/main/java/com/futo/platformplayer/developer/DeveloperEndpoints.kt index 0245d9bc..7348af6a 100644 --- a/app/src/main/java/com/futo/platformplayer/developer/DeveloperEndpoints.kt +++ b/app/src/main/java/com/futo/platformplayer/developer/DeveloperEndpoints.kt @@ -9,7 +9,10 @@ import com.futo.platformplayer.api.http.server.HttpGET import com.futo.platformplayer.api.http.server.HttpPOST import com.futo.platformplayer.api.media.platforms.js.JSClient import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig +import com.futo.platformplayer.api.media.platforms.js.SourcePluginDescriptor +import com.futo.platformplayer.api.media.platforms.js.internal.JSDocs import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient +import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.dev.V8RemoteObject import com.futo.platformplayer.engine.dev.V8RemoteObject.Companion.gsonStandard @@ -20,18 +23,29 @@ import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateAssets import com.futo.platformplayer.states.StateDeveloper import com.futo.platformplayer.states.StatePlatform +import com.google.gson.ExclusionStrategy +import com.google.gson.FieldAttributes +import com.google.gson.Gson +import com.google.gson.GsonBuilder import com.google.gson.JsonArray +import com.google.gson.JsonElement import com.google.gson.JsonParser import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import java.lang.reflect.Field import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Modifier import java.util.UUID +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.memberFunctions +import kotlin.reflect.jvm.javaType import kotlin.reflect.jvm.jvmErasure class DeveloperEndpoints(private val context: Context) { private val TAG = "DeveloperEndpoints"; private val _client = ManagedHttpClient(); private var _testPlugin: V8Plugin? = null; + private var _testPluginFull: JSClient? = null; private val testPluginOrThrow: V8Plugin get() = _testPlugin ?: throw IllegalStateException("Attempted to use test plugin without plugin"); private val _testPluginVariables: HashMap = hashMapOf(); @@ -190,6 +204,17 @@ class DeveloperEndpoints(private val context: Context) { val client = JSHttpClient(null, null, null, config); val clientAuth = JSHttpClient(null, null, null, config); _testPlugin = V8Plugin(StateApp.instance.context, config, null, client, clientAuth); + try { + val script = _client.get(config.absoluteScriptUrl); + _testPluginFull = JSClient(StateApp.instance.context, SourcePluginDescriptor( + config, null, null, null + ), null, script.body?.string() ?: ""); + _testPluginFull!!.initialize(); + } + catch (ex: Throwable) { + Logger.e(TAG, "Loading full client failed", ex); + _testPluginFull = null; + } context.respondJson(200, testPluginOrThrow.getPackageVariables()); } @@ -440,6 +465,68 @@ class DeveloperEndpoints(private val context: Context) { } } + private val _fieldAttributesField = FieldAttributes::class.java.getDeclaredField("field"); + init { + _fieldAttributesField.isAccessible = true; + } + private val _remoteTestGson = GsonBuilder() + .setExclusionStrategies(object : ExclusionStrategy { + override fun shouldSkipClass(clazz: Class<*>?): Boolean { + return clazz?.simpleName == "JSClient" || + clazz?.simpleName == "KSerializer[]" || + clazz?.simpleName == "V8ValueObject"; + } + + override fun shouldSkipField(f: FieldAttributes?): Boolean { + val isPublic = f?.hasModifier(Modifier.PUBLIC) ?: true; + if(!isPublic) { + val underlyingField = _fieldAttributesField.get(f) as Field; + return !(underlyingField.declaringClass as Class).methods.any { it.name == "get" + underlyingField.name.replaceFirstChar { it.uppercaseChar() } && Modifier.isPublic(it.modifiers) }; + } + else + return !isPublic; + } + }).create(); + @HttpPOST("/plugin/remoteTest") + fun pluginRemoteTest(context: HttpContext) { + val method = context.query.getOrDefault("method", ""); + try { + + val parameters = context.readContentString(); + val paras = JsonParser.parseString(parameters); + if(!paras.isJsonArray) + throw IllegalArgumentException("Expected json array as body"); + + val plugin = _testPluginFull ?: throw IllegalStateException("Plugin not loaded"); + + val function = plugin::class.memberFunctions.filter { it.findAnnotation() != null } + .find { it.name == method }; + if(function == null) + throw java.lang.IllegalArgumentException("Plugin method [${function}] not found"); + val callResult = function.call(*(listOf(plugin) + paras.asJsonArray.take(function.parameters.size - 1).mapIndexed { index, jsonElement -> + //For now, manual conversion. + val parameter = function.parameters[index + 1]; + val value = _remoteTestGson.fromJson(jsonElement, parameter.type.javaType); + return@mapIndexed value; + }).toTypedArray()); + val json = if(callResult is IPager<*>) + _remoteTestGson.toJson(callResult.getResults()) + else + _remoteTestGson.toJson(callResult); + //val json = wrapRemoteResult(callResult, false); + + context.respondCode(200, json); + } + catch(ex: InvocationTargetException) { + Logger.e(TAG, "Remote test for [${method}] is failed", ex.targetException); + context.respondCode(500, ex.targetException.message ?: "", "text/plain") + } + catch(ex: Exception) { + Logger.e(TAG, "Remote test for [${method}] is failed", ex); + context.respondCode(500, ex.message ?: "", "text/plain") + } + } + //Internal calls @HttpPOST("/get") fun get(context: HttpContext) { diff --git a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt index 8b7c08da..4fe088c3 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -2,6 +2,7 @@ package com.futo.platformplayer.engine import android.content.Context import com.caoccao.javet.exceptions.JavetCompilationException +import com.caoccao.javet.exceptions.JavetException import com.caoccao.javet.exceptions.JavetExecutionException import com.caoccao.javet.interop.V8Host import com.caoccao.javet.interop.V8Runtime @@ -10,6 +11,7 @@ import com.caoccao.javet.values.primitive.V8ValueBoolean import com.caoccao.javet.values.primitive.V8ValueInteger import com.caoccao.javet.values.primitive.V8ValueString import com.caoccao.javet.values.reference.V8ValueObject +import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient import com.futo.platformplayer.constructs.Event1 @@ -173,8 +175,16 @@ class V8Plugin { isStopped = true; _runtime?.let { _runtime = null; - if(!it.isClosed && !it.isDead) - it.close(); + if(!it.isClosed && !it.isDead) { + try { + it.close(); + } + catch(ex: JavetException) { + //In case race conditions are going on, already closed runtimes are fine. + if(ex.message?.contains("Runtime is already closed") != true) + throw ex; + } + } Logger.i(TAG, "Stopped plugin [${config.name}]"); }; } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt index 5a95ac1f..3e60c4eb 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt @@ -32,6 +32,9 @@ import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewHolder import com.futo.platformplayer.views.announcements.AnnouncementView import com.futo.platformplayer.views.buttons.BigButton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import java.time.OffsetDateTime import java.util.UUID @@ -168,7 +171,9 @@ class HomeFragment : MainFragment() { Pair("grayjay") { req -> StateApp.instance.contextOrNull?.let { if(it is MainActivity) { - it.handleUrlAll(req.url.toString()); + runBlocking { + it.handleUrlAll(req.url.toString()); + } } }; } diff --git a/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt b/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt index dc414440..a9c83202 100644 --- a/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt +++ b/app/src/main/java/com/futo/platformplayer/others/PlatformLinkMovementMethod.kt @@ -12,6 +12,7 @@ import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.receivers.MediaControlReceiver import com.futo.platformplayer.timestampRegex +import kotlinx.coroutines.runBlocking class PlatformLinkMovementMethod : LinkMovementMethod { private val _context: Context; @@ -32,33 +33,36 @@ class PlatformLinkMovementMethod : LinkMovementMethod { val links = buffer.getSpans(off, off, URLSpan::class.java); if (links.isNotEmpty()) { - for (link in links) { - Logger.i(TAG) { "Link clicked '${link.url}'." }; + runBlocking { + for (link in links) { + Logger.i(TAG) { "Link clicked '${link.url}'." }; - if (_context is MainActivity) { - if (_context.handleUrl(link.url)) { - continue; - } - - if (timestampRegex.matches(link.url)) { - val tokens = link.url.split(':'); - - var time_s = -1L; - if (tokens.size == 2) { - time_s = tokens[0].toLong() * 60 + tokens[1].toLong(); - } else if (tokens.size == 3) { - time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong(); - } - - if (time_s != -1L) { - MediaControlReceiver.onSeekToReceived.emit(time_s * 1000); + if (_context is MainActivity) { + if (_context.handleUrl(link.url)) { continue; } + + if (timestampRegex.matches(link.url)) { + val tokens = link.url.split(':'); + + var time_s = -1L; + if (tokens.size == 2) { + time_s = tokens[0].toLong() * 60 + tokens[1].toLong(); + } else if (tokens.size == 3) { + time_s = + tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong(); + } + + if (time_s != -1L) { + MediaControlReceiver.onSeekToReceived.emit(time_s * 1000); + continue; + } + } } + + + _context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url))); } - - - _context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url))); } return true; diff --git a/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt b/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt index c6d2f4bf..2beaf40a 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateHistory.kt @@ -1,5 +1,6 @@ package com.futo.platformplayer.states +import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.structures.IPager @@ -92,12 +93,18 @@ class StateHistory { } fun getHistoryByVideo(video: IPlatformVideo, create: Boolean = false, watchDate: OffsetDateTime? = null): DBHistory.Index? { val existing = historyIndex[video.url]; - if(existing != null) - return _historyDBStore.get(existing.id!!); + var result: DBHistory.Index? = null; + if(existing != null) { + result = _historyDBStore.getOrNull(existing.id!!); + if(result == null) + UIDialogs.toast("History item null?\nNo history tracking.."); + } else if(create) { val newHistItem = HistoryVideo(SerializedPlatformVideo.fromVideo(video), 0, watchDate ?: OffsetDateTime.now()); val id = _historyDBStore.insert(newHistItem); - return _historyDBStore.get(id); + result = _historyDBStore.getOrNull(id); + if(result == null) + UIDialogs.toast("History creation failed?\nNo history tracking.."); } return null; } diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt index 679a9fa2..c9eb0fde 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt @@ -46,6 +46,7 @@ import com.futo.platformplayer.models.ImageVariable import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.StringArrayStorage import com.futo.platformplayer.stores.StringStorage +import com.futo.platformplayer.views.ToastView import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers @@ -166,8 +167,13 @@ class StatePlatform { var enabled: Array; synchronized(_clientsLock) { for(e in _enabledClients) { - e.disable(); - onSourceDisabled.emit(e); + try { + e.disable(); + onSourceDisabled.emit(e); + } + catch(ex: Throwable) { + UIDialogs.appToast(ToastView.Toast("If this happens often, please inform the developers on Github", false, null, "Plugin [${e.name}] failed to disable")); + } } _enabledClients.clear(); diff --git a/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt b/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt index dc873bc0..a07b4f5b 100644 --- a/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt +++ b/app/src/main/java/com/futo/platformplayer/stores/db/ManagedDBStore.kt @@ -12,6 +12,7 @@ import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.stores.v2.JsonStoreSerializer import com.futo.platformplayer.stores.v2.StoreSerializer import kotlinx.serialization.KSerializer +import java.lang.IllegalArgumentException import java.lang.reflect.Field import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap @@ -209,7 +210,9 @@ class ManagedDBStore, T, D: ManagedDBDatabase, DA fun getObject(id: Long) = get(id).obj!!; fun get(id: Long): I { - return deserializeIndex(dbDaoBase.get(_sqlGet(id))); + val result = dbDaoBase.getNullable(_sqlGet(id)) + ?: throw IllegalArgumentException("DB [${name}] has no entry with id ${id}"); + return deserializeIndex(result); } fun getOrNull(id: Long): I? { val result = dbDaoBase.getNullable(_sqlGet(id)); diff --git a/app/src/main/java/com/futo/platformplayer/views/behavior/NonScrollingTextView.kt b/app/src/main/java/com/futo/platformplayer/views/behavior/NonScrollingTextView.kt index 65ef8478..cb5e8638 100644 --- a/app/src/main/java/com/futo/platformplayer/views/behavior/NonScrollingTextView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/behavior/NonScrollingTextView.kt @@ -13,6 +13,7 @@ import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.others.PlatformLinkMovementMethod import com.futo.platformplayer.receivers.MediaControlReceiver import com.futo.platformplayer.timestampRegex +import kotlinx.coroutines.runBlocking class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView { constructor(context: Context) : super(context) {} @@ -40,32 +41,34 @@ class NonScrollingTextView : androidx.appcompat.widget.AppCompatTextView { if (text is Spannable) { val links = text.getSpans(offset, offset, URLSpan::class.java) if (links.isNotEmpty()) { - for (link in links) { - Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." }; + runBlocking { + for (link in links) { + Logger.i(PlatformLinkMovementMethod.TAG) { "Link clicked '${link.url}'." }; - val c = context; - if (c is MainActivity) { - if (c.handleUrl(link.url)) { - continue; - } - - if (timestampRegex.matches(link.url)) { - val tokens = link.url.split(':'); - - var time_s = -1L; - if (tokens.size == 2) { - time_s = tokens[0].toLong() * 60 + tokens[1].toLong(); - } else if (tokens.size == 3) { - time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong(); - } - - if (time_s != -1L) { - MediaControlReceiver.onSeekToReceived.emit(time_s * 1000); + val c = context; + if (c is MainActivity) { + if (c.handleUrl(link.url)) { continue; } - } - c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url))); + if (timestampRegex.matches(link.url)) { + val tokens = link.url.split(':'); + + var time_s = -1L; + if (tokens.size == 2) { + time_s = tokens[0].toLong() * 60 + tokens[1].toLong(); + } else if (tokens.size == 3) { + time_s = tokens[0].toLong() * 60 * 60 + tokens[1].toLong() * 60 + tokens[2].toLong(); + } + + if (time_s != -1L) { + MediaControlReceiver.onSeekToReceived.emit(time_s * 1000); + continue; + } + } + + c.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link.url))); + } } } diff --git a/app/src/stable/assets/sources/rumble b/app/src/stable/assets/sources/rumble index 263ed8c7..bedbc4a9 160000 --- a/app/src/stable/assets/sources/rumble +++ b/app/src/stable/assets/sources/rumble @@ -1 +1 @@ -Subproject commit 263ed8c7dfea3915f4981b6dae0999ff02815f03 +Subproject commit bedbc4a9891913e0bfc06b94383a89478274e79d diff --git a/app/src/unstable/assets/sources/rumble b/app/src/unstable/assets/sources/rumble index 263ed8c7..bedbc4a9 160000 --- a/app/src/unstable/assets/sources/rumble +++ b/app/src/unstable/assets/sources/rumble @@ -1 +1 @@ -Subproject commit 263ed8c7dfea3915f4981b6dae0999ff02815f03 +Subproject commit bedbc4a9891913e0bfc06b94383a89478274e79d