diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt index 3e8106b7..191ef67d 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt @@ -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( diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt index ace97914..8579ab69 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt @@ -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 ) diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt index 56aaaa66..e84f8047 100644 --- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt @@ -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) { diff --git a/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt b/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt index 95aa7c37..5d05c4ea 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt @@ -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 diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt index 0202e344..e759d0bc 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt @@ -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 = { diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt index 7d5a4c85..dfab990a 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt @@ -150,7 +150,7 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { private val launchActivityChannel = Channel() 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") diff --git a/downloader-plugin/build.gradle.kts b/downloader-plugin/build.gradle.kts index 90c17add..cd6a1e0a 100644 --- a/downloader-plugin/build.gradle.kts +++ b/downloader-plugin/build.gradle.kts @@ -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 { diff --git a/downloader-plugin/src/main/AndroidManifest.xml b/downloader-plugin/src/main/AndroidManifest.xml index a5918e68..74b7379f 100644 --- a/downloader-plugin/src/main/AndroidManifest.xml +++ b/downloader-plugin/src/main/AndroidManifest.xml @@ -1,4 +1,3 @@ - \ No newline at end of file diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Downloader.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Downloader.kt index d07b4349..58405583 100644 --- a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Downloader.kt +++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Downloader.kt @@ -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 = Pair class DownloaderScope 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?)? = null - /** - * The package name of the plugin. - */ - val pluginPackageName: String get() = context.packageName - - fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult?) { - get = block - } - /** * Define the download function for this plugin. */ @@ -61,6 +78,16 @@ class DownloaderScope internal constructor( } } + /** + * Define the get function for this plugin. + */ + fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult?) { + get = block + } + + /** + * Utilize the service specified by the provided [Intent]. The service will be unbound when the scope ends. + */ suspend fun withBoundService(intent: Intent, block: suspend (IBinder) -> R): R { var onBind: ((IBinder) -> Unit)? = null val serviceConn = object : ServiceConnection { @@ -86,8 +113,8 @@ class DownloaderScope internal constructor( class DownloaderBuilder internal constructor(private val block: DownloaderScope.() -> Unit) { @PluginHostApi - fun build(hostPackageName: String, context: Context) = - with(DownloaderScope(hostPackageName, context)) { + fun build(scopeImpl: Scope, context: Context) = + with(DownloaderScope(scopeImpl, context)) { block() Downloader( diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Extensions.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Extensions.kt index 9fc80308..3288ba34 100644 --- a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Extensions.kt +++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Extensions.kt @@ -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 DownloaderScope.download(block: suspend DownloadScope.(T download = block } -suspend inline fun GetScope.requestStartActivity(packageName: String) = +/** + * Performs [GetScope.requestStartActivity] with an [Intent] created using the type information of [ACTIVITY]. + * @see [GetScope.requestStartActivity] + */ +suspend inline fun 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 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 ) \ No newline at end of file diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/WebViewActivity.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/WebViewActivity.kt new file mode 100644 index 00000000..f79a51fd --- /dev/null +++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/webview/WebViewActivity.kt @@ -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(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 + } + } + } +} \ No newline at end of file diff --git a/downloader-plugin/src/main/res/layout/activity_webview.xml b/downloader-plugin/src/main/res/layout/activity_webview.xml new file mode 100644 index 00000000..f07432bb --- /dev/null +++ b/downloader-plugin/src/main/res/layout/activity_webview.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/downloader-plugin/src/main/res/values/strings.xml b/downloader-plugin/src/main/res/values/strings.xml new file mode 100644 index 00000000..73862c41 --- /dev/null +++ b/downloader-plugin/src/main/res/values/strings.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugin.kt b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugin.kt index d9f79cc1..d0de82cb 100644 --- a/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugin.kt +++ b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugin.kt @@ -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 { } if (version != null && packageInfo.versionName != version) return@get null - requestStartActivity(pluginPackageName) + requestStartActivity() InstalledApp(packageInfo.applicationInfo.sourceDir) to packageInfo.versionName } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3df65c95..8504018a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }