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