This commit is contained in:
Ax333l 2024-10-26 23:23:35 +02:00
parent 11a2a140e6
commit 754988a395
No known key found for this signature in database
GPG Key ID: D2B4D85271127D23
15 changed files with 181 additions and 56 deletions

View File

@ -20,7 +20,7 @@ import java.nio.file.StandardOpenOption
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
import kotlin.io.path.outputStream import kotlin.io.path.outputStream
class DownloadedAppRepository(app: Application, db: AppDatabase, private val pm: PM) { class DownloadedAppRepository(private val app: Application, db: AppDatabase, private val pm: PM) {
private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE) private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE)
private val dao = db.downloadedAppDao() private val dao = db.downloadedAppDao()
@ -54,6 +54,8 @@ class DownloadedAppRepository(app: Application, db: AppDatabase, private val pm:
channelFlow { channelFlow {
val scope = object : DownloadScope { val scope = object : DownloadScope {
override val pluginPackageName = plugin.packageName
override val hostPackageName = app.packageName
override suspend fun reportSize(size: Long) { override suspend fun reportSize(size: Long) {
require(size > 0) { "Size must be greater than zero" } require(size > 0) { "Size must be greater than zero" }
require( require(

View File

@ -12,6 +12,7 @@ import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.network.downloader.ParceledDownloaderData import app.revanced.manager.network.downloader.ParceledDownloaderData
import app.revanced.manager.plugin.downloader.DownloaderBuilder import app.revanced.manager.plugin.downloader.DownloaderBuilder
import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.Scope
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import dalvik.system.PathClassLoader import dalvik.system.PathClassLoader
@ -97,7 +98,10 @@ class DownloaderPluginRepository(
.loadClass(className) .loadClass(className)
.getDownloaderBuilder() .getDownloaderBuilder()
.build( .build(
hostPackageName = app.packageName, scopeImpl = object : Scope {
override val hostPackageName = app.packageName
override val pluginPackageName = pluginContext.packageName
},
context = pluginContext context = pluginContext
) )

View File

@ -42,9 +42,11 @@ import app.revanced.manager.util.Options
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.File import java.io.File
@ -173,7 +175,12 @@ class PatcherWorker(
} }
is SelectedApp.Search -> { is SelectedApp.Search -> {
downloaderPluginRepository.loadedPluginsFlow.first()
.firstNotNullOfOrNull { plugin ->
try {
val getScope = object : GetScope { val getScope = object : GetScope {
override val pluginPackageName = plugin.packageName
override val hostPackageName = applicationContext.packageName
override suspend fun requestStartActivity(intent: Intent): Intent? { override suspend fun requestStartActivity(intent: Intent): Intent? {
val result = args.handleStartActivityRequest(intent) val result = args.handleStartActivityRequest(intent)
return when (result.resultCode) { return when (result.resultCode) {
@ -186,16 +193,13 @@ class PatcherWorker(
} }
} }
} }
withContext(Dispatchers.IO) {
downloaderPluginRepository.loadedPluginsFlow.first()
.firstNotNullOfOrNull { plugin ->
try {
plugin.get( plugin.get(
getScope, getScope,
selectedApp.packageName, selectedApp.packageName,
selectedApp.version selectedApp.version
) )
?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version } }?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
} catch (e: UserInteractionException.Activity.NotCompleted) { } catch (e: UserInteractionException.Activity.NotCompleted) {
throw e throw e
} catch (_: UserInteractionException) { } catch (_: UserInteractionException) {

View File

@ -12,7 +12,7 @@ sealed interface SelectedApp : Parcelable {
@Parcelize @Parcelize
data class Download( data class Download(
override val packageName: String, override val packageName: String,
override val version: String, override val version: String?,
val data: ParceledDownloaderData val data: ParceledDownloaderData
) : SelectedApp ) : SelectedApp

View File

@ -98,7 +98,7 @@ fun SelectedAppInfoScreen(
NavBackHandler(controller = navController) NavBackHandler(controller = navController)
AnimatedNavHost(controller = navController) { destination -> AnimatedNavHost(controller = navController) { destination ->
val error by vm.error.collectAsStateWithLifecycle(null) val error by vm.errorFlow.collectAsStateWithLifecycle(null)
when (destination) { when (destination) {
is SelectedAppInfoDestination.Main -> Scaffold( is SelectedAppInfoDestination.Main -> Scaffold(
topBar = { topBar = {

View File

@ -150,7 +150,7 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
private val launchActivityChannel = Channel<Intent>() private val launchActivityChannel = Channel<Intent>()
val launchActivityFlow = launchActivityChannel.receiveAsFlow() val launchActivityFlow = launchActivityChannel.receiveAsFlow()
val error = combine(plugins, snapshotFlow { selectedApp }) { pluginsList, app -> val errorFlow = combine(plugins, snapshotFlow { selectedApp }) { pluginsList, app ->
when { when {
app is SelectedApp.Search && pluginsList.isEmpty() -> Error.NoPlugins app is SelectedApp.Search && pluginsList.isEmpty() -> Error.NoPlugins
else -> null else -> null
@ -162,18 +162,23 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
showSourceSelector = true showSourceSelector = true
} }
fun dismissSourceSelector() { private fun cancelPluginAction() {
pluginAction?.second?.cancel() pluginAction?.second?.cancel()
pluginAction = null pluginAction = null
}
fun dismissSourceSelector() {
cancelPluginAction()
showSourceSelector = false showSourceSelector = false
} }
fun searchInPlugin(plugin: LoadedDownloaderPlugin) { fun searchInPlugin(plugin: LoadedDownloaderPlugin) {
pluginAction?.second?.cancel() cancelPluginAction()
pluginAction = null
pluginAction = plugin to viewModelScope.launch { pluginAction = plugin to viewModelScope.launch {
try { try {
val scope = object : GetScope { val scope = object : GetScope {
override val hostPackageName = app.packageName
override val pluginPackageName = plugin.packageName
override suspend fun requestStartActivity(intent: Intent) = override suspend fun requestStartActivity(intent: Intent) =
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (launchedActivity != null) error("Previous activity has not finished") if (launchedActivity != null) error("Previous activity has not finished")
@ -206,8 +211,7 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
} }
selectedApp = SelectedApp.Download( selectedApp = SelectedApp.Download(
packageName, packageName,
version version,
?: error("Umm, I guess I need to make the parameter nullable now?"),
ParceledDownloaderData(plugin, data) ParceledDownloaderData(plugin, data)
) )
} ?: app.toast("App was not found") } ?: app.toast("App was not found")

View File

@ -32,6 +32,12 @@ android {
jvmTarget = "17" jvmTarget = "17"
} }
} }
dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
}
publishing { publishing {
repositories { repositories {

View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest> </manifest>

View File

@ -1,11 +1,11 @@
package app.revanced.manager.plugin.downloader package app.revanced.manager.plugin.downloader
import android.app.Service
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.IBinder import android.os.IBinder
import android.app.Activity
import android.os.Parcelable import android.os.Parcelable
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@ -16,9 +16,36 @@ import kotlin.coroutines.suspendCoroutine
level = RequiresOptIn.Level.ERROR, level = RequiresOptIn.Level.ERROR,
message = "This API is only intended for plugin hosts, don't use it in a plugin.", message = "This API is only intended for plugin hosts, don't use it in a plugin.",
) )
@Retention(AnnotationRetention.BINARY)
annotation class PluginHostApi annotation class PluginHostApi
interface GetScope { /**
* The base interface for all DSL scopes.
*/
interface Scope {
/**
* The package name of ReVanced Manager.
*/
val hostPackageName: String
/**
* The package name of the plugin.
*/
val pluginPackageName: String
}
/**
* The scope of [DownloaderScope.get].
*/
interface GetScope : Scope {
/**
* Ask the user to perform some required interaction contained in the activity specified by the provided [Intent].
* This function returns normally with the resulting [Intent] when the activity finishes with code [Activity.RESULT_OK].
*
* @throws UserInteractionException.RequestDenied User decided to skip this plugin.
* @throws UserInteractionException.Activity.Cancelled The activity was cancelled.
* @throws UserInteractionException.Activity.NotCompleted The activity finished with an unknown result code.
*/
suspend fun requestStartActivity(intent: Intent): Intent? suspend fun requestStartActivity(intent: Intent): Intent?
} }
@ -29,24 +56,14 @@ typealias Version = String
typealias GetResult<T> = Pair<T, Version?> typealias GetResult<T> = Pair<T, Version?>
class DownloaderScope<T : Parcelable> internal constructor( class DownloaderScope<T : Parcelable> internal constructor(
/** private val scopeImpl: Scope,
* The package name of ReVanced Manager.
*/
val hostPackageName: String,
internal val context: Context internal val context: Context
) { ) : Scope by scopeImpl {
// Returning an InputStream is the primary way for plugins to implement the download function, but we also want to offer an OutputStream API since using InputStream might not be convenient in all cases.
// It is much easier to implement the main InputStream API on top of OutputStreams compared to doing it the other way around, which is why we are using OutputStream here. This detail is not visible to plugins.
internal var download: (suspend DownloadScope.(T, OutputStream) -> Unit)? = null internal var download: (suspend DownloadScope.(T, OutputStream) -> Unit)? = null
internal var get: (suspend GetScope.(String, String?) -> GetResult<T>?)? = null internal var get: (suspend GetScope.(String, String?) -> GetResult<T>?)? = null
/**
* The package name of the plugin.
*/
val pluginPackageName: String get() = context.packageName
fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?) {
get = block
}
/** /**
* Define the download function for this plugin. * Define the download function for this plugin.
*/ */
@ -61,6 +78,16 @@ class DownloaderScope<T : Parcelable> internal constructor(
} }
} }
/**
* Define the get function for this plugin.
*/
fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?) {
get = block
}
/**
* Utilize the service specified by the provided [Intent]. The service will be unbound when the scope ends.
*/
suspend fun <R : Any?> withBoundService(intent: Intent, block: suspend (IBinder) -> R): R { suspend fun <R : Any?> withBoundService(intent: Intent, block: suspend (IBinder) -> R): R {
var onBind: ((IBinder) -> Unit)? = null var onBind: ((IBinder) -> Unit)? = null
val serviceConn = object : ServiceConnection { val serviceConn = object : ServiceConnection {
@ -86,8 +113,8 @@ class DownloaderScope<T : Parcelable> internal constructor(
class DownloaderBuilder<T : Parcelable> internal constructor(private val block: DownloaderScope<T>.() -> Unit) { class DownloaderBuilder<T : Parcelable> internal constructor(private val block: DownloaderScope<T>.() -> Unit) {
@PluginHostApi @PluginHostApi
fun build(hostPackageName: String, context: Context) = fun build(scopeImpl: Scope, context: Context) =
with(DownloaderScope<T>(hostPackageName, context)) { with(DownloaderScope<T>(scopeImpl, context)) {
block() block()
Downloader( Downloader(

View File

@ -7,7 +7,10 @@ import android.os.IBinder
import android.os.Parcelable import android.os.Parcelable
import java.io.OutputStream import java.io.OutputStream
interface DownloadScope { /**
* The scope of [DownloaderScope.download].
*/
interface DownloadScope : Scope {
suspend fun reportSize(size: Long) suspend fun reportSize(size: Long)
} }
@ -16,14 +19,21 @@ fun <T : Parcelable> DownloaderScope<T>.download(block: suspend DownloadScope.(T
download = block download = block
} }
suspend inline fun <reified ACTIVITY : Activity> GetScope.requestStartActivity(packageName: String) = /**
* Performs [GetScope.requestStartActivity] with an [Intent] created using the type information of [ACTIVITY].
* @see [GetScope.requestStartActivity]
*/
suspend inline fun <reified ACTIVITY : Activity> GetScope.requestStartActivity() =
requestStartActivity( requestStartActivity(
Intent().apply { setClassName(packageName, ACTIVITY::class.qualifiedName!!) } Intent().apply { setClassName(pluginPackageName, ACTIVITY::class.qualifiedName!!) }
) )
/**
* Performs [DownloaderScope.withBoundService] with an [Intent] created using the type information of [SERVICE].
* @see [DownloaderScope.withBoundService]
*/
suspend inline fun <reified SERVICE : Service, R : Any?> DownloaderScope<*>.withBoundService( suspend inline fun <reified SERVICE : Service, R : Any?> DownloaderScope<*>.withBoundService(
packageName: String,
noinline block: suspend (IBinder) -> R noinline block: suspend (IBinder) -> R
) = withBoundService( ) = withBoundService(
Intent().apply { setClassName(packageName, SERVICE::class.qualifiedName!!) }, block Intent().apply { setClassName(pluginPackageName, SERVICE::class.qualifiedName!!) }, block
) )

View File

@ -0,0 +1,39 @@
package app.revanced.manager.plugin.downloader.webview
import android.annotation.SuppressLint
import android.os.Bundle
import android.webkit.CookieManager
import android.webkit.WebSettings
import android.webkit.WebView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import app.revanced.manager.plugin.downloader.R
// TODO: use ComponentActivity instead.
class WebViewActivity : AppCompatActivity() {
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_webview)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
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.
settings.apply {
cacheMode = WebSettings.LOAD_NO_CACHE
databaseEnabled = false
allowContentAccess = true
domStorageEnabled = false
javaScriptEnabled = true
}
}
}
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".webview.WebViewActivity">
<WebView
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>

View File

@ -0,0 +1 @@
<resources></resources>

View File

@ -14,7 +14,7 @@ import kotlin.io.path.Path
import kotlin.io.path.fileSize import kotlin.io.path.fileSize
import kotlin.io.path.inputStream import kotlin.io.path.inputStream
// TODO: document and update API (update requestUserInteraction, add bound service function), change dispatcher, finish UI // TODO: document API, update UI error presentation and strings
@Parcelize @Parcelize
class InstalledApp(val path: String) : Parcelable class InstalledApp(val path: String) : Parcelable
@ -37,7 +37,7 @@ val installedAppDownloader = downloader<InstalledApp> {
} }
if (version != null && packageInfo.versionName != version) return@get null if (version != null && packageInfo.versionName != version) return@get null
requestStartActivity<InteractionActivity>(pluginPackageName) requestStartActivity<InteractionActivity>()
InstalledApp(packageInfo.applicationInfo.sourceDir) to packageInfo.versionName InstalledApp(packageInfo.applicationInfo.sourceDir) to packageInfo.versionName
} }

View File

@ -36,6 +36,12 @@ compose-icons = "1.2.4"
kotlin-process = "1.4.1" kotlin-process = "1.4.1"
hidden-api-stub = "4.3.3" 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] [libraries]
# AndroidX Core # AndroidX Core
androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "ktx" } androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "ktx" }
@ -127,6 +133,12 @@ reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reo
# switch to br.com.devsrsouza.compose.icons after DevSrSouza/compose-icons#30 is merged # 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" } 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] [plugins]
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" } android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }