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 9c591c1c..3e8106b7 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 @@ -2,12 +2,13 @@ package app.revanced.manager.domain.repository import android.app.Application import android.content.Context +import android.os.Parcelable import app.revanced.manager.data.room.AppDatabase import app.revanced.manager.data.room.AppDatabase.Companion.generateUid import app.revanced.manager.data.room.apps.downloaded.DownloadedApp import app.revanced.manager.network.downloader.LoadedDownloaderPlugin -import app.revanced.manager.plugin.downloader.App import app.revanced.manager.plugin.downloader.DownloadScope +import app.revanced.manager.util.PM import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.conflate @@ -19,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) { +class DownloadedAppRepository(app: Application, db: AppDatabase, private val pm: PM) { private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE) private val dao = db.downloadedAppDao() @@ -32,12 +33,15 @@ class DownloadedAppRepository(app: Application, db: AppDatabase) { suspend fun download( plugin: LoadedDownloaderPlugin, - app: App, + data: Parcelable, + expectedPackageName: String, + expectedVersion: String?, onDownload: suspend (downloadProgress: Pair) -> Unit, ): File { - this.get(app.packageName, app.version)?.let { downloaded -> - return getApkFileForApp(downloaded) - } + if (expectedVersion != null) this.get(expectedPackageName, expectedVersion) + ?.let { downloaded -> + return getApkFileForApp(downloaded) + } // Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here. val relativePath = File(generateUid().toString()) @@ -80,19 +84,23 @@ class DownloadedAppRepository(app: Application, db: AppDatabase) { ) } } - plugin.download(scope, app, stream) + plugin.download(scope, data, stream) } } .conflate() .flowOn(Dispatchers.IO) .collect { (downloaded, size) -> onDownload(downloaded.megaBytes to size.megaBytes) } - if (downloadedBytes.get() < 1) throw Exception("Downloader did not download any files") + if (downloadedBytes.get() < 1) error("Downloader did not download anything.") + val pkgInfo = + pm.getPackageInfo(targetFile.toFile()) ?: error("Downloaded APK file is invalid") + if (pkgInfo.packageName != expectedPackageName) error("Downloaded APK has the wrong package name. Expected: $expectedPackageName, Actual: ${pkgInfo.packageName}") + if (expectedVersion != null && pkgInfo.versionName != expectedVersion) error("Downloaded APK has the wrong version. Expected: $expectedVersion, Actual: ${pkgInfo.versionName}") dao.insert( DownloadedApp( - packageName = app.packageName, - version = app.version, + packageName = pkgInfo.packageName, + version = pkgInfo.versionName, directory = relativePath, ) ) 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 0dda0467..ace97914 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 @@ -2,15 +2,14 @@ package app.revanced.manager.domain.repository import android.app.Application import android.content.pm.PackageManager +import android.os.Parcelable import android.util.Log import app.revanced.manager.data.room.AppDatabase import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.network.downloader.DownloaderPluginState import app.revanced.manager.network.downloader.LoadedDownloaderPlugin -import app.revanced.manager.network.downloader.ParceledDownloaderApp -import app.revanced.manager.plugin.downloader.App -import app.revanced.manager.plugin.downloader.Downloader +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.util.PM @@ -67,12 +66,12 @@ class DownloaderPluginRepository( } } - fun unwrapParceledApp(app: ParceledDownloaderApp): Pair { + fun unwrapParceledData(data: ParceledDownloaderData): Pair { val plugin = - (_pluginStates.value[app.pluginPackageName] as? DownloaderPluginState.Loaded)?.plugin - ?: throw Exception("Downloader plugin with name ${app.pluginPackageName} is not available") + (_pluginStates.value[data.pluginPackageName] as? DownloaderPluginState.Loaded)?.plugin + ?: throw Exception("Downloader plugin with name ${data.pluginPackageName} is not available") - return plugin to app.unwrapWith(plugin) + return plugin to data.unwrapWith(plugin) } private suspend fun loadPlugin(packageName: String): DownloaderPluginState { @@ -159,7 +158,7 @@ class DownloaderPluginRepository( fun Class<*>.getDownloaderBuilder() = declaredMethods .firstOrNull { it.modifiers.isPublicStatic && it.returnType.isDownloaderBuilder && it.parameterTypes.isEmpty() } - ?.let { it(null) as DownloaderBuilder } + ?.let { it(null) as DownloaderBuilder } ?: throw Exception("Could not find a valid downloader implementation in class $canonicalName") } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/network/downloader/LoadedDownloaderPlugin.kt b/app/src/main/java/app/revanced/manager/network/downloader/LoadedDownloaderPlugin.kt index d7be6b83..ce28d047 100644 --- a/app/src/main/java/app/revanced/manager/network/downloader/LoadedDownloaderPlugin.kt +++ b/app/src/main/java/app/revanced/manager/network/downloader/LoadedDownloaderPlugin.kt @@ -1,6 +1,6 @@ package app.revanced.manager.network.downloader -import app.revanced.manager.plugin.downloader.App +import android.os.Parcelable import app.revanced.manager.plugin.downloader.DownloadScope import app.revanced.manager.plugin.downloader.GetScope import java.io.OutputStream @@ -9,7 +9,7 @@ class LoadedDownloaderPlugin( val packageName: String, val name: String, val version: String, - val get: suspend GetScope.(packageName: String, version: String?) -> App?, - val download: suspend DownloadScope.(app: App, outputStream: OutputStream) -> Unit, + val get: suspend GetScope.(packageName: String, version: String?) -> Pair?, + val download: suspend DownloadScope.(data: Parcelable, outputStream: OutputStream) -> Unit, val classLoader: ClassLoader ) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderApp.kt b/app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderData.kt similarity index 52% rename from app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderApp.kt rename to app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderData.kt index 222388b3..a43db930 100644 --- a/app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderApp.kt +++ b/app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderData.kt @@ -3,43 +3,42 @@ package app.revanced.manager.network.downloader import android.os.Build import android.os.Bundle import android.os.Parcelable -import app.revanced.manager.plugin.downloader.App import kotlinx.parcelize.Parcelize @Parcelize /** - * A parceled [App]. Instances of this class can be safely stored in a bundle without needing to set the [ClassLoader]. + * A container for [Parcelable] data returned from downloaders. Instances of this class can be safely stored in a bundle without needing to set the [ClassLoader]. */ -class ParceledDownloaderApp private constructor( +class ParceledDownloaderData private constructor( val pluginPackageName: String, private val bundle: Bundle ) : Parcelable { - constructor(plugin: LoadedDownloaderPlugin, app: App) : this( + constructor(plugin: LoadedDownloaderPlugin, data: Parcelable) : this( plugin.packageName, - createBundle(app) + createBundle(data) ) - fun unwrapWith(plugin: LoadedDownloaderPlugin): App { + fun unwrapWith(plugin: LoadedDownloaderPlugin): Parcelable { bundle.classLoader = plugin.classLoader return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val className = bundle.getString(CLASS_NAME_KEY)!! val clazz = plugin.classLoader.loadClass(className) - bundle.getParcelable(APP_KEY, clazz)!! as App - } else @Suppress("Deprecation") bundle.getParcelable(APP_KEY)!! + bundle.getParcelable(DATA_KEY, clazz)!! as Parcelable + } else @Suppress("Deprecation") bundle.getParcelable(DATA_KEY)!! } private companion object { const val CLASS_NAME_KEY = "class" - const val APP_KEY = "app" + const val DATA_KEY = "data" - fun createBundle(app: App) = Bundle().apply { - putParcelable(APP_KEY, app) + fun createBundle(data: Parcelable) = Bundle().apply { + putParcelable(DATA_KEY, data) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) putString( CLASS_NAME_KEY, - app::class.java.canonicalName + data::class.java.canonicalName ) } } 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 729e4381..9d00d43f 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 @@ -1,5 +1,6 @@ package app.revanced.manager.patcher.worker +import android.app.Activity import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager @@ -9,9 +10,11 @@ import android.content.Intent import android.content.pm.ServiceInfo import android.graphics.drawable.Icon import android.os.Build +import android.os.Parcelable import android.os.PowerManager import android.util.Log import android.view.WindowManager +import androidx.activity.result.ActivityResult import androidx.core.content.ContextCompat import androidx.work.ForegroundInfo import androidx.work.WorkerParameters @@ -30,10 +33,9 @@ import app.revanced.manager.network.downloader.LoadedDownloaderPlugin import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.runtime.CoroutineRuntime import app.revanced.manager.patcher.runtime.ProcessRuntime -import app.revanced.manager.plugin.downloader.ActivityLaunchPermit import app.revanced.manager.plugin.downloader.GetScope +import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.plugin.downloader.UserInteractionException -import app.revanced.manager.plugin.downloader.App as DownloaderApp import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.State import app.revanced.manager.util.Options @@ -49,6 +51,7 @@ import java.io.File typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit +@OptIn(PluginHostApi::class) class PatcherWorker( context: Context, parameters: WorkerParameters @@ -71,7 +74,7 @@ class PatcherWorker( val logger: Logger, val downloadProgress: MutableStateFlow?>, val patchesProgress: MutableStateFlow>, - val handleUserInteractionRequest: suspend () -> ActivityLaunchPermit?, + val handleStartActivityRequest: suspend (Intent) -> ActivityResult, val setInputFile: (File) -> Unit, val onProgress: ProgressEventHandler ) { @@ -150,10 +153,12 @@ class PatcherWorker( } } - suspend fun download(plugin: LoadedDownloaderPlugin, app: DownloaderApp) = + suspend fun download(plugin: LoadedDownloaderPlugin, data: Parcelable) = downloadedAppRepository.download( plugin, - app, + data, + args.packageName, + args.input.version, onDownload = args.downloadProgress::emit ).also { args.setInputFile(it) @@ -162,16 +167,24 @@ class PatcherWorker( val inputFile = when (val selectedApp = args.input) { is SelectedApp.Download -> { - val (plugin, app) = downloaderPluginRepository.unwrapParceledApp(selectedApp.app) + val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.app) - download(plugin, app) + download(plugin, data) } is SelectedApp.Search -> { val getScope = object : GetScope { - override suspend fun requestUserInteraction() = - args.handleUserInteractionRequest() - ?: throw UserInteractionException.RequestDenied() + 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() @@ -182,12 +195,12 @@ class PatcherWorker( selectedApp.packageName, selectedApp.version ) - ?.takeIf { selectedApp.version == null || it.version == selectedApp.version } + ?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version } } catch (e: UserInteractionException.Activity.NotCompleted) { throw e } catch (_: UserInteractionException) { null - }?.let { app -> download(plugin, app) } + }?.let { (data, _) -> download(plugin, data) } } ?: throw Exception("App is not available.") } 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 5a1ae6ac..3b5e3f40 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 @@ -1,7 +1,7 @@ package app.revanced.manager.ui.model import android.os.Parcelable -import app.revanced.manager.network.downloader.ParceledDownloaderApp +import app.revanced.manager.network.downloader.ParceledDownloaderData import kotlinx.parcelize.Parcelize import java.io.File @@ -13,7 +13,7 @@ sealed interface SelectedApp : Parcelable { data class Download( override val packageName: String, override val version: String, - val app: ParceledDownloaderApp + val app: ParceledDownloaderData ) : SelectedApp @Parcelize diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index e01bd640..c0f82541 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -1,6 +1,5 @@ package app.revanced.manager.ui.viewmodel -import android.app.Activity import android.app.Application import android.content.BroadcastReceiver import android.content.Context @@ -32,7 +31,7 @@ import app.revanced.manager.domain.worker.WorkerRepository import app.revanced.manager.patcher.logger.LogLevel import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.worker.PatcherWorker -import app.revanced.manager.plugin.downloader.ActivityLaunchPermit +import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.service.InstallService import app.revanced.manager.ui.destination.Destination @@ -64,6 +63,7 @@ import java.time.Duration import java.util.UUID @Stable +@OptIn(PluginHostApi::class) class PatcherViewModel( private val input: Destination.Patcher ) : ViewModel(), KoinComponent { @@ -81,9 +81,8 @@ class PatcherViewModel( var isInstalling by mutableStateOf(false) private set - private var currentInteractionRequest: CompletableDeferred? by mutableStateOf( - null - ) + // TODO: rename these + private var currentInteractionRequest: CompletableDeferred? by mutableStateOf(null) val activeInteractionRequest by derivedStateOf { currentInteractionRequest != null } private var launchedActivity: CompletableDeferred? = null private val launchActivityChannel = Channel() @@ -130,13 +129,29 @@ class PatcherViewModel( downloadProgress, patchesProgress, setInputFile = { inputFile = it }, - handleUserInteractionRequest = { + handleStartActivityRequest = { intent -> withContext(Dispatchers.Main) { - if (activeInteractionRequest) throw Exception("Another request is already pending.") + if (currentInteractionRequest != null) throw Exception("Another request is already pending.") try { - val job = CompletableDeferred() - currentInteractionRequest = job - job.await() + // Wait for the dialog interaction. + val accepted = with(CompletableDeferred()) { + currentInteractionRequest = this + + println(activeInteractionRequest) + await() + } + if (!accepted) throw UserInteractionException.RequestDenied() + + // Launch the activity and wait for the result. + try { + with(CompletableDeferred()) { + launchedActivity = this + launchActivityChannel.send(intent) + await() + } + } finally { + launchedActivity = null + } } finally { currentInteractionRequest = null } @@ -232,10 +247,12 @@ class PatcherViewModel( } fun rejectInteraction() { - currentInteractionRequest?.complete(null) + currentInteractionRequest?.complete(false) } fun allowInteraction() { + currentInteractionRequest?.complete(true) + /* currentInteractionRequest?.complete(ActivityLaunchPermit { intent -> withContext(Dispatchers.Main) { if (launchedActivity != null) throw Exception("An activity has already been launched.") @@ -257,7 +274,7 @@ class PatcherViewModel( launchedActivity = null } } - }) + })*/ } fun handleActivityResult(result: ActivityResult) { diff --git a/downloader-plugin/api/downloader-plugin.api b/downloader-plugin/api/downloader-plugin.api index bc446e78..a3c0d500 100644 --- a/downloader-plugin/api/downloader-plugin.api +++ b/downloader-plugin/api/downloader-plugin.api @@ -1,26 +1,3 @@ -public abstract interface class app/revanced/manager/plugin/downloader/ActivityLaunchPermit { - public abstract fun launch (Landroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public class app/revanced/manager/plugin/downloader/App : android/os/Parcelable { - public static final field CREATOR Landroid/os/Parcelable$Creator; - public fun (Ljava/lang/String;Ljava/lang/String;)V - public fun describeContents ()I - public fun equals (Ljava/lang/Object;)Z - public fun getPackageName ()Ljava/lang/String; - public fun getVersion ()Ljava/lang/String; - public fun hashCode ()I - public fun writeToParcel (Landroid/os/Parcel;I)V -} - -public final class app/revanced/manager/plugin/downloader/App$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/App; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/App; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - public final class app/revanced/manager/plugin/downloader/ConstantsKt { public static final field PLUGIN_HOST_PERMISSION Ljava/lang/String; } @@ -35,12 +12,6 @@ public final class app/revanced/manager/plugin/downloader/Downloader { public final class app/revanced/manager/plugin/downloader/DownloaderBuilder { } -public final class app/revanced/manager/plugin/downloader/DownloaderContext { - public fun (Landroid/content/Context;Ljava/lang/String;)V - public final fun getAndroidContext ()Landroid/content/Context; - public final fun getPluginHostPackageName ()Ljava/lang/String; -} - public final class app/revanced/manager/plugin/downloader/DownloaderKt { public static final fun downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder; } @@ -50,6 +21,7 @@ public final class app/revanced/manager/plugin/downloader/DownloaderScope { public final fun get (Lkotlin/jvm/functions/Function4;)V public final fun getHostPackageName ()Ljava/lang/String; public final fun getPluginPackageName ()Ljava/lang/String; + public final fun withBoundService (Landroid/content/Intent;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class app/revanced/manager/plugin/downloader/ExtensionsKt { @@ -57,7 +29,31 @@ public final class app/revanced/manager/plugin/downloader/ExtensionsKt { } public abstract interface class app/revanced/manager/plugin/downloader/GetScope { - public abstract fun requestUserInteraction (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun requestStartActivity (Landroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class app/revanced/manager/plugin/downloader/Package : android/os/Parcelable { + public static final field CREATOR Landroid/os/Parcelable$Creator; + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/manager/plugin/downloader/Package; + public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/Package;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/Package; + public final fun describeContents ()I + public fun equals (Ljava/lang/Object;)Z + public final fun getName ()Ljava/lang/String; + public final fun getVersion ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public final fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class app/revanced/manager/plugin/downloader/Package$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/Package; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/Package; + public synthetic fun newArray (I)[Ljava/lang/Object; } public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation { @@ -72,16 +68,13 @@ public abstract class app/revanced/manager/plugin/downloader/UserInteractionExce } public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/plugin/downloader/UserInteractionException$Activity { - public fun ()V } public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/plugin/downloader/UserInteractionException$Activity { - public fun (ILandroid/content/Intent;)V public final fun getIntent ()Landroid/content/Intent; public final fun getResultCode ()I } public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException { - public fun ()V } diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/App.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/App.kt deleted file mode 100644 index 3cd2265d..00000000 --- a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/App.kt +++ /dev/null @@ -1,15 +0,0 @@ -package app.revanced.manager.plugin.downloader - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import java.util.Objects - -@Parcelize -open class App(open val packageName: String, open val version: String) : Parcelable { - override fun hashCode() = Objects.hash(packageName, version) - override fun equals(other: Any?): Boolean { - if (other !is App) return false - - return other.packageName == packageName && other.version == version - } -} \ 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 d853dfd1..d07b4349 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,9 +1,16 @@ 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.os.Parcelable import java.io.InputStream import java.io.OutputStream +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine @RequiresOptIn( level = RequiresOptIn.Level.ERROR, @@ -12,43 +19,38 @@ import java.io.OutputStream annotation class PluginHostApi interface GetScope { - suspend fun requestUserInteraction(): ActivityLaunchPermit -} - -fun interface ActivityLaunchPermit { - suspend fun launch(intent: Intent): Intent? -} - -interface DownloadScope { - suspend fun reportSize(size: Long) + suspend fun requestStartActivity(intent: Intent): Intent? } typealias Size = Long typealias DownloadResult = Pair -class DownloaderScope internal constructor( +typealias Version = String +typealias GetResult = Pair + +class DownloaderScope internal constructor( /** * The package name of ReVanced Manager. */ val hostPackageName: String, internal val context: Context ) { - internal var download: (suspend DownloadScope.(A, OutputStream) -> Unit)? = null - internal var get: (suspend GetScope.(String, String?) -> A?)? = null + 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?) -> A?) { + fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult?) { get = block } /** * Define the download function for this plugin. */ - fun download(block: suspend (app: A) -> DownloadResult) { + fun download(block: suspend (data: T) -> DownloadResult) { download = { app, outputStream -> val (inputStream, size) = block(app) @@ -58,12 +60,34 @@ class DownloaderScope internal constructor( } } } + + suspend fun withBoundService(intent: Intent, block: suspend (IBinder) -> R): R { + var onBind: ((IBinder) -> Unit)? = null + val serviceConn = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) = + onBind!!(service!!) + + override fun onServiceDisconnected(name: ComponentName?) {} + } + + return try { + // TODO: add a timeout + block(suspendCoroutine { continuation -> + onBind = continuation::resume + context.bindService(intent, serviceConn, Context.BIND_AUTO_CREATE) + }) + } finally { + onBind = null + // TODO: should we stop it? + context.unbindService(serviceConn) + } + } } -class DownloaderBuilder internal constructor(private val block: DownloaderScope.() -> Unit) { +class DownloaderBuilder internal constructor(private val block: DownloaderScope.() -> Unit) { @PluginHostApi fun build(hostPackageName: String, context: Context) = - with(DownloaderScope(hostPackageName, context)) { + with(DownloaderScope(hostPackageName, context)) { block() Downloader( @@ -73,19 +97,20 @@ class DownloaderBuilder internal constructor(private val block: Downloa } } -class Downloader internal constructor( - @property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> A?, - @property:PluginHostApi val download: suspend DownloadScope.(app: A, outputStream: OutputStream) -> Unit +class Downloader internal constructor( + @property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult?, + @property:PluginHostApi val download: suspend DownloadScope.(data: T, outputStream: OutputStream) -> Unit ) -fun downloader(block: DownloaderScope.() -> Unit) = DownloaderBuilder(block) +fun downloader(block: DownloaderScope.() -> Unit) = DownloaderBuilder(block) sealed class UserInteractionException(message: String) : Exception(message) { - class RequestDenied : UserInteractionException("Request was denied") + class RequestDenied @PluginHostApi constructor() : + UserInteractionException("Request was denied") sealed class Activity(message: String) : UserInteractionException(message) { - class Cancelled : Activity("Interaction was cancelled") - class NotCompleted(val resultCode: Int, val intent: Intent?) : + class Cancelled @PluginHostApi constructor() : Activity("Interaction was cancelled") + class NotCompleted @PluginHostApi constructor(val resultCode: Int, val intent: Intent?) : Activity("Unexpected activity result code: $resultCode") } } \ No newline at end of file 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 b0e9ead1..9fc80308 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 @@ -1,8 +1,29 @@ package app.revanced.manager.plugin.downloader +import android.app.Activity +import android.app.Service +import android.content.Intent +import android.os.IBinder +import android.os.Parcelable import java.io.OutputStream +interface DownloadScope { + suspend fun reportSize(size: Long) +} + // OutputStream-based version of download -fun DownloaderScope.download(block: suspend DownloadScope.(A, OutputStream) -> Unit) { +fun DownloaderScope.download(block: suspend DownloadScope.(T, OutputStream) -> Unit) { download = block -} \ No newline at end of file +} + +suspend inline fun GetScope.requestStartActivity(packageName: String) = + requestStartActivity( + Intent().apply { setClassName(packageName, ACTIVITY::class.qualifiedName!!) } + ) + +suspend inline fun DownloaderScope<*>.withBoundService( + packageName: String, + noinline block: suspend (IBinder) -> R +) = withBoundService( + Intent().apply { setClassName(packageName, SERVICE::class.qualifiedName!!) }, block +) \ No newline at end of file diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Package.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Package.kt new file mode 100644 index 00000000..f2646872 --- /dev/null +++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Package.kt @@ -0,0 +1,7 @@ +package app.revanced.manager.plugin.downloader + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class Package(val name: String, val version: String) : Parcelable \ No newline at end of file diff --git a/example-downloader-plugin/src/main/AndroidManifest.xml b/example-downloader-plugin/src/main/AndroidManifest.xml index 9ebafb34..e10b2d28 100644 --- a/example-downloader-plugin/src/main/AndroidManifest.xml +++ b/example-downloader-plugin/src/main/AndroidManifest.xml @@ -17,7 +17,7 @@ + android:value="app.revanced.manager.plugin.downloader.example.ExamplePluginKt" /> \ 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 109a9241..d9f79cc1 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 @@ -3,25 +3,21 @@ package app.revanced.manager.plugin.downloader.example import android.app.Application -import android.content.Intent import android.content.pm.PackageManager -import app.revanced.manager.plugin.downloader.App +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 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 and update API, change dispatcher, finish UI +// TODO: document and update API (update requestUserInteraction, add bound service function), change dispatcher, finish UI @Parcelize -class InstalledApp( - override val packageName: String, - override val version: String, - internal val apkPath: String -) : App(packageName, version) +class InstalledApp(val path: String) : Parcelable private val application by lazy { // Don't do this in a real plugin. @@ -39,27 +35,19 @@ val installedAppDownloader = downloader { } catch (_: PackageManager.NameNotFoundException) { return@get null } + if (version != null && packageInfo.versionName != version) return@get null - requestUserInteraction().launch(Intent().apply { - setClassName( - pluginPackageName, - InteractionActivity::class.java.canonicalName!! - ) - }) + requestStartActivity(pluginPackageName) - InstalledApp( - packageName, - packageInfo.versionName, - packageInfo.applicationInfo.sourceDir - ).takeIf { version == null || it.version == version } + InstalledApp(packageInfo.applicationInfo.sourceDir) to packageInfo.versionName } download { app -> - with(Path(app.apkPath)) { inputStream() to fileSize() } + with(Path(app.path)) { inputStream() to fileSize() } } download { app, outputStream -> - val path = Path(app.apkPath) + val path = Path(app.path) reportSize(path.fileSize()) Files.copy(path, outputStream) }