mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-05-02 14:54:25 +02:00
add webview api
This commit is contained in:
parent
3029a61e99
commit
66c06a6fe5
@ -113,9 +113,10 @@ dependencies {
|
||||
implementation(libs.runtime.ktx)
|
||||
implementation(libs.runtime.compose)
|
||||
implementation(libs.splash.screen)
|
||||
implementation(libs.compose.activity)
|
||||
implementation(libs.activity.compose)
|
||||
implementation(libs.work.runtime.ktx)
|
||||
implementation(libs.preferences.datastore)
|
||||
implementation(libs.appcompat)
|
||||
|
||||
// Compose
|
||||
implementation(platform(libs.compose.bom))
|
||||
|
@ -48,6 +48,8 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
||||
|
||||
<service android:name=".service.InstallService" />
|
||||
<service android:name=".service.UninstallService" />
|
||||
|
||||
|
@ -3,6 +3,7 @@ package app.revanced.manager
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -36,7 +37,7 @@ class MainActivity : ComponentActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
enableEdgeToEdge()
|
||||
installSplashScreen()
|
||||
|
||||
val vm: MainViewModel = getAndroidViewModel()
|
||||
|
@ -215,6 +215,8 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
||||
ParceledDownloaderData(plugin, data)
|
||||
)
|
||||
} ?: app.toast("App was not found")
|
||||
} catch (e: UserInteractionException.Activity) {
|
||||
app.toast(e.message!!)
|
||||
} finally {
|
||||
pluginAction = null
|
||||
dismissSourceSelector()
|
||||
|
@ -4,5 +4,7 @@
|
||||
<style name="Theme.ReVancedManager" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.AppCompat.NoActionBar</item>
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
</style>
|
||||
</resources>
|
@ -31,12 +31,15 @@ android {
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.androidx.ktx)
|
||||
implementation(libs.activity.ktx)
|
||||
implementation(libs.runtime.ktx)
|
||||
implementation(libs.appcompat)
|
||||
}
|
||||
|
||||
publishing {
|
||||
|
@ -0,0 +1,7 @@
|
||||
// IWebView.aidl
|
||||
package app.revanced.manager.plugin.downloader.webview;
|
||||
|
||||
oneway interface IWebView {
|
||||
void load(String url);
|
||||
void finish();
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
// IWebViewEvents.aidl
|
||||
package app.revanced.manager.plugin.downloader.webview;
|
||||
|
||||
import app.revanced.manager.plugin.downloader.webview.IWebView;
|
||||
|
||||
oneway interface IWebViewEvents {
|
||||
void ready(IWebView iface);
|
||||
void pageLoad(String url);
|
||||
void download(String url, String mimetype, String userAgent);
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
package app.revanced.manager.plugin.downloader.webview
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.plugin.downloader.DownloaderScope
|
||||
import app.revanced.manager.plugin.downloader.GetResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URI
|
||||
|
||||
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 ReadyCallback<T> = suspend WebViewCallbackScope<T>.() -> Unit
|
||||
|
||||
@Parcelize
|
||||
data class DownloadUrl(val url: String, val mimeType: String, val userAgent: String) : Parcelable {
|
||||
fun toResult() = with(URI.create(url).toURL().openConnection() as HttpURLConnection) {
|
||||
useCaches = false
|
||||
allowUserInteraction = false
|
||||
setRequestProperty("User-Agent", userAgent)
|
||||
connectTimeout = 10_000
|
||||
connect()
|
||||
inputStream to getHeaderField("Content-Length").toLong()
|
||||
}
|
||||
}
|
||||
|
||||
interface WebViewCallbackScope<T : Parcelable> {
|
||||
suspend fun finish(result: GetResult<T>?)
|
||||
suspend fun load(url: String)
|
||||
}
|
||||
|
||||
class WebViewScope<T : Parcelable> internal constructor(
|
||||
coroutineScope: CoroutineScope,
|
||||
setResult: (GetResult<T>?) -> Unit
|
||||
) {
|
||||
private var onPageLoadCallback: PageLoadCallback<T> = {}
|
||||
private var onDownloadCallback: DownloadCallback<T> = { _, _, _ -> }
|
||||
private var onReadyCallback: ReadyCallback<T> =
|
||||
{ throw Exception("Ready callback not set") }
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val dispatcher = Dispatchers.Default.limitedParallelism(1)
|
||||
private var current: IWebView? = null
|
||||
private val webView: IWebView
|
||||
inline get() = current ?: throw Exception("WebView interface unavailable")
|
||||
|
||||
internal val binder = object : IWebViewEvents.Stub() {
|
||||
override fun ready(iface: IWebView?) {
|
||||
coroutineScope.launch(dispatcher) {
|
||||
val wasNull = current == null
|
||||
current = iface
|
||||
if (wasNull) onReadyCallback(callbackScope)
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageLoad(url: String?) {
|
||||
coroutineScope.launch(dispatcher) { onPageLoadCallback(callbackScope, url!!) }
|
||||
}
|
||||
|
||||
override fun download(url: String?, mimetype: String?, userAgent: String?) {
|
||||
coroutineScope.launch(dispatcher) {
|
||||
onDownloadCallback(
|
||||
callbackScope,
|
||||
url!!,
|
||||
mimetype!!,
|
||||
userAgent!!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val callbackScope = object : WebViewCallbackScope<T> {
|
||||
override suspend fun finish(result: GetResult<T>?) {
|
||||
setResult(result)
|
||||
// Tell the WebViewActivity to finish
|
||||
webView.let { withContext(Dispatchers.IO) { it.finish() } }
|
||||
}
|
||||
|
||||
override suspend fun load(url: String) {
|
||||
webView.let { withContext(Dispatchers.IO) { it.load(url) } }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun onDownload(block: DownloadCallback<T>) {
|
||||
onDownloadCallback = block
|
||||
}
|
||||
|
||||
fun onPageLoad(block: PageLoadCallback<T>) {
|
||||
onPageLoadCallback = block
|
||||
}
|
||||
|
||||
fun onReady(block: ReadyCallback<T>) {
|
||||
onReadyCallback = block
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Parcelable> DownloaderScope<T>.webView(block: WebViewScope<T>.(packageName: String, version: String?) -> Unit) =
|
||||
get { pkgName, version ->
|
||||
var result: GetResult<T>? = null
|
||||
|
||||
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!!
|
||||
)
|
||||
})
|
||||
}
|
||||
result
|
||||
}
|
@ -2,20 +2,33 @@ package app.revanced.manager.plugin.downloader.webview
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.plugin.downloader.R
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
// TODO: use ComponentActivity instead.
|
||||
class WebViewActivity : AppCompatActivity() {
|
||||
class WebViewActivity : ComponentActivity() {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val vm by viewModels<WebViewModel>()
|
||||
|
||||
enableEdgeToEdge()
|
||||
setContentView(R.layout.activity_webview)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
|
||||
@ -23,10 +36,16 @@ class WebViewActivity : AppCompatActivity() {
|
||||
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
|
||||
insets
|
||||
}
|
||||
val cookieManager = CookieManager.getInstance()
|
||||
findViewById<WebView>(R.id.content).apply {
|
||||
cookieManager.setAcceptCookie(true)
|
||||
// TODO: murder cookies if this is the first time setting it up.
|
||||
actionBar?.apply {
|
||||
title = intent.getStringExtra(TITLE_KEY)
|
||||
setHomeAsUpIndicator(android.R.drawable.ic_menu_close_clear_cancel)
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
val events = IWebViewEvents.Stub.asInterface(intent.extras!!.getBinder(BINDER_KEY))!!
|
||||
vm.setup(events)
|
||||
|
||||
val webView = findViewById<WebView>(R.id.content).apply {
|
||||
settings.apply {
|
||||
cacheMode = WebSettings.LOAD_NO_CACHE
|
||||
databaseEnabled = false
|
||||
@ -34,6 +53,86 @@ class WebViewActivity : AppCompatActivity() {
|
||||
domStorageEnabled = false
|
||||
javaScriptEnabled = true
|
||||
}
|
||||
|
||||
webViewClient = vm.webViewClient
|
||||
setDownloadListener { url, userAgent, _, mimetype, _ ->
|
||||
vm.onDownload(url, mimetype, userAgent)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
vm.commands.collect {
|
||||
when (it) {
|
||||
is WebViewModel.Command.Finish -> {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
|
||||
is WebViewModel.Command.Load -> webView.loadUrl(it.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
true
|
||||
} else super.onOptionsItemSelected(item)
|
||||
|
||||
internal companion object {
|
||||
const val BINDER_KEY = "EVENTS"
|
||||
const val TITLE_KEY = "TITLE"
|
||||
}
|
||||
}
|
||||
|
||||
internal class WebViewModel : ViewModel() {
|
||||
init {
|
||||
CookieManager.getInstance().apply {
|
||||
removeAllCookies(null)
|
||||
setAcceptCookie(true)
|
||||
}
|
||||
}
|
||||
|
||||
private val commandChannel = Channel<Command>()
|
||||
val commands = commandChannel.receiveAsFlow()
|
||||
|
||||
private var eventBinder: IWebViewEvents? = null
|
||||
private val ctrlBinder = object : IWebView.Stub() {
|
||||
override fun load(url: String?) {
|
||||
viewModelScope.launch {
|
||||
commandChannel.send(Command.Load(url!!))
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
viewModelScope.launch {
|
||||
commandChannel.send(Command.Finish)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val webViewClient = object : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
eventBinder!!.pageLoad(url)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDownload(url: String, mimeType: String, userAgent: String) {
|
||||
eventBinder!!.download(url, mimeType, userAgent)
|
||||
}
|
||||
|
||||
fun setup(binder: IWebViewEvents) {
|
||||
if (eventBinder != null) return
|
||||
eventBinder = binder
|
||||
binder.ready(ctrlBinder)
|
||||
}
|
||||
|
||||
sealed interface Command {
|
||||
data class Load(val url: String) : Command
|
||||
data object Finish : Command
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
@ -11,7 +11,6 @@
|
||||
android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:rotationX="25"
|
||||
tools:layout_editor_absoluteX="1dp"
|
||||
tools:layout_editor_absoluteY="1dp" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
7
downloader-plugin/src/main/res/values/themes.xml
Normal file
7
downloader-plugin/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.WebViewActivity" parent="Theme.AppCompat.DayNight">
|
||||
<item name="android:windowActionBar">true</item>
|
||||
<item name="android:windowNoTitle">false</item>
|
||||
</style>
|
||||
</resources>
|
@ -43,7 +43,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.compose.activity)
|
||||
implementation(libs.activity.compose)
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.compose.ui)
|
||||
implementation(libs.compose.ui.tooling)
|
||||
|
@ -3,16 +3,12 @@
|
||||
package app.revanced.manager.plugin.downloader.example
|
||||
|
||||
import android.app.Application
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.plugin.downloader.download
|
||||
import app.revanced.manager.plugin.downloader.downloader
|
||||
import app.revanced.manager.plugin.downloader.requestStartActivity
|
||||
import app.revanced.manager.plugin.downloader.webview.DownloadUrl
|
||||
import app.revanced.manager.plugin.downloader.webview.webView
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.nio.file.Files
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.fileSize
|
||||
import kotlin.io.path.inputStream
|
||||
|
||||
// TODO: document API, update UI error presentation and strings
|
||||
|
||||
@ -26,9 +22,10 @@ private val application by lazy {
|
||||
clazz.getMethod("getApplication")(activityThread) as Application
|
||||
}
|
||||
|
||||
val installedAppDownloader = downloader<InstalledApp> {
|
||||
val installedAppDownloader = downloader<DownloadUrl> {
|
||||
val pm = application.packageManager
|
||||
|
||||
/*
|
||||
get { packageName, version ->
|
||||
val packageInfo = try {
|
||||
pm.getPackageInfo(packageName, 0)
|
||||
@ -40,8 +37,37 @@ val installedAppDownloader = downloader<InstalledApp> {
|
||||
requestStartActivity<InteractionActivity>()
|
||||
|
||||
InstalledApp(packageInfo.applicationInfo.sourceDir) to packageInfo.versionName
|
||||
}*/
|
||||
webView { packageName, version ->
|
||||
val startUrl = with(Uri.Builder()) {
|
||||
scheme("https")
|
||||
authority("www.apkmirror.com")
|
||||
mapOf(
|
||||
"post_type" to "app_release",
|
||||
"searchtype" to "apk",
|
||||
"s" to (version?.let { "$packageName $it" } ?: packageName),
|
||||
"bundles%5B%5D" to "apk_files" // bundles[]
|
||||
).forEach { (key, value) ->
|
||||
appendQueryParameter(key, value)
|
||||
}
|
||||
|
||||
build().toString()
|
||||
}
|
||||
|
||||
onDownload { url, mimeType, userAgent ->
|
||||
finish(DownloadUrl(url, mimeType, userAgent) to version)
|
||||
}
|
||||
|
||||
onReady {
|
||||
load(startUrl)
|
||||
}
|
||||
}
|
||||
|
||||
download { downloadable ->
|
||||
downloadable.toResult()
|
||||
}
|
||||
|
||||
/*
|
||||
download { app ->
|
||||
with(Path(app.path)) { inputStream() to fileSize() }
|
||||
}
|
||||
@ -50,5 +76,5 @@ val installedAppDownloader = downloader<InstalledApp> {
|
||||
val path = Path(app.path)
|
||||
reportSize(path.fileSize())
|
||||
Files.copy(path, outputStream)
|
||||
}
|
||||
}*/
|
||||
}
|
@ -4,7 +4,8 @@ material3 = "1.3.1"
|
||||
ui-tooling = "1.7.5"
|
||||
viewmodel-lifecycle = "2.8.7"
|
||||
splash-screen = "1.0.1"
|
||||
compose-activity = "1.9.3"
|
||||
activity = "1.9.3"
|
||||
appcompat = "1.7.0"
|
||||
preferences-datastore = "1.1.1"
|
||||
work-runtime = "2.10.0"
|
||||
compose-bom = "2024.11.00"
|
||||
@ -37,21 +38,17 @@ compose-icons = "1.2.4"
|
||||
kotlin-process = "1.4.1"
|
||||
hidden-api-stub = "4.3.3"
|
||||
|
||||
# TODO: get rid of these.
|
||||
appcompat = "1.7.0"
|
||||
material = "1.12.0"
|
||||
activity = "1.9.1"
|
||||
constraintlayout = "2.1.4"
|
||||
|
||||
[libraries]
|
||||
# AndroidX Core
|
||||
androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "ktx" }
|
||||
runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "viewmodel-lifecycle" }
|
||||
runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "viewmodel-lifecycle" }
|
||||
splash-screen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splash-screen" }
|
||||
compose-activity = { group = "androidx.activity", name = "activity-compose", version.ref = "compose-activity" }
|
||||
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity" }
|
||||
activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" }
|
||||
work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work-runtime" }
|
||||
preferences-datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "preferences-datastore" }
|
||||
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
|
||||
# Compose
|
||||
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
|
||||
@ -138,12 +135,6 @@ reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reo
|
||||
# switch to br.com.devsrsouza.compose.icons after DevSrSouza/compose-icons#30 is merged
|
||||
compose-icons-fontawesome = { group = "com.github.BenjaminHalko.compose-icons", name = "font-awesome", version.ref = "compose-icons" }
|
||||
|
||||
|
||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
||||
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
|
||||
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }
|
||||
|
Loading…
x
Reference in New Issue
Block a user