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 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 dao = db.downloadedAppDao()
@ -54,6 +54,8 @@ class DownloadedAppRepository(app: Application, db: AppDatabase, private val pm:
channelFlow {
val scope = object : DownloadScope {
override val pluginPackageName = plugin.packageName
override val hostPackageName = app.packageName
override suspend fun reportSize(size: Long) {
require(size > 0) { "Size must be greater than zero" }
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.plugin.downloader.DownloaderBuilder
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.tag
import dalvik.system.PathClassLoader
@ -97,7 +98,10 @@ class DownloaderPluginRepository(
.loadClass(className)
.getDownloaderBuilder()
.build(
hostPackageName = app.packageName,
scopeImpl = object : Scope {
override val hostPackageName = app.packageName
override val pluginPackageName = pluginContext.packageName
},
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.PatchSelection
import app.revanced.manager.util.tag
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
@ -173,29 +175,31 @@ class PatcherWorker(
}
is SelectedApp.Search -> {
val getScope = object : GetScope {
override suspend fun requestStartActivity(intent: Intent): Intent? {
val result = args.handleStartActivityRequest(intent)
return when (result.resultCode) {
Activity.RESULT_OK -> result.data
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
else -> throw UserInteractionException.Activity.NotCompleted(
result.resultCode,
result.data
)
}
}
}
downloaderPluginRepository.loadedPluginsFlow.first()
.firstNotNullOfOrNull { plugin ->
try {
plugin.get(
getScope,
selectedApp.packageName,
selectedApp.version
)
?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
val getScope = object : GetScope {
override val pluginPackageName = plugin.packageName
override val hostPackageName = applicationContext.packageName
override suspend fun requestStartActivity(intent: Intent): Intent? {
val result = args.handleStartActivityRequest(intent)
return when (result.resultCode) {
Activity.RESULT_OK -> result.data
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
else -> throw UserInteractionException.Activity.NotCompleted(
result.resultCode,
result.data
)
}
}
}
withContext(Dispatchers.IO) {
plugin.get(
getScope,
selectedApp.packageName,
selectedApp.version
)
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
} catch (e: UserInteractionException.Activity.NotCompleted) {
throw e
} catch (_: UserInteractionException) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
package app.revanced.manager.plugin.downloader
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.app.Activity
import android.os.Parcelable
import java.io.InputStream
import java.io.OutputStream
@ -16,9 +16,36 @@ import kotlin.coroutines.suspendCoroutine
level = RequiresOptIn.Level.ERROR,
message = "This API is only intended for plugin hosts, don't use it in a plugin.",
)
@Retention(AnnotationRetention.BINARY)
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?
}
@ -29,24 +56,14 @@ typealias Version = String
typealias GetResult<T> = Pair<T, Version?>
class DownloaderScope<T : Parcelable> internal constructor(
/**
* The package name of ReVanced Manager.
*/
val hostPackageName: String,
private val scopeImpl: Scope,
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 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.
*/
@ -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 {
var onBind: ((IBinder) -> Unit)? = null
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) {
@PluginHostApi
fun build(hostPackageName: String, context: Context) =
with(DownloaderScope<T>(hostPackageName, context)) {
fun build(scopeImpl: Scope, context: Context) =
with(DownloaderScope<T>(scopeImpl, context)) {
block()
Downloader(

View File

@ -7,7 +7,10 @@ import android.os.IBinder
import android.os.Parcelable
import java.io.OutputStream
interface DownloadScope {
/**
* The scope of [DownloaderScope.download].
*/
interface DownloadScope : Scope {
suspend fun reportSize(size: Long)
}
@ -16,14 +19,21 @@ fun <T : Parcelable> DownloaderScope<T>.download(block: suspend DownloadScope.(T
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(
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(
packageName: String,
noinline block: suspend (IBinder) -> R
) = 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.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
class InstalledApp(val path: String) : Parcelable
@ -37,7 +37,7 @@ val installedAppDownloader = downloader<InstalledApp> {
}
if (version != null && packageInfo.versionName != version) return@get null
requestStartActivity<InteractionActivity>(pluginPackageName)
requestStartActivity<InteractionActivity>()
InstalledApp(packageInfo.applicationInfo.sourceDir) to packageInfo.versionName
}

View File

@ -36,6 +36,12 @@ 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" }
@ -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
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" }