document and improve the webview api

This commit is contained in:
Ax333l 2024-12-01 21:29:12 +01:00
parent 66c06a6fe5
commit c3e48a331a
No known key found for this signature in database
GPG Key ID: D2B4D85271127D23
4 changed files with 89 additions and 38 deletions

View File

@ -7,7 +7,7 @@ plugins {
android { android {
namespace = "app.revanced.manager.plugin.downloader" namespace = "app.revanced.manager.plugin.downloader"
compileSdk = 34 compileSdk = 35
defaultConfig { defaultConfig {
minSdk = 26 minSdk = 26

View File

@ -5,6 +5,8 @@ import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import app.revanced.manager.plugin.downloader.DownloaderScope import app.revanced.manager.plugin.downloader.DownloaderScope
import app.revanced.manager.plugin.downloader.GetResult 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -14,32 +16,49 @@ import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URI import java.net.URI
import kotlin.properties.Delegates
internal typealias PageLoadCallback<T> = suspend WebViewCallbackScope<T>.(url: String) -> Unit internal typealias PageLoadCallback<T> = suspend WebViewCallbackScope<T>.(url: String) -> Unit
internal typealias DownloadCallback<T> = suspend WebViewCallbackScope<T>.(url: String, mimeType: String, userAgent: String) -> Unit internal typealias DownloadCallback<T> = suspend WebViewCallbackScope<T>.(url: String, mimeType: String, userAgent: String) -> Unit
internal typealias ReadyCallback<T> = suspend WebViewCallbackScope<T>.() -> Unit internal typealias ReadyCallback<T> = suspend WebViewCallbackScope<T>.() -> Unit
@Parcelize @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) { fun toResult() = with(URI.create(url).toURL().openConnection() as HttpURLConnection) {
useCaches = false useCaches = false
allowUserInteraction = false allowUserInteraction = false
setRequestProperty("User-Agent", userAgent) userAgent?.let { setRequestProperty("User-Agent", it) }
connectTimeout = 10_000 connectTimeout = 10_000
connect() connect()
inputStream to getHeaderField("Content-Length").toLong() inputStream to getHeaderField("Content-Length").toLong()
} }
} }
interface WebViewCallbackScope<T : Parcelable> { interface WebViewCallbackScope<T> : Scope {
suspend fun finish(result: GetResult<T>?) /**
* 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) suspend fun load(url: String)
} }
class WebViewScope<T : Parcelable> internal constructor( class WebViewScope<T> internal constructor(
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
setResult: (GetResult<T>?) -> Unit private val scopeImpl: Scope,
) { setResult: (T) -> Unit
) : Scope by scopeImpl {
private var onPageLoadCallback: PageLoadCallback<T> = {} private var onPageLoadCallback: PageLoadCallback<T> = {}
private var onDownloadCallback: DownloadCallback<T> = { _, _, _ -> } private var onDownloadCallback: DownloadCallback<T> = { _, _, _ -> }
private var onReadyCallback: ReadyCallback<T> = private var onReadyCallback: ReadyCallback<T> =
@ -76,8 +95,8 @@ class WebViewScope<T : Parcelable> internal constructor(
} }
} }
private val callbackScope = object : WebViewCallbackScope<T> { private val callbackScope = object : WebViewCallbackScope<T>, Scope by scopeImpl {
override suspend fun finish(result: GetResult<T>?) { override suspend fun finish(result: T) {
setResult(result) setResult(result)
// Tell the WebViewActivity to finish // Tell the WebViewActivity to finish
webView.let { withContext(Dispatchers.IO) { it.finish() } } webView.let { withContext(Dispatchers.IO) { it.finish() } }
@ -89,38 +108,66 @@ class WebViewScope<T : Parcelable> internal constructor(
} }
fun onDownload(block: DownloadCallback<T>) { /**
* Called when the WebView attempts to navigate to a downloadable file.
*/
fun download(block: DownloadCallback<T>) {
onDownloadCallback = block onDownloadCallback = block
} }
fun onPageLoad(block: PageLoadCallback<T>) { /**
* Called when the WebView finishes loading a page.
*/
fun pageLoad(block: PageLoadCallback<T>) {
onPageLoadCallback = block onPageLoadCallback = block
} }
fun onReady(block: ReadyCallback<T>) { /**
* Called when the WebView is ready. This should always call [WebViewCallbackScope.load].
*/
fun ready(block: ReadyCallback<T>) {
onReadyCallback = block onReadyCallback = block
} }
} }
fun <T : Parcelable> DownloaderScope<T>.webView(block: WebViewScope<T>.(packageName: String, version: String?) -> Unit) = @JvmInline
get { pkgName, version -> private value class Container<U>(val value: U)
var result: GetResult<T>? = null
private suspend fun <T> GetScope.runWebView(title: String, block: WebViewScope<T>.() -> Unit) =
coroutineScope { coroutineScope {
val scope = WebViewScope(this) { result = it } var result by Delegates.notNull<Container<T>>()
scope.block(pkgName, version)
val scope = WebViewScope<T>(this@coroutineScope, this@runWebView) { result = Container(it) }
scope.block()
// Start the webview activity and wait until it finishes
requestStartActivity(Intent().apply { requestStartActivity(Intent().apply {
putExtras(Bundle().apply { putExtras(Bundle().apply {
putBinder(WebViewActivity.BINDER_KEY, scope.binder) putBinder(WebViewActivity.BINDER_KEY, scope.binder)
val pm = context.packageManager putString(WebViewActivity.TITLE_KEY, title)
val label = pm.getPackageInfo(pluginPackageName, 0).applicationInfo.loadLabel(pm).toString()
putString(WebViewActivity.TITLE_KEY, label)
}) })
setClassName( setClassName(
hostPackageName, hostPackageName,
WebViewActivity::class.qualifiedName!! 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 <T : Parcelable> DownloaderScope<T>.webView(
title: String = context.applicationInfo.loadLabel(
context.packageManager
).toString(),
block: WebViewScope<GetResult<T>?>.(packageName: String, version: String?) -> Unit
) = get { pkgName, version ->
runWebView(title) {
block(pkgName, version)
} }
result
} }

View File

@ -9,12 +9,12 @@ android {
val packageName = "app.revanced.manager.plugin.downloader.example" val packageName = "app.revanced.manager.plugin.downloader.example"
namespace = packageName namespace = packageName
compileSdk = 34 compileSdk = 35
defaultConfig { defaultConfig {
applicationId = packageName applicationId = packageName
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 35
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
} }

View File

@ -54,11 +54,15 @@ val installedAppDownloader = downloader<DownloadUrl> {
build().toString() build().toString()
} }
onDownload { url, mimeType, userAgent -> download { url, _, userAgent ->
finish(DownloadUrl(url, mimeType, userAgent) to version) finish(DownloadUrl(url, userAgent) to version)
} }
onReady { pageLoad { url ->
println(url)
}
ready {
load(startUrl) load(startUrl)
} }
} }