Browser support

This commit is contained in:
Kelvin
2026-01-30 16:17:06 +01:00
parent 5ba2f2be75
commit 31a6ea0f39
7 changed files with 100 additions and 33 deletions
@@ -61,6 +61,11 @@ class SourcePluginConfig(
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
val absoluteScriptUrl: String get() = resolveAbsoluteUrl(scriptUrl, sourceUrl)!!;
fun isOfficialAuthor(): Boolean {
return scriptSignature != null &&
scriptPublicKey == "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsoFJU4AReDyUnSQI9A99UjLCwkY8OH+1o8cdtf2EjSb+fO2qmP8MGMTAvfvgmq5d2QBJE2XHRkRO3JKcTlcc1j0WlOlU8P9W272DYCeX6oYaavpKNqGKoGEuodp9wtiyNwyH46++JfpU/uIUacZbZKkHv9gIGchmNvpKYZQjFd/8pUqXGpcXZP54tGSC9PLcY+5TozZThK7Oy1+3YEf1bZ44UinRYYATbLk/wNuAfsupvlt6nxZOcJhABhdo9V+gY0FE6Ayg5+1cd1noWhnRtLF+sPdEr3z8Nt15JEK5a/524t25FMhwz8yKxlGW5qW3QLJHSUgLQncL6a1zlZ1s8QIDAQAB"
}
private fun resolveAbsoluteUrl(url: String?, sourceUrl: String?): String? {
if(url == null)
return null;
@@ -165,6 +170,12 @@ class SourcePluginConfig(
"Unrestricted Http Header access",
"Allows this plugin to access all headers (including cookies and authorization headers) for unauthenticated requests."
))
if(packagesOptional.contains("Browser") || packages.contains("Browser")) {
list.add(Pair(
"Browser Interop",
"This plugin requires webbrowser interop. May access urls outside of the restricted urls. This will only work for official plugins and during development builds."
))
}
return list;
}
@@ -17,6 +17,7 @@ import com.caoccao.javet.values.reference.V8ValueObject
import com.caoccao.javet.values.reference.V8ValuePromise
import com.futo.platformplayer.BuildConfig
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.api.media.platforms.js.internal.JSHttpClient
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.engine.exceptions.NoInternetException
@@ -46,6 +47,7 @@ import com.futo.platformplayer.getOrDefault
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateAssets
import com.futo.platformplayer.states.StateDeveloper
import com.futo.platformplayer.toList
import com.futo.platformplayer.toV8ValueBlocking
import com.futo.platformplayer.toV8ValueAsync
@@ -220,6 +222,9 @@ class V8Plugin {
if(pack is PackageHttp) {
pack.cleanup();
}
else if(pack is PackageBrowser) {
pack.deinitialize();
}
}
_runtime?.let {
@@ -390,9 +395,16 @@ class V8Plugin {
"Utilities" -> PackageUtilities(this, config)
"JSDOM" -> PackageJSDOM(this, config)
"Browser" -> {
if(!BuildConfig.DEBUG)
throw IllegalArgumentException("Browser is only allowed for debug builds due to security");
PackageBrowser(this)
val isOfficial = (config is SourcePluginConfig && config.isOfficialAuthor());
if(BuildConfig.DEBUG)
PackageBrowser(this)
else if(isOfficial)
PackageBrowser(this)
else if(config is SourcePluginConfig && config.id == StateDeveloper.DEV_ID)
PackageBrowser(this)
else
throw IllegalArgumentException("Browser is only allowed for debug and official plugins due to security");
};
else -> if(allowNull) null else throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
};
@@ -105,6 +105,11 @@ class PackageBridge : V8Package {
)
}
@V8Function
fun hasPackage(str: String): Boolean {
return _plugin.getPackages().any { it.name == str };
}
@V8Function
fun dispose(value: V8Value) {
Logger.e(TAG, "Manual dispose: " + value.javaClass.name);
@@ -10,19 +10,26 @@ import android.webkit.WebViewClient
import androidx.collection.emptyLongSet
import com.caoccao.javet.annotations.V8Function
import com.caoccao.javet.values.reference.V8ValueFunction
import com.futo.platformplayer.api.media.platforms.js.JSClient
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDeveloper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.withTimeout
class PackageBrowser: V8Package {
override val name: String get() = "Browser";
override val variableName: String = "browser";
@Transient
private val _readySemaphore = java.util.concurrent.Semaphore(1);
private var _readySemaphore: Semaphore? = null;
@Transient
private val _callbacks = mutableMapOf<String, (String?)->Unit>();
@Transient
@@ -44,23 +51,34 @@ class PackageBrowser: V8Package {
StateApp.instance.scope.launch(Dispatchers.Main) {
_browser = WebView(StateApp.instance.contextOrNull ?: return@launch);
_browser?.settings?.javaScriptEnabled = true;
_browser?.settings?.blockNetworkImage = true;
_browser?.settings?.blockNetworkLoads = true;
_browser?.settings?.blockNetworkImage = false;
_browser?.settings?.blockNetworkLoads = false;
_browser?.settings?.allowContentAccess = false;
_browser?.settings?.allowFileAccess = false;
//_browser?.settings?.useWideViewPort = true;
//_browser?.settings?.loadWithOverviewMode = true;
_browser?.webViewClient = object : WebViewClient() {
override fun onPageCommitVisible(view: WebView?, url: String?) {
super.onPageCommitVisible(view, url)
_readySemaphore.release();
_readySemaphore?.release();
_readySemaphore = null;
Logger.i("PackageBrowser", "Browser loaded");
}
}
_browser?.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
if(consoleMessage?.messageLevel() == ConsoleMessage.MessageLevel.ERROR)
Logger.e("PackageBrowser", "Console Error:${consoleMessage?.message()} [${consoleMessage?.lineNumber()}]" ?: "");
else
Logger.i("PackageBrowser", "Console Log:" + consoleMessage?.message() ?: "");
if(consoleMessage?.messageLevel() == ConsoleMessage.MessageLevel.ERROR) {
val msg = "Browser Error:${consoleMessage?.message()} [${consoleMessage?.lineNumber()}]" ?: ""
Logger.e("PackageBrowser", msg);
if(_plugin.config is SourcePluginConfig && _plugin.config.id == StateDeveloper.DEV_ID)
StateDeveloper.instance.logDevException(StateDeveloper.instance.currentDevID ?: "", msg)
}
else {
val msg = "Browser Log:" + consoleMessage?.message() ?: "";
Logger.e("PackageBrowser", msg);
if(_plugin.config is SourcePluginConfig && _plugin.config.id == StateDeveloper.DEV_ID)
StateDeveloper.instance.logDevInfo(StateDeveloper.instance.currentDevID ?: "", msg);
}
return super.onConsoleMessage(consoleMessage)
}
}
@@ -71,6 +89,7 @@ class PackageBrowser: V8Package {
}
@V8Function
fun deinitialize() {
_browser?.destroy();
_browser = null;
}
@@ -79,27 +98,38 @@ class PackageBrowser: V8Package {
return browser.url;
}
@V8Function
fun waitTillLoaded() {
if(!_readySemaphore.tryAcquire()) {
Logger.i("PackageBrowser", "Waiting for browser to be ready");
_readySemaphore.acquire();
}
_readySemaphore.release();
Logger.i("PackageBrowser", "Browser is ready");
fun waitTillLoaded(timeout: Int = 1000): Boolean {
val acquired = _readySemaphore?.let {
if(!it.tryAcquire()) {
Logger.i("PackageBrowser", "Waiting for browser to be ready");
if(!runBlocking {
try {
return@runBlocking withTimeout(timeout.toLong(), {
it.acquire()
return@withTimeout true;
});
}
catch(ex: TimeoutCancellationException) {
return@runBlocking false;
}
}) return@let false;
}
it.release();
return@let true;
} ?: true;
if(acquired)
Logger.i("PackageBrowser", "Browser is ready");
else
Logger.i("PackageBrowser", "Browser failed wait ready");
return acquired;
}
@V8Function
fun load(url: String, html: String? = null) {
if(html != null)
Logger.i("PackageBrowser", "Browser loading html with base url [${url}]");
else
Logger.i("PackageBrowser", "Browser loading url [${url}]");
_readySemaphore.acquire();
fun load(url: String) {
Logger.i("PackageBrowser", "Browser loading url [${url}]");
_readySemaphore = Semaphore(1, 1);
StateApp.instance.scope.launch(Dispatchers.Main) {
if (html == null)
browser.loadUrl(url);
else
browser.loadDataWithBaseURL(url, html, "text/html", "utf-8", null);
browser.loadUrl(url);
}
}
@@ -10,6 +10,7 @@ import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.LinearLayout
import android.widget.Spinner
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -21,6 +22,7 @@ class BrowserFragment : MainFragment() {
override val isTab: Boolean = false;
override val hasBottomBar: Boolean get() = true;
private var _root: LinearLayout? = null;
private var _webview: WebView? = null;
private val _webviewWithoutHandling = object: WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
@@ -31,6 +33,7 @@ class BrowserFragment : MainFragment() {
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_browser, container, false);
_root = view.findViewById<LinearLayout>(R.id.root);
_webview = view.findViewById<WebView?>(R.id.webview).apply {
this.webViewClient = _webviewWithoutHandling;
this.settings.javaScriptEnabled = true;
@@ -43,7 +46,12 @@ class BrowserFragment : MainFragment() {
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
super.onShownWithView(parameter, isBack)
if(parameter is String) {
if(parameter is WebView) {
_root?.removeView(_webview);
_root?.addView(parameter);
_webview = parameter;
}
else if(parameter is String) {
_webview?.webViewClient = _webviewWithoutHandling;
_webview?.loadUrl(parameter);
}
@@ -266,7 +266,7 @@ class LibraryFragment : MainFragment() {
});
if(this.allowMusic) {
val artists = StateLibrary.instance.getArtists(ArtistOrdering.TrackCount);
val artists = StateLibrary.instance.getArtists(ArtistOrdering.TrackCount)
adapterArtists.setData(artists);
if (artists.size == 0)
sectionArtists.setEmpty(
@@ -289,7 +289,7 @@ class LibraryFragment : MainFragment() {
}
if(this.allowMusic) {
val albums = StateLibrary.instance.getAlbums();
val albums = StateLibrary.instance.getAlbums()
adapterAlbums.setData(albums);
if (albums.size == 0)
sectionAlbums.setEmpty("No albums", "No albums were found on your device", -1);
+3 -2
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
<LinearLayout
android:id="@+id/root"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@@ -11,4 +12,4 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>