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 3741ab5096..a5c3a3ee8c 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
@@ -5,15 +5,13 @@ import android.content.Context
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.plugin.downloader.DownloaderPlugin
+import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
+import app.revanced.manager.plugin.downloader.App
+import app.revanced.manager.plugin.downloader.DownloadScope
import kotlinx.coroutines.flow.distinctUntilChanged
import java.io.File
-class DownloadedAppRepository(
- app: Application,
- db: AppDatabase,
- private val downloaderPluginRepository: DownloaderPluginRepository
-) {
+class DownloadedAppRepository(app: Application, db: AppDatabase) {
private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE)
private val dao = db.downloadedAppDao()
@@ -22,10 +20,10 @@ class DownloadedAppRepository(
fun getApkFileForApp(app: DownloadedApp): File = getApkFileForDir(dir.resolve(app.directory))
private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
- suspend fun download(
- plugin: DownloaderPlugin,
- app: A,
- onDownload: suspend (downloadProgress: Pair?) -> Unit,
+ suspend fun download(
+ plugin: LoadedDownloaderPlugin,
+ app: App,
+ onDownload: suspend (downloadProgress: Pair) -> Unit,
): File {
this.get(app.packageName, app.version)?.let { downloaded ->
return getApkFileForApp(downloaded)
@@ -36,17 +34,12 @@ class DownloadedAppRepository(
val savePath = dir.resolve(relativePath).also { it.mkdirs() }
try {
- val parameters = DownloaderPlugin.DownloadParameters(
- targetFile = savePath.resolve("base.apk"),
- onDownloadProgress = { progress ->
- val (bytesReceived, bytesTotal) = progress
- ?: return@DownloadParameters onDownload(null)
+ val scope = object : DownloadScope {
+ override val saveLocation = savePath.resolve("base.apk")
+ override suspend fun reportProgress(bytesReceived: Int, bytesTotal: Int?) = onDownload(bytesReceived.megaBytes to bytesTotal?.megaBytes)
+ }
- onDownload(bytesReceived.megaBytes to bytesTotal.megaBytes)
- }
- )
-
- plugin.download(app, parameters)
+ plugin.download(scope, app)
dao.insert(
DownloadedApp(
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 fc85cf0c69..67fdbe977a 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
@@ -5,13 +5,21 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.Signature
import android.util.Log
+import androidx.paging.PagingConfig
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
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.DownloaderPlugin
+import app.revanced.manager.plugin.downloader.App
+import app.revanced.manager.plugin.downloader.DownloadScope
+import app.revanced.manager.plugin.downloader.Downloader
+import app.revanced.manager.plugin.downloader.DownloaderContext
+import app.revanced.manager.plugin.downloader.DownloaderMarker
+import app.revanced.manager.plugin.downloader.PaginatedDownloader
import app.revanced.manager.util.PM
import app.revanced.manager.util.tag
import dalvik.system.PathClassLoader
@@ -22,6 +30,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import java.io.File
+import java.lang.reflect.Modifier
class DownloaderPluginRepository(
private val pm: PM,
@@ -48,7 +57,7 @@ class DownloaderPluginRepository(
_pluginStates.value = pluginPackages.associate { it.packageName to loadPlugin(it) }
}
- fun unwrapParceledApp(app: ParceledDownloaderApp): Pair {
+ fun unwrapParceledApp(app: ParceledDownloaderApp): Pair {
val plugin =
(_pluginStates.value[app.pluginPackageName] as? DownloaderPluginState.Loaded)?.plugin
?: throw Exception("Downloader plugin with name ${app.pluginPackageName} is not available")
@@ -66,35 +75,70 @@ class DownloaderPluginRepository(
return DownloaderPluginState.Failed(e)
}
- val pluginParameters = DownloaderPlugin.Parameters(
- context = context,
+ val downloaderContext = DownloaderContext(
+ androidContext = context,
tempDirectory = fs.tempDir.resolve("dl_plugin_${packageInfo.packageName}")
.also(File::mkdir)
)
return try {
- val pluginClassName =
+ val className =
packageInfo.applicationInfo.metaData.getString(METADATA_PLUGIN_CLASS)
?: throw Exception("Missing metadata attribute $METADATA_PLUGIN_CLASS")
val classLoader = PathClassLoader(
packageInfo.applicationInfo.sourceDir,
- DownloaderPlugin::class.java.classLoader
+ Downloader::class.java.classLoader
+ )
+
+ val downloader = classLoader
+ .loadClass(className)
+ .getDownloaderImplementation(downloaderContext)
+
+ class PluginComponents(
+ val download: suspend DownloadScope.(App) -> Unit,
+ val pagingConfig: PagingConfig,
+ val versionPager: (String, String?) -> PagingSource<*, out App>
)
@Suppress("UNCHECKED_CAST")
- val downloaderPluginClass =
- classLoader.loadClass(pluginClassName) as Class>
+ val components = when (downloader) {
+ is PaginatedDownloader<*> -> PluginComponents(
+ downloader.download as suspend DownloadScope.(App) -> Unit,
+ downloader.pagingConfig,
+ downloader.versionPager
+ )
- val plugin = downloaderPluginClass
- .getDeclaredConstructor(DownloaderPlugin.Parameters::class.java)
- .newInstance(pluginParameters)
+ is Downloader<*> -> PluginComponents(
+ downloader.download as suspend DownloadScope.(App) -> Unit,
+ PagingConfig(pageSize = 1)
+ ) { packageName: String, versionHint: String? ->
+ // Convert the lambda into a PagingSource.
+ object : PagingSource() {
+ override fun getRefreshKey(state: PagingState) = null
+
+ override suspend fun load(params: LoadParams) = try {
+ LoadResult.Page(
+ downloader.getVersions(packageName, versionHint),
+ nextKey = null,
+ prevKey = null
+ )
+ } catch (e: CancellationException) {
+ throw e
+ } catch (e: Exception) {
+ LoadResult.Error(e)
+ }
+ }
+ }
+ }
DownloaderPluginState.Loaded(
LoadedDownloaderPlugin(
packageInfo.packageName,
with(pm) { packageInfo.label() },
packageInfo.versionName,
- plugin,
+ components.versionPager,
+ components.download,
+ components.pagingConfig,
classLoader
)
)
@@ -131,5 +175,23 @@ class DownloaderPluginRepository(
const val METADATA_PLUGIN_CLASS = "app.revanced.manager.plugin.downloader.class"
val packageFlags = PackageManager.GET_META_DATA or PM.signaturesFlag
+
+ val Class<*>.isDownloaderMarker get() = DownloaderMarker::class.java.isAssignableFrom(this)
+ const val PUBLIC_STATIC = Modifier.PUBLIC or Modifier.STATIC
+ val Int.isPublicStatic get() = (this and PUBLIC_STATIC) == PUBLIC_STATIC
+
+ fun Class<*>.getDownloaderImplementation(context: DownloaderContext) =
+ declaredMethods
+ .filter { it.modifiers.isPublicStatic && it.returnType.isDownloaderMarker }
+ .firstNotNullOfOrNull callMethod@{
+ if (it.parameterTypes contentEquals arrayOf(DownloaderContext::class.java)) return@callMethod it(
+ null,
+ context
+ ) as DownloaderMarker
+ if (it.parameterTypes.isEmpty()) return@callMethod it(null) as DownloaderMarker
+
+ return@callMethod null
+ }
+ ?: 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 9934a14d18..ab8d945f52 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,11 +1,16 @@
package app.revanced.manager.network.downloader
-import app.revanced.manager.plugin.downloader.DownloaderPlugin
+import androidx.paging.PagingConfig
+import androidx.paging.PagingSource
+import app.revanced.manager.plugin.downloader.App
+import app.revanced.manager.plugin.downloader.DownloadScope
class LoadedDownloaderPlugin(
val packageName: String,
val name: String,
val version: String,
- private val instance: DownloaderPlugin,
+ val createVersionPagingSource: (packageName: String, versionHint: String?) -> PagingSource<*, out App>,
+ val download: suspend DownloadScope.(app: App) -> Unit,
+ val pagingConfig: PagingConfig,
val classLoader: ClassLoader
-) : DownloaderPlugin by instance
\ No newline at end of file
+)
\ 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/ParceledDownloaderApp.kt
index e4e40451b5..222388b3dc 100644
--- a/app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderApp.kt
+++ b/app/src/main/java/app/revanced/manager/network/downloader/ParceledDownloaderApp.kt
@@ -3,38 +3,38 @@ package app.revanced.manager.network.downloader
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
-import app.revanced.manager.plugin.downloader.DownloaderPlugin
+import app.revanced.manager.plugin.downloader.App
import kotlinx.parcelize.Parcelize
@Parcelize
/**
- * A parceled [DownloaderPlugin.App]. Instances of this class can be safely stored in a bundle without needing to set the [ClassLoader].
+ * A parceled [App]. Instances of this class can be safely stored in a bundle without needing to set the [ClassLoader].
*/
class ParceledDownloaderApp private constructor(
val pluginPackageName: String,
private val bundle: Bundle
) : Parcelable {
- constructor(plugin: LoadedDownloaderPlugin, app: DownloaderPlugin.App) : this(
+ constructor(plugin: LoadedDownloaderPlugin, app: App) : this(
plugin.packageName,
createBundle(app)
)
- fun unwrapWith(plugin: LoadedDownloaderPlugin): DownloaderPlugin.App {
+ fun unwrapWith(plugin: LoadedDownloaderPlugin): App {
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 DownloaderPlugin.App
- } else @Suppress("DEPRECATION") bundle.getParcelable(APP_KEY)!!
+ bundle.getParcelable(APP_KEY, clazz)!! as App
+ } else @Suppress("Deprecation") bundle.getParcelable(APP_KEY)!!
}
private companion object {
const val CLASS_NAME_KEY = "class"
const val APP_KEY = "app"
- fun createBundle(app: DownloaderPlugin.App) = Bundle().apply {
+ fun createBundle(app: App) = Bundle().apply {
putParcelable(APP_KEY, app)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) putString(
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 6c33f4676e..ff57433711 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
@@ -63,7 +63,7 @@ class PatcherWorker(
val selectedPatches: PatchSelection,
val options: Options,
val logger: Logger,
- val downloadProgress: MutableStateFlow?>,
+ val downloadProgress: MutableStateFlow?>,
val patchesProgress: MutableStateFlow>,
val setInputFile: (File) -> Unit,
val onProgress: ProgressEventHandler
diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt
index 6840837b7c..1a3b84ed43 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt
@@ -134,7 +134,7 @@ fun SubStep(
name: String,
state: State,
message: String? = null,
- downloadProgress: Pair? = null
+ downloadProgress: Pair? = null
) {
var messageExpanded by rememberSaveable { mutableStateOf(true) }
@@ -180,7 +180,7 @@ fun SubStep(
} else {
downloadProgress?.let { (current, total) ->
Text(
- "$current/$total MB",
+ if (total != null) "$current/$total MB" else "$current MB",
style = MaterialTheme.typography.labelSmall
)
}
@@ -199,7 +199,7 @@ fun SubStep(
}
@Composable
-fun StepIcon(state: State, progress: Pair? = null, size: Dp) {
+fun StepIcon(state: State, progress: Pair? = null, size: Dp) {
val strokeWidth = Dp(floor(size.value / 10) + 1)
when (state) {
@@ -233,7 +233,12 @@ fun StepIcon(state: State, progress: Pair? = null, size: Dp) {
contentDescription = description
}
},
- progress = { progress?.let { (current, total) -> current / total } },
+ progress = {
+ progress?.let { (current, total) ->
+ if (total == null) return@let null
+ current / total
+ }
+ },
strokeWidth = strokeWidth
)
}
diff --git a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt
index 4c7fc417e8..4c2d82c774 100644
--- a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt
+++ b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt
@@ -19,5 +19,5 @@ data class Step(
val category: StepCategory,
val state: State = State.WAITING,
val message: String? = null,
- val downloadProgress: StateFlow?>? = null
+ val downloadProgress: StateFlow?>? = null
)
\ No newline at end of file
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 27049127ca..c4238c96f8 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
@@ -94,7 +94,7 @@ class PatcherViewModel(
}
val patchesProgress = MutableStateFlow(Pair(0, input.selectedPatches.values.sumOf { it.size }))
- private val downloadProgress = MutableStateFlow?>(null)
+ private val downloadProgress = MutableStateFlow?>(null)
val steps = generateSteps(
app,
input.selectedApp,
@@ -304,7 +304,7 @@ class PatcherViewModel(
fun generateSteps(
context: Context,
selectedApp: SelectedApp,
- downloadProgress: StateFlow?>? = null
+ downloadProgress: StateFlow?>? = null
): List {
val needsDownload = selectedApp is SelectedApp.Download
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt
index aaee8c5f27..9d7dcf6880 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt
@@ -12,17 +12,14 @@ import androidx.paging.cachedIn
import androidx.paging.map
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.installer.RootInstaller
-import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.domain.repository.DownloaderPluginRepository
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
-import app.revanced.manager.plugin.downloader.DownloaderPlugin
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.network.downloader.ParceledDownloaderApp
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.PM
-import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.first
@@ -100,12 +97,7 @@ class VersionSelectorViewModel(
Pager(
config = plugin.pagingConfig
) {
- plugin.createPagingSource(
- DownloaderPlugin.SearchParameters(
- packageName,
- suggestedVersion
- )
- )
+ plugin.createVersionPagingSource(packageName, suggestedVersion)
}.flow.map { pagingData ->
pagingData.map {
SelectedApp.Download(
diff --git a/downloader-plugin/api/downloader-plugin.api b/downloader-plugin/api/downloader-plugin.api
index 7f70954adf..20b61ede09 100644
--- a/downloader-plugin/api/downloader-plugin.api
+++ b/downloader-plugin/api/downloader-plugin.api
@@ -1,33 +1,67 @@
-public abstract interface class app/revanced/manager/plugin/downloader/DownloaderPlugin {
- public abstract fun createPagingSource (Lapp/revanced/manager/plugin/downloader/DownloaderPlugin$SearchParameters;)Landroidx/paging/PagingSource;
- public abstract fun download (Lapp/revanced/manager/plugin/downloader/DownloaderPlugin$App;Lapp/revanced/manager/plugin/downloader/DownloaderPlugin$DownloadParameters;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
- public abstract fun getPagingConfig ()Landroidx/paging/PagingConfig;
+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 abstract interface class app/revanced/manager/plugin/downloader/DownloaderPlugin$App : android/os/Parcelable {
- public abstract fun getPackageName ()Ljava/lang/String;
- public abstract fun getVersion ()Ljava/lang/String;
+public abstract interface class app/revanced/manager/plugin/downloader/DownloadScope {
+ public abstract fun getSaveLocation ()Ljava/io/File;
+ public abstract fun reportProgress (ILjava/lang/Integer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
-public final class app/revanced/manager/plugin/downloader/DownloaderPlugin$DownloadParameters {
- public fun (Ljava/io/File;Lkotlin/jvm/functions/Function2;)V
- public final fun getOnDownloadProgress ()Lkotlin/jvm/functions/Function2;
- public final fun getTargetFile ()Ljava/io/File;
+public final class app/revanced/manager/plugin/downloader/Downloader : app/revanced/manager/plugin/downloader/DownloaderMarker {
+ public final fun getDownload ()Lkotlin/jvm/functions/Function3;
+ public final fun getGetVersions ()Lkotlin/jvm/functions/Function3;
}
-public final class app/revanced/manager/plugin/downloader/DownloaderPlugin$Parameters {
+public final class app/revanced/manager/plugin/downloader/DownloaderBuilder {
+ public fun ()V
+ public final fun build ()Lapp/revanced/manager/plugin/downloader/Downloader;
+ public final fun download (Lkotlin/jvm/functions/Function3;)V
+ public final fun getVersions (Lkotlin/jvm/functions/Function3;)V
+}
+
+public final class app/revanced/manager/plugin/downloader/DownloaderContext {
public fun (Landroid/content/Context;Ljava/io/File;)V
- public final fun getContext ()Landroid/content/Context;
+ public final fun getAndroidContext ()Landroid/content/Context;
public final fun getTempDirectory ()Ljava/io/File;
}
-public final class app/revanced/manager/plugin/downloader/DownloaderPlugin$SearchParameters {
- public fun (Ljava/lang/String;Ljava/lang/String;)V
- public final fun getPackageName ()Ljava/lang/String;
- public final fun getVersionHint ()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/Downloader;
+}
+
+public abstract interface class app/revanced/manager/plugin/downloader/DownloaderMarker {
+}
+
+public final class app/revanced/manager/plugin/downloader/PaginatedDownloader : app/revanced/manager/plugin/downloader/DownloaderMarker {
+ public final fun getDownload ()Lkotlin/jvm/functions/Function3;
+ public final fun getPagingConfig ()Landroidx/paging/PagingConfig;
+ public final fun getVersionPager ()Lkotlin/jvm/functions/Function2;
+}
+
+public final class app/revanced/manager/plugin/downloader/PaginatedDownloaderBuilder {
+ public fun ()V
+ public final fun build ()Lapp/revanced/manager/plugin/downloader/PaginatedDownloader;
+ public final fun download (Lkotlin/jvm/functions/Function3;)V
+ public final fun versionPager (Landroidx/paging/PagingConfig;Lkotlin/jvm/functions/Function2;)V
+ public static synthetic fun versionPager$default (Lapp/revanced/manager/plugin/downloader/PaginatedDownloaderBuilder;Landroidx/paging/PagingConfig;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
}
-public final class app/revanced/manager/plugin/downloader/UtilsKt {
- public static final fun singlePagePagingSource (Lkotlin/jvm/functions/Function1;)Landroidx/paging/PagingSource;
+public final class app/revanced/manager/plugin/downloader/PaginatedDownloaderKt {
+ public static final fun paginatedDownloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/PaginatedDownloader;
}
diff --git a/downloader-plugin/build.gradle.kts b/downloader-plugin/build.gradle.kts
index 051baeff4a..f24478473c 100644
--- a/downloader-plugin/build.gradle.kts
+++ b/downloader-plugin/build.gradle.kts
@@ -1,6 +1,7 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
+ id("kotlin-parcelize")
}
android {
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
new file mode 100644
index 0000000000..3cd2265dae
--- /dev/null
+++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/App.kt
@@ -0,0 +1,15 @@
+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/DownloadScope.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloadScope.kt
new file mode 100644
index 0000000000..a5a08a77db
--- /dev/null
+++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloadScope.kt
@@ -0,0 +1,15 @@
+package app.revanced.manager.plugin.downloader
+
+import java.io.File
+
+interface DownloadScope {
+ /**
+ * The location where the downloaded APK should be saved.
+ */
+ val saveLocation: File
+
+ /**
+ * A callback for reporting download progress
+ */
+ suspend fun reportProgress(bytesReceived: Int, bytesTotal: Int?)
+}
\ 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
new file mode 100644
index 0000000000..835498d16d
--- /dev/null
+++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Downloader.kt
@@ -0,0 +1,27 @@
+package app.revanced.manager.plugin.downloader
+
+class Downloader internal constructor(
+ val getVersions: suspend (packageName: String, versionHint: String?) -> List,
+ val download: suspend DownloadScope.(app: A) -> Unit
+) : DownloaderMarker
+
+class DownloaderBuilder {
+ private var getVersions: (suspend (String, String?) -> List)? = null
+ private var download: (suspend DownloadScope.(A) -> Unit)? = null
+
+ fun getVersions(block: suspend (packageName: String, versionHint: String?) -> List) {
+ getVersions = block
+ }
+
+ fun download(block: suspend DownloadScope.(app: A) -> Unit) {
+ download = block
+ }
+
+ fun build() = Downloader(
+ getVersions = getVersions ?: error("getVersions was not declared"),
+ download = download ?: error("download was not declared")
+ )
+}
+
+fun downloader(block: DownloaderBuilder.() -> Unit) =
+ DownloaderBuilder().apply(block).build()
\ No newline at end of file
diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloaderContext.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloaderContext.kt
new file mode 100644
index 0000000000..4615176213
--- /dev/null
+++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloaderContext.kt
@@ -0,0 +1,13 @@
+package app.revanced.manager.plugin.downloader
+
+import android.content.Context
+import java.io.File
+
+@Suppress("Unused", "MemberVisibilityCanBePrivate")
+/**
+ * The downloader plugin context.
+ *
+ * @param androidContext An Android [Context].
+ * @param tempDirectory The temporary directory belonging to this plugin.
+ */
+class DownloaderContext(val androidContext: Context, val tempDirectory: File)
\ No newline at end of file
diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloaderMarker.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloaderMarker.kt
new file mode 100644
index 0000000000..7f384929aa
--- /dev/null
+++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloaderMarker.kt
@@ -0,0 +1,3 @@
+package app.revanced.manager.plugin.downloader
+
+sealed interface DownloaderMarker
\ No newline at end of file
diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloaderPlugin.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloaderPlugin.kt
deleted file mode 100644
index 4981d1b725..0000000000
--- a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloaderPlugin.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-package app.revanced.manager.plugin.downloader
-
-import android.content.Context
-import android.os.Parcelable
-import androidx.paging.PagingConfig
-import androidx.paging.PagingSource
-import java.io.File
-
-@Suppress("Unused")
-/**
- * The main interface for downloader plugins.
- * Implementors must have a public constructor that takes exactly one argument of type [DownloaderPlugin.Parameters].
- */
-interface DownloaderPlugin {
- val pagingConfig: PagingConfig
- fun createPagingSource(parameters: SearchParameters): PagingSource<*, A>
- suspend fun download(app: A, parameters: DownloadParameters)
-
- interface App : Parcelable {
- val packageName: String
- val version: String
- }
-
- /**
- * The plugin constructor parameters.
- *
- * @param context An Android [Context].
- * @param tempDirectory The temporary directory belonging to this [DownloaderPlugin].
- */
- class Parameters(val context: Context, val tempDirectory: File)
-
- /**
- * The application pager parameters.
- *
- * @param packageName The package name to search for.
- * @param versionHint The preferred version to search for. It is not mandatory to respect this parameter.
- */
- class SearchParameters(val packageName: String, val versionHint: String?)
-
- /**
- * The parameters for downloading apps.
- *
- * @param targetFile The location where the downloaded APK should be saved.
- * @param onDownloadProgress A callback for reporting download progress.
- */
- class DownloadParameters(
- val targetFile: File,
- val onDownloadProgress: suspend (progress: Pair?) -> Unit
- )
-}
-
-typealias BytesReceived = Int
-typealias BytesTotal = Int
\ No newline at end of file
diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/PaginatedDownloader.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/PaginatedDownloader.kt
new file mode 100644
index 0000000000..c1fd596287
--- /dev/null
+++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/PaginatedDownloader.kt
@@ -0,0 +1,37 @@
+package app.revanced.manager.plugin.downloader
+
+import androidx.paging.PagingConfig
+import androidx.paging.PagingSource
+
+class PaginatedDownloader internal constructor(
+ val versionPager: (packageName: String, versionHint: String?) -> PagingSource<*, A>,
+ val pagingConfig: PagingConfig,
+ val download: suspend DownloadScope.(app: A) -> Unit
+) : DownloaderMarker
+
+class PaginatedDownloaderBuilder {
+ private var versionPager: ((String, String?) -> PagingSource<*, A>)? = null
+ private var download: (suspend DownloadScope.(A) -> Unit)? = null
+ private var pagingConfig: PagingConfig? = null
+
+ fun versionPager(
+ pagingConfig: PagingConfig = PagingConfig(pageSize = 5),
+ block: (packageName: String, versionHint: String?) -> PagingSource<*, A>
+ ) {
+ versionPager = block
+ this.pagingConfig = pagingConfig
+ }
+
+ fun download(block: suspend DownloadScope.(app: A) -> Unit) {
+ download = block
+ }
+
+ fun build() = PaginatedDownloader(
+ versionPager = versionPager ?: error("versionPager was not declared"),
+ download = download ?: error("download was not declared"),
+ pagingConfig = pagingConfig!!
+ )
+}
+
+fun paginatedDownloader(block: PaginatedDownloaderBuilder.() -> Unit) =
+ PaginatedDownloaderBuilder().apply(block).build()
\ No newline at end of file
diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Utils.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Utils.kt
deleted file mode 100644
index 5f63da0040..0000000000
--- a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Utils.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package app.revanced.manager.plugin.downloader
-
-import androidx.paging.PagingSource
-import androidx.paging.PagingState
-import kotlinx.coroutines.CancellationException
-
-/**
- * Creates a [PagingSource] that loads one page containing the return value of [block].
- */
-fun singlePagePagingSource(block: suspend () -> List): PagingSource =
- object : PagingSource() {
- override fun getRefreshKey(state: PagingState) = null
-
- override suspend fun load(params: LoadParams) = try {
- LoadResult.Page(
- block(),
- nextKey = null,
- prevKey = null
- )
- } catch (e: CancellationException) {
- throw e
- } catch (e: Exception) {
- LoadResult.Error(e)
- }
- }
\ 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 e904cdff03..f0a5559f8c 100644
--- a/example-downloader-plugin/src/main/AndroidManifest.xml
+++ b/example-downloader-plugin/src/main/AndroidManifest.xml
@@ -11,6 +11,6 @@
+ android:value="app.revanced.manager.plugin.downloader.example.ExamplePluginsKt" />
\ No newline at end of file
diff --git a/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/DownloaderPluginImpl.kt b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/DownloaderPluginImpl.kt
deleted file mode 100644
index 9216a086c7..0000000000
--- a/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/DownloaderPluginImpl.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-package app.revanced.manager.plugin.downloader.example
-
-import android.content.pm.PackageManager
-import androidx.paging.PagingConfig
-import app.revanced.manager.plugin.downloader.DownloaderPlugin
-import app.revanced.manager.plugin.downloader.singlePagePagingSource
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import kotlinx.parcelize.Parcelize
-import java.nio.file.Files
-import java.nio.file.StandardCopyOption
-import kotlin.io.path.Path
-
-@Suppress("Unused", "MemberVisibilityCanBePrivate")
-class DownloaderPluginImpl(downloaderPluginParameters: DownloaderPlugin.Parameters) :
- DownloaderPlugin {
- private val pm = downloaderPluginParameters.context.packageManager
-
- private fun getPackageInfo(packageName: String) = try {
- pm.getPackageInfo(packageName, 0)
- } catch (_: PackageManager.NameNotFoundException) {
- null
- }
-
- override val pagingConfig = PagingConfig(pageSize = 1)
-
- override fun createPagingSource(parameters: DownloaderPlugin.SearchParameters) =
- singlePagePagingSource {
- val impl = withContext(Dispatchers.IO) { getPackageInfo(parameters.packageName) }?.let {
- AppImpl(
- parameters.packageName,
- it.versionName,
- it.applicationInfo.sourceDir
- )
- }
-
- listOfNotNull(impl)
- }
-
- override suspend fun download(
- app: AppImpl, parameters: DownloaderPlugin.DownloadParameters
- ) {
- withContext(Dispatchers.IO) {
- Files.copy(
- Path(app.apkPath),
- parameters.targetFile.toPath(),
- StandardCopyOption.REPLACE_EXISTING
- )
- }
- }
-
- @Parcelize
- class AppImpl(
- override val packageName: String,
- override val version: String,
- internal val apkPath: String
- ) : DownloaderPlugin.App
-}
\ No newline at end of file
diff --git a/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugins.kt b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugins.kt
new file mode 100644
index 0000000000..6a9a09dd8e
--- /dev/null
+++ b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugins.kt
@@ -0,0 +1,90 @@
+@file:Suppress("Unused")
+package app.revanced.manager.plugin.downloader.example
+
+import android.content.pm.PackageManager
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+import app.revanced.manager.plugin.downloader.App
+import app.revanced.manager.plugin.downloader.DownloaderContext
+import app.revanced.manager.plugin.downloader.downloader
+import app.revanced.manager.plugin.downloader.paginatedDownloader
+import kotlinx.coroutines.delay
+import kotlinx.parcelize.Parcelize
+import java.nio.file.Files
+import java.nio.file.StandardCopyOption
+import kotlin.io.path.Path
+
+// TODO: document API, change dispatcher.
+
+@Parcelize
+class InstalledApp(
+ override val packageName: String,
+ override val version: String,
+ internal val apkPath: String
+) : App(packageName, version)
+
+private fun installedAppDownloader(context: DownloaderContext) = downloader {
+ val pm = context.androidContext.packageManager
+
+ getVersions { packageName, _ ->
+ val packageInfo = try {
+ pm.getPackageInfo(packageName, 0)
+ } catch (_: PackageManager.NameNotFoundException) {
+ return@getVersions emptyList()
+ }
+
+ listOf(
+ InstalledApp(
+ packageName,
+ packageInfo.versionName,
+ packageInfo.applicationInfo.sourceDir
+ )
+ )
+ }
+
+ download {
+ Files.copy(Path(it.apkPath), saveLocation.toPath(), StandardCopyOption.REPLACE_EXISTING)
+ }
+}
+
+private val Int.megabytes get() = times(1_000_000)
+
+val examplePaginatedDownloader = paginatedDownloader {
+ versionPager { packageName, versionHint ->
+ object : PagingSource() {
+ override fun getRefreshKey(state: PagingState) = state.anchorPosition?.let {
+ state.closestPageToPosition(it)?.prevKey?.plus(1)
+ ?: state.closestPageToPosition(it)?.nextKey?.minus(1)
+ }
+
+ override suspend fun load(params: LoadParams): LoadResult {
+ val page = params.key ?: 0
+ if (page == 0 && versionHint != null) return LoadResult.Page(
+ listOf(
+ App(
+ packageName,
+ versionHint
+ )
+ ),
+ prevKey = null,
+ nextKey = 1
+ )
+
+ return LoadResult.Page(
+ data = List(params.loadSize) { App(packageName, "fake.$page.$it") },
+ prevKey = page.minus(1).takeIf { it >= 0 },
+ nextKey = page.plus(1).takeIf { it < 5 }
+ )
+ }
+ }
+ }
+
+ download {
+ for (i in 0..5) {
+ reportProgress(i.megabytes , 5.megabytes)
+ delay(1000L)
+ }
+
+ throw Exception("Download simulation complete")
+ }
+}
\ No newline at end of file