From c3e48a331aebb2c5fde72aece9cf16576f7fc42d Mon Sep 17 00:00:00 2001 From: Ax333l Date: Sun, 1 Dec 2024 21:29:12 +0100 Subject: [PATCH] document and improve the webview api --- downloader-plugin/build.gradle.kts | 2 +- .../manager/plugin/downloader/webview/API.kt | 111 +++++++++++++----- example-downloader-plugin/build.gradle.kts | 4 +- .../downloader/example/ExamplePlugin.kt | 10 +- 4 files changed, 89 insertions(+), 38 deletions(-) diff --git a/downloader-plugin/build.gradle.kts b/downloader-plugin/build.gradle.kts index dfa6beee..9d66a6e0 100644 --- a/downloader-plugin/build.gradle.kts +++ b/downloader-plugin/build.gradle.kts @@ -7,7 +7,7 @@ plugins { android { namespace = "app.revanced.manager.plugin.downloader" - compileSdk = 34 + compileSdk = 35 defaultConfig { minSdk = 26 diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/API.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/API.kt index 4e0df78c..4caaf23b 100644 --- a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/API.kt +++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/API.kt @@ -5,6 +5,8 @@ import android.os.Bundle import android.os.Parcelable import app.revanced.manager.plugin.downloader.DownloaderScope import app.revanced.manager.plugin.downloader.GetResult +import app.revanced.manager.plugin.downloader.GetScope +import app.revanced.manager.plugin.downloader.Scope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -14,32 +16,49 @@ import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import java.net.HttpURLConnection import java.net.URI +import kotlin.properties.Delegates internal typealias PageLoadCallback = suspend WebViewCallbackScope.(url: String) -> Unit internal typealias DownloadCallback = suspend WebViewCallbackScope.(url: String, mimeType: String, userAgent: String) -> Unit internal typealias ReadyCallback = suspend WebViewCallbackScope.() -> Unit @Parcelize -data class DownloadUrl(val url: String, val mimeType: String, val userAgent: String) : Parcelable { +/** + * A data class for storing a download + */ +data class DownloadUrl(val url: String, val userAgent: String?) : Parcelable { + /** + * Converts this into a [app.revanced.manager.plugin.downloader.DownloadResult]. + */ fun toResult() = with(URI.create(url).toURL().openConnection() as HttpURLConnection) { useCaches = false allowUserInteraction = false - setRequestProperty("User-Agent", userAgent) + userAgent?.let { setRequestProperty("User-Agent", it) } + connectTimeout = 10_000 connect() + inputStream to getHeaderField("Content-Length").toLong() } } -interface WebViewCallbackScope { - suspend fun finish(result: GetResult?) +interface WebViewCallbackScope : Scope { + /** + * Finishes the activity and returns the [result]. + */ + suspend fun finish(result: T) + + /** + * Tells the WebView to load the specified [url]. + */ suspend fun load(url: String) } -class WebViewScope internal constructor( +class WebViewScope internal constructor( coroutineScope: CoroutineScope, - setResult: (GetResult?) -> Unit -) { + private val scopeImpl: Scope, + setResult: (T) -> Unit +) : Scope by scopeImpl { private var onPageLoadCallback: PageLoadCallback = {} private var onDownloadCallback: DownloadCallback = { _, _, _ -> } private var onReadyCallback: ReadyCallback = @@ -76,8 +95,8 @@ class WebViewScope internal constructor( } } - private val callbackScope = object : WebViewCallbackScope { - override suspend fun finish(result: GetResult?) { + private val callbackScope = object : WebViewCallbackScope, Scope by scopeImpl { + override suspend fun finish(result: T) { setResult(result) // Tell the WebViewActivity to finish webView.let { withContext(Dispatchers.IO) { it.finish() } } @@ -89,38 +108,66 @@ class WebViewScope internal constructor( } - fun onDownload(block: DownloadCallback) { + /** + * Called when the WebView attempts to navigate to a downloadable file. + */ + fun download(block: DownloadCallback) { onDownloadCallback = block } - fun onPageLoad(block: PageLoadCallback) { + /** + * Called when the WebView finishes loading a page. + */ + fun pageLoad(block: PageLoadCallback) { onPageLoadCallback = block } - fun onReady(block: ReadyCallback) { + /** + * Called when the WebView is ready. This should always call [WebViewCallbackScope.load]. + */ + fun ready(block: ReadyCallback) { onReadyCallback = block } } -fun DownloaderScope.webView(block: WebViewScope.(packageName: String, version: String?) -> Unit) = - get { pkgName, version -> - var result: GetResult? = null +@JvmInline +private value class Container(val value: U) - coroutineScope { - val scope = WebViewScope(this) { result = it } - scope.block(pkgName, version) - requestStartActivity(Intent().apply { - putExtras(Bundle().apply { - putBinder(WebViewActivity.BINDER_KEY, scope.binder) - val pm = context.packageManager - val label = pm.getPackageInfo(pluginPackageName, 0).applicationInfo.loadLabel(pm).toString() - putString(WebViewActivity.TITLE_KEY, label) - }) - setClassName( - hostPackageName, - WebViewActivity::class.qualifiedName!! - ) +private suspend fun GetScope.runWebView(title: String, block: WebViewScope.() -> Unit) = + coroutineScope { + var result by Delegates.notNull>() + + val scope = WebViewScope(this@coroutineScope, this@runWebView) { result = Container(it) } + scope.block() + + // Start the webview activity and wait until it finishes + requestStartActivity(Intent().apply { + putExtras(Bundle().apply { + putBinder(WebViewActivity.BINDER_KEY, scope.binder) + putString(WebViewActivity.TITLE_KEY, title) }) - } - result - } \ No newline at end of file + setClassName( + hostPackageName, + WebViewActivity::class.qualifiedName!! + ) + }) + + result.value + } + +/** + * Implements [DownloaderScope.get] using an [android.webkit.WebView]. Event handlers are defined in the provided [block]. + * The activity will keep running until it is cancelled or an event handler calls [WebViewCallbackScope.finish]. + * + * @param title The title that will be shown in the WebView activity. The default value is the plugin application label. + */ +fun DownloaderScope.webView( + title: String = context.applicationInfo.loadLabel( + context.packageManager + ).toString(), + block: WebViewScope?>.(packageName: String, version: String?) -> Unit +) = get { pkgName, version -> + runWebView(title) { + block(pkgName, version) + } +} \ No newline at end of file diff --git a/example-downloader-plugin/build.gradle.kts b/example-downloader-plugin/build.gradle.kts index ec81e0e6..b480add9 100644 --- a/example-downloader-plugin/build.gradle.kts +++ b/example-downloader-plugin/build.gradle.kts @@ -9,12 +9,12 @@ android { val packageName = "app.revanced.manager.plugin.downloader.example" namespace = packageName - compileSdk = 34 + compileSdk = 35 defaultConfig { applicationId = packageName minSdk = 26 - targetSdk = 34 + targetSdk = 35 versionCode = 1 versionName = "1.0" } diff --git a/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugin.kt b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugin.kt index 5bf58725..fa46e423 100644 --- a/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugin.kt +++ b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugin.kt @@ -54,11 +54,15 @@ val installedAppDownloader = downloader { build().toString() } - onDownload { url, mimeType, userAgent -> - finish(DownloadUrl(url, mimeType, userAgent) to version) + download { url, _, userAgent -> + finish(DownloadUrl(url, userAgent) to version) } - onReady { + pageLoad { url -> + println(url) + } + + ready { load(startUrl) } }