mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-05-05 16:24:27 +02:00
a
This commit is contained in:
parent
11a2a140e6
commit
754988a395
@ -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(
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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")
|
||||
|
@ -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 {
|
||||
|
@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
@ -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(
|
||||
|
@ -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
|
||||
)
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
downloader-plugin/src/main/res/layout/activity_webview.xml
Normal file
17
downloader-plugin/src/main/res/layout/activity_webview.xml
Normal 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>
|
1
downloader-plugin/src/main/res/values/strings.xml
Normal file
1
downloader-plugin/src/main/res/values/strings.xml
Normal file
@ -0,0 +1 @@
|
||||
<resources></resources>
|
@ -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
|
||||
}
|
||||
|
@ -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" }
|
||||
|
Loading…
x
Reference in New Issue
Block a user