diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt index 3e92db3c..60390e05 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/SourcePluginConfig.kt @@ -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; } 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 48361ab0..1c44aed1 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/V8Plugin.kt @@ -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}"); }; diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt index 038c734d..6c7dab49 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBridge.kt @@ -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); diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBrowser.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBrowser.kt index ddf6b7e1..4cf0ab09 100644 --- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBrowser.kt +++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageBrowser.kt @@ -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 = mutableMapOfUnit>(); @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); } } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BrowserFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BrowserFragment.kt index d73f38c7..8f08e098 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BrowserFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BrowserFragment.kt @@ -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(R.id.root); _webview = view.findViewById(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); } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFragment.kt index 16c8df37..40ae4536 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/LibraryFragment.kt @@ -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); diff --git a/app/src/main/res/layout/fragment_browser.xml b/app/src/main/res/layout/fragment_browser.xml index 9454a48e..ac8fe198 100644 --- a/app/src/main/res/layout/fragment_browser.xml +++ b/app/src/main/res/layout/fragment_browser.xml @@ -1,5 +1,6 @@ - - \ No newline at end of file + \ No newline at end of file