From f9e8d30ff696d8bc802205768ebe6ba24c01624f Mon Sep 17 00:00:00 2001 From: Ax333l Date: Fri, 19 Jul 2024 14:41:41 +0200 Subject: [PATCH] start implementing the new API --- app/build.gradle.kts | 2 +- .../java/app/revanced/manager/MainActivity.kt | 32 +- .../revanced/manager/di/ViewModelModule.kt | 1 - .../repository/DownloaderPluginRepository.kt | 54 +-- .../downloader/LoadedDownloaderPlugin.kt | 6 +- .../manager/patcher/worker/PatcherWorker.kt | 53 ++- .../manager/ui/destination/Destination.kt | 3 - .../destination/SelectedAppInfoDestination.kt | 3 - .../revanced/manager/ui/model/SelectedApp.kt | 31 +- .../manager/ui/screen/AppSelectorScreen.kt | 16 +- .../manager/ui/screen/PatcherScreen.kt | 34 ++ .../ui/screen/SelectedAppInfoScreen.kt | 11 +- .../ui/screen/VersionSelectorScreen.kt | 337 ------------------ .../manager/ui/viewmodel/PatcherViewModel.kt | 58 ++- .../ui/viewmodel/SelectedAppInfoViewModel.kt | 2 +- .../ui/viewmodel/VersionSelectorViewModel.kt | 142 -------- .../revanced/manager/util/IntentContract.kt | 12 + downloader-plugin/api/downloader-plugin.api | 34 +- .../plugin/downloader/DownloadScope.kt | 15 - .../manager/plugin/downloader/Downloader.kt | 56 ++- .../plugin/downloader/DownloaderMarker.kt | 3 - .../plugin/downloader/PaginatedDownloader.kt | 37 -- example-downloader-plugin/build.gradle.kts | 18 +- .../src/main/AndroidManifest.xml | 6 + .../downloader/example/ExamplePlugins.kt | 77 ++-- .../downloader/example/InteractionActivity.kt | 65 ++++ 26 files changed, 381 insertions(+), 727 deletions(-) delete mode 100644 app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt delete mode 100644 app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt create mode 100644 app/src/main/java/app/revanced/manager/util/IntentContract.kt delete mode 100644 downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloadScope.kt delete mode 100644 downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloaderMarker.kt delete mode 100644 downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/PaginatedDownloader.kt create mode 100644 example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/InteractionActivity.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1315999e..c15102a1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -83,7 +83,7 @@ android { buildFeatures.compose = true buildFeatures.aidl = true - buildFeatures.buildConfig=true + buildFeatures.buildConfig = true android { androidResources { diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt index 095945e0..9f9c26c0 100644 --- a/app/src/main/java/app/revanced/manager/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -9,13 +9,13 @@ import androidx.compose.runtime.getValue import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import app.revanced.manager.ui.destination.Destination import app.revanced.manager.ui.destination.SettingsDestination +import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.screen.AppSelectorScreen import app.revanced.manager.ui.screen.DashboardScreen import app.revanced.manager.ui.screen.InstalledAppInfoScreen import app.revanced.manager.ui.screen.PatcherScreen import app.revanced.manager.ui.screen.SelectedAppInfoScreen import app.revanced.manager.ui.screen.SettingsScreen -import app.revanced.manager.ui.screen.VersionSelectorScreen import app.revanced.manager.ui.theme.ReVancedManagerTheme import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.viewmodel.MainViewModel @@ -76,12 +76,13 @@ class MainActivity : ComponentActivity() { is Destination.InstalledApplicationInfo -> InstalledAppInfoScreen( onPatchClick = { packageName, patchSelection -> + /* navController.navigate( Destination.VersionSelector( packageName, patchSelection ) - ) + )*/ }, onBackClick = { navController.pop() }, viewModel = getComposeViewModel { parametersOf(destination.installedApp) } @@ -93,7 +94,14 @@ class MainActivity : ComponentActivity() { ) is Destination.AppSelector -> AppSelectorScreen( - onAppClick = { navController.navigate(Destination.VersionSelector(it)) }, + // onAppClick = { navController.navigate(Destination.VersionSelector(it)) }, + onAppClick = { packageName, version -> + navController.navigate( + Destination.SelectedApplicationInfo( + SelectedApp.Downloadable(packageName, version.orEmpty()) + ) + ) + }, onStorageClick = { navController.navigate( Destination.SelectedApplicationInfo( @@ -104,24 +112,6 @@ class MainActivity : ComponentActivity() { onBackClick = { navController.pop() } ) - is Destination.VersionSelector -> VersionSelectorScreen( - onBackClick = { navController.pop() }, - onAppClick = { selectedApp -> - navController.navigate( - Destination.SelectedApplicationInfo( - selectedApp, - destination.patchSelection, - ) - ) - }, - viewModel = getComposeViewModel { - parametersOf( - destination.packageName, - destination.patchSelection - ) - } - ) - is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen( onPatchClick = { app, patches, options -> navController.navigate( diff --git a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt index 0c69767c..a59d65a2 100644 --- a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt +++ b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt @@ -12,7 +12,6 @@ val viewModelModule = module { viewModelOf(::SettingsViewModel) viewModelOf(::AdvancedSettingsViewModel) viewModelOf(::AppSelectorViewModel) - viewModelOf(::VersionSelectorViewModel) viewModelOf(::PatcherViewModel) viewModelOf(::UpdateViewModel) viewModelOf(::ChangelogsViewModel) 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 b4f9583f..d1a6c184 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,9 +5,6 @@ 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 @@ -19,8 +16,6 @@ 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 @@ -115,51 +110,14 @@ class DownloaderPluginRepository( .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 components = when (downloader) { - is PaginatedDownloader<*> -> PluginComponents( - downloader.download as suspend DownloadScope.(App) -> Unit, - downloader.pagingConfig, - downloader.versionPager - ) - - 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, - components.versionPager, - components.download, - components.pagingConfig, + downloader.get, + downloader.download as suspend DownloadScope.(App) -> Unit, classLoader ) ) @@ -204,19 +162,19 @@ class DownloaderPluginRepository( val packageFlags = PackageManager.GET_META_DATA or PM.signaturesFlag - val Class<*>.isDownloaderMarker get() = DownloaderMarker::class.java.isAssignableFrom(this) + val Class<*>.isDownloader get() = Downloader::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 } + .filter { it.modifiers.isPublicStatic && it.returnType.isDownloader } .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 + ) as Downloader<*> + if (it.parameterTypes.isEmpty()) return@callMethod it(null) as Downloader<*> return@callMethod null } 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 ab8d945f..76c5677d 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,16 +1,14 @@ package app.revanced.manager.network.downloader -import androidx.paging.PagingConfig -import androidx.paging.PagingSource import app.revanced.manager.plugin.downloader.App import app.revanced.manager.plugin.downloader.DownloadScope +import app.revanced.manager.plugin.downloader.GetScope class LoadedDownloaderPlugin( val packageName: String, val name: String, val version: String, - val createVersionPagingSource: (packageName: String, versionHint: String?) -> PagingSource<*, out App>, + val get: suspend GetScope.(packageName: String, version: String?) -> App?, val download: suspend DownloadScope.(app: App) -> Unit, - val pagingConfig: PagingConfig, val classLoader: ClassLoader ) \ No newline at end of file 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 ff574337..704b5402 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 @@ -26,9 +26,14 @@ import app.revanced.manager.domain.repository.DownloaderPluginRepository import app.revanced.manager.domain.repository.InstalledAppRepository import app.revanced.manager.domain.worker.Worker import app.revanced.manager.domain.worker.WorkerRepository +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.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 @@ -36,6 +41,7 @@ import app.revanced.manager.util.PM import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.tag import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -57,7 +63,7 @@ class PatcherWorker( private val installedAppRepository: InstalledAppRepository by inject() private val rootInstaller: RootInstaller by inject() - data class Args( + class Args( val input: SelectedApp, val output: String, val selectedPatches: PatchSelection, @@ -65,6 +71,7 @@ class PatcherWorker( val logger: Logger, val downloadProgress: MutableStateFlow?>, val patchesProgress: MutableStateFlow>, + val handleUserInteractionRequest: suspend () -> ActivityLaunchPermit?, val setInputFile: (File) -> Unit, val onProgress: ProgressEventHandler ) { @@ -143,18 +150,43 @@ class PatcherWorker( } } + suspend fun download(plugin: LoadedDownloaderPlugin, app: DownloaderApp) = + downloadedAppRepository.download( + plugin, + app, + onDownload = args.downloadProgress::emit + ).also { + args.setInputFile(it) + updateProgress(state = State.COMPLETED) // Download APK + } + val inputFile = when (val selectedApp = args.input) { is SelectedApp.Download -> { val (plugin, app) = downloaderPluginRepository.unwrapParceledApp(selectedApp.app) - downloadedAppRepository.download( - plugin, - app, - onDownload = args.downloadProgress::emit - ).also { - args.setInputFile(it) - updateProgress(state = State.COMPLETED) // Download APK + download(plugin, app) + } + + is SelectedApp.Downloadable -> { + val getScope = object : GetScope { + override suspend fun requestUserInteraction() = + args.handleUserInteractionRequest() + ?: throw UserInteractionException.RequestDenied() } + + downloaderPluginRepository.loadedPluginsFlow.first() + .firstNotNullOfOrNull { plugin -> + try { + plugin.get( + getScope, + selectedApp.packageName, + selectedApp.suggestedVersion + ) + ?.takeIf { selectedApp.suggestedVersion == null || it.version == selectedApp.suggestedVersion } + } catch (_: UserInteractionException) { + null + }?.let { app -> download(plugin, app) } + } ?: throw Exception("App is not available.") } is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) } @@ -188,7 +220,10 @@ class PatcherWorker( Log.i(tag, "Patching succeeded".logFmt()) Result.success() } catch (e: ProcessRuntime.RemoteFailureException) { - Log.e(tag, "An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()) + Log.e( + tag, + "An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt() + ) updateProgress(state = State.FAILED, message = e.originalStackTrace) Result.failure() } catch (e: Exception) { diff --git a/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt b/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt index e15bdfb6..93c59411 100644 --- a/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt +++ b/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt @@ -22,9 +22,6 @@ sealed interface Destination : Parcelable { @Parcelize data class Settings(val startDestination: SettingsDestination = SettingsDestination.Settings) : Destination - @Parcelize - data class VersionSelector(val packageName: String, val patchSelection: PatchSelection? = null) : Destination - @Parcelize data class SelectedApplicationInfo(val selectedApp: SelectedApp, val patchSelection: PatchSelection? = null) : Destination diff --git a/app/src/main/java/app/revanced/manager/ui/destination/SelectedAppInfoDestination.kt b/app/src/main/java/app/revanced/manager/ui/destination/SelectedAppInfoDestination.kt index a1fafa32..9a1f3e29 100644 --- a/app/src/main/java/app/revanced/manager/ui/destination/SelectedAppInfoDestination.kt +++ b/app/src/main/java/app/revanced/manager/ui/destination/SelectedAppInfoDestination.kt @@ -13,7 +13,4 @@ sealed interface SelectedAppInfoDestination : Parcelable { @Parcelize data class PatchesSelector(val app: SelectedApp, val currentSelection: PatchSelection?, val options: @RawValue Options) : SelectedAppInfoDestination - - @Parcelize - data object VersionSelector: SelectedAppInfoDestination } \ No newline at end of file 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 9fa7a82f..c0146cc8 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 @@ -2,19 +2,38 @@ package app.revanced.manager.ui.model import android.os.Parcelable import app.revanced.manager.network.downloader.ParceledDownloaderApp +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import java.io.File -sealed class SelectedApp : Parcelable { - abstract val packageName: String - abstract val version: String +sealed interface SelectedApp : Parcelable { + val packageName: String + val version: String // TODO: make this nullable @Parcelize - data class Download(override val packageName: String, override val version: String, val app: ParceledDownloaderApp) : SelectedApp() + data class Download( + override val packageName: String, + override val version: String, + val app: ParceledDownloaderApp + ) : SelectedApp @Parcelize - data class Local(override val packageName: String, override val version: String, val file: File, val temporary: Boolean) : SelectedApp() + data class Downloadable(override val packageName: String, val suggestedVersion: String?) : SelectedApp { + @IgnoredOnParcel + override val version = suggestedVersion.orEmpty() + } @Parcelize - data class Installed(override val packageName: String, override val version: String) : SelectedApp() + data class Local( + override val packageName: String, + override val version: String, + val file: File, + val temporary: Boolean + ) : SelectedApp + + @Parcelize + data class Installed( + override val packageName: String, + override val version: String + ) : SelectedApp } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt index 6d2e1d5f..aed978a2 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt @@ -38,7 +38,7 @@ import org.koin.androidx.compose.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable fun AppSelectorScreen( - onAppClick: (packageName: String) -> Unit, + onAppClick: (packageName: String, version: String?) -> Unit, onStorageClick: (SelectedApp.Local) -> Unit, onBackClick: () -> Unit, vm: AppSelectorViewModel = koinViewModel() @@ -90,7 +90,12 @@ fun AppSelectorScreen( key = { it.packageName } ) { app -> ListItem( - modifier = Modifier.clickable { onAppClick(app.packageName) }, + modifier = Modifier.clickable { + onAppClick( + app.packageName, + suggestedVersions[app.packageName] + ) + }, leadingContent = { AppIcon( app.packageInfo, @@ -183,7 +188,12 @@ fun AppSelectorScreen( key = { it.packageName } ) { app -> ListItem( - modifier = Modifier.clickable { onAppClick(app.packageName) }, + modifier = Modifier.clickable { + onAppClick( + app.packageName, + suggestedVersions[app.packageName] + ) + }, leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) }, headlineContent = { AppLabel( diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index 163dfbc6..28816545 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.material.icons.automirrored.outlined.OpenInNew import androidx.compose.material.icons.outlined.FileDownload import androidx.compose.material.icons.outlined.PostAdd import androidx.compose.material.icons.outlined.Save +import androidx.compose.material3.AlertDialog import androidx.compose.material3.BottomAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton @@ -24,7 +25,9 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -46,6 +49,7 @@ import app.revanced.manager.ui.model.State import app.revanced.manager.ui.model.StepCategory import app.revanced.manager.ui.viewmodel.PatcherViewModel import app.revanced.manager.util.APK_MIMETYPE +import app.revanced.manager.util.IntentContract @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -91,6 +95,36 @@ fun PatcherScreen( onConfirm = vm::install ) + val activityLauncher = rememberLauncherForActivityResult(contract = IntentContract) { + vm.handleActivityResult(it) + } + SideEffect { + vm.launchActivity = activityLauncher::launch + } + + if (vm.activeInteractionRequest) + AlertDialog( + onDismissRequest = vm::rejectInteraction, + confirmButton = { + TextButton( + onClick = vm::allowInteraction + ) { + Text(stringResource(R.string.continue_)) + } + }, + dismissButton = { + TextButton( + onClick = vm::rejectInteraction + ) { + Text(stringResource(R.string.cancel)) + } + }, + title = { Text("User interaction required.") }, + text = { + Text("User interaction is required to proceed.") + } + ) + AppScaffold( topBar = { AppTopBar( diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt index 0dd786d7..573391a8 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt @@ -108,7 +108,7 @@ fun SelectedAppInfoScreen( ) }, onVersionSelectorClick = { - navController.navigate(SelectedAppInfoDestination.VersionSelector) + // navController.navigate(SelectedAppInfoDestination.VersionSelector) }, onBackClick = onBackClick, availablePatchCount = availablePatchCount, @@ -118,15 +118,6 @@ fun SelectedAppInfoScreen( packageInfo = vm.selectedAppInfo, ) - is SelectedAppInfoDestination.VersionSelector -> VersionSelectorScreen( - onBackClick = navController::pop, - onAppClick = { - vm.selectedApp = it - navController.pop() - }, - viewModel = koinViewModel { parametersOf(packageName) } - ) - is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen( onSave = { patches, options -> vm.updateConfiguration(patches, options, bundles) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt deleted file mode 100644 index 08cc526c..00000000 --- a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt +++ /dev/null @@ -1,337 +0,0 @@ -package app.revanced.manager.ui.screen - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Download -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.paging.LoadState -import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.itemKey -import app.revanced.manager.R -import app.revanced.manager.data.room.apps.installed.InstallType -import app.revanced.manager.network.downloader.LoadedDownloaderPlugin -import app.revanced.manager.ui.component.AppTopBar -import app.revanced.manager.ui.component.GroupHeader -import app.revanced.manager.ui.component.LazyColumnWithScrollbar -import app.revanced.manager.ui.component.LoadingIndicator -import app.revanced.manager.ui.component.NonSuggestedVersionDialog -import app.revanced.manager.ui.model.SelectedApp -import app.revanced.manager.ui.viewmodel.VersionSelectorViewModel -import app.revanced.manager.util.isScrollingUp -import app.revanced.manager.util.simpleMessage - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun VersionSelectorScreen( - onBackClick: () -> Unit, - onAppClick: (SelectedApp) -> Unit, - viewModel: VersionSelectorViewModel -) { - val supportedVersions by viewModel.supportedVersions.collectAsStateWithLifecycle(emptyMap()) - val downloadedVersions by viewModel.downloadedVersions.collectAsStateWithLifecycle(emptyList()) - val downloadableVersions = viewModel.downloadableApps?.collectAsLazyPagingItems() - - val sortedDownloadedVersions by remember { - derivedStateOf { - downloadedVersions - .distinctBy { it.version } - .sortedWith( - compareByDescending { supportedVersions[it.version] }.thenByDescending { it.version } - ) - } - } - - if (viewModel.showNonSuggestedVersionDialog) - NonSuggestedVersionDialog( - suggestedVersion = viewModel.requiredVersion.orEmpty(), - onDismiss = viewModel::dismissNonSuggestedVersionDialog - ) - - var showDownloaderSelectionDialog by rememberSaveable { - mutableStateOf(false) - } - if (showDownloaderSelectionDialog) { - val plugins by viewModel.downloadersFlow.collectAsStateWithLifecycle(emptyList()) - val hasInstalledPlugins by viewModel.hasInstalledPlugins.collectAsStateWithLifecycle(false) - - DownloaderSelectionDialog( - plugins = plugins, - hasInstalledPlugins = hasInstalledPlugins, - onConfirm = { - viewModel.selectDownloaderPlugin(it) - showDownloaderSelectionDialog = false - }, - onDismiss = { showDownloaderSelectionDialog = false } - ) - } - - val lazyListState = rememberLazyListState() - Scaffold( - topBar = { - AppTopBar( - title = stringResource(R.string.select_version), - actions = { - IconButton(onClick = { showDownloaderSelectionDialog = true }) { - Icon(Icons.Filled.Download, stringResource(R.string.downloader_select)) - } - }, - onBackClick = onBackClick, - ) - }, - floatingActionButton = { - ExtendedFloatingActionButton( - text = { Text(stringResource(R.string.select_version)) }, - icon = { Icon(Icons.Default.Check, null) }, - expanded = lazyListState.isScrollingUp, - onClick = { viewModel.selectedVersion?.let(onAppClick) } - ) - } - ) { paddingValues -> - LazyColumnWithScrollbar( - modifier = Modifier - .padding(paddingValues) - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - state = lazyListState - ) { - viewModel.installedApp?.let { (packageInfo, installedApp) -> - SelectedApp.Installed( - packageName = viewModel.packageName, - version = packageInfo.versionName - ).let { - item { - SelectedAppItem( - selectedApp = it, - selected = viewModel.selectedVersion == it, - onClick = { viewModel.select(it) }, - patchCount = supportedVersions[it.version], - enabled = - !(installedApp?.installType == InstallType.ROOT && !viewModel.rootInstaller.hasRootAccess()), - alreadyPatched = installedApp != null && installedApp.installType != InstallType.ROOT - ) - } - } - } - - if (sortedDownloadedVersions.isNotEmpty()) item { - Row(Modifier.fillMaxWidth()) { - GroupHeader(stringResource(R.string.downloaded_versions)) - } - } - - items( - items = sortedDownloadedVersions, - key = { it.version } - ) { - SelectedAppItem( - selectedApp = it, - selected = viewModel.selectedVersion == it, - onClick = { viewModel.select(it) }, - patchCount = supportedVersions[it.version] - ) - } - - item { - Row(Modifier.fillMaxWidth()) { - GroupHeader(stringResource(R.string.downloadable_versions)) - } - } - if (downloadableVersions == null) { - item { - Text(stringResource(R.string.downloader_not_selected)) - } - } else { - (downloadableVersions.loadState.prepend as? LoadState.Error)?.let { errorState -> - item { - errorState.Render() - } - } - - items( - count = downloadableVersions.itemCount, - key = downloadableVersions.itemKey { it.version } - ) { - val item = downloadableVersions[it]!! - - SelectedAppItem( - selectedApp = item, - selected = viewModel.selectedVersion == item, - onClick = { viewModel.select(item) }, - patchCount = supportedVersions[item.version] - ) - } - - val loadStates = arrayOf( - downloadableVersions.loadState.append, - downloadableVersions.loadState.refresh - ) - - if (loadStates.any { it is LoadState.Loading }) { - item { - LoadingIndicator() - } - } else if (downloadableVersions.itemCount == 0) { - item { Text(stringResource(R.string.downloader_no_versions)) } - } - - loadStates.firstNotNullOfOrNull { it as? LoadState.Error }?.let { errorState -> - item { - errorState.Render() - } - } - } - } - } -} - -@Composable -fun SelectedAppItem( - selectedApp: SelectedApp, - selected: Boolean, - onClick: () -> Unit, - patchCount: Int?, - enabled: Boolean = true, - alreadyPatched: Boolean = false, -) { - ListItem( - leadingContent = { RadioButton(selected, null) }, - headlineContent = { Text(selectedApp.version) }, - supportingContent = when (selectedApp) { - is SelectedApp.Installed -> - if (alreadyPatched) { - { Text(stringResource(R.string.already_patched)) } - } else { - { Text(stringResource(R.string.installed)) } - } - - is SelectedApp.Local -> { - { Text(stringResource(R.string.already_downloaded)) } - } - - else -> null - }, - trailingContent = patchCount?.let { - { - Text(pluralStringResource(R.plurals.patch_count, it, it)) - } - }, - modifier = Modifier - .clickable(enabled = !alreadyPatched && enabled, onClick = onClick) - .run { - if (!enabled || alreadyPatched) alpha(0.5f) - else this - } - ) -} - -@Composable -private fun LoadState.Error.Render() { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - val message = - remember(error) { error.simpleMessage().orEmpty() } - Text(stringResource(R.string.error_occurred)) - Text( - text = message, - modifier = Modifier.padding(horizontal = 15.dp) - ) - Text(error.stackTraceToString()) - } -} - -@Composable -private fun DownloaderSelectionDialog( - plugins: List, - hasInstalledPlugins: Boolean, - onConfirm: (LoadedDownloaderPlugin) -> Unit, - onDismiss: () -> Unit -) { - var selectedPackageName: String? by rememberSaveable { - mutableStateOf(null) - } - - AlertDialog( - onDismissRequest = onDismiss, - confirmButton = { - TextButton( - enabled = selectedPackageName != null, - onClick = { onConfirm(plugins.single { it.packageName == selectedPackageName }) } - ) { - Text(stringResource(R.string.select)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel)) - } - }, - title = { - Text(stringResource(R.string.downloader_select)) - }, - icon = { - Icon(Icons.Filled.Download, null) - }, - // TODO: fix dialog header centering issue - // textHorizontalPadding = PaddingValues(horizontal = if (plugins.isNotEmpty()) 0.dp else 24.dp), - text = { - LazyColumn { - items(plugins, key = { it.packageName }) { - ListItem( - modifier = Modifier.clickable { selectedPackageName = it.packageName }, - headlineContent = { Text(it.name) }, - leadingContent = { - RadioButton( - selected = selectedPackageName == it.packageName, - onClick = { selectedPackageName = it.packageName } - ) - } - ) - } - - if (plugins.isEmpty()) { - item { - val resource = - if (hasInstalledPlugins) R.string.downloader_no_plugins_available else R.string.downloader_no_plugins_installed - - Text(stringResource(resource)) - } - } - } - } - ) -} \ 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 c4238c96..2d7837c0 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,5 +1,6 @@ package app.revanced.manager.ui.viewmodel +import android.app.Activity import android.app.Application import android.content.BroadcastReceiver import android.content.Context @@ -9,6 +10,7 @@ import android.content.pm.PackageInstaller import android.net.Uri import android.util.Log import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -29,17 +31,21 @@ 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.UserInteractionException import app.revanced.manager.service.InstallService import app.revanced.manager.ui.destination.Destination import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.State import app.revanced.manager.ui.model.Step import app.revanced.manager.ui.model.StepCategory +import app.revanced.manager.util.IntentContract import app.revanced.manager.util.PM import app.revanced.manager.util.simpleMessage import app.revanced.manager.util.tag import app.revanced.manager.util.toast import app.revanced.manager.util.uiSafe +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -48,7 +54,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.time.withTimeout import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout import org.koin.core.component.KoinComponent import org.koin.core.component.inject import java.io.File @@ -74,6 +79,13 @@ class PatcherViewModel( var isInstalling by mutableStateOf(false) private set + private var currentInteractionRequest: CompletableDeferred? by mutableStateOf( + null + ) + val activeInteractionRequest by derivedStateOf { currentInteractionRequest != null } + private var launchedActivity: CompletableDeferred? = null + var launchActivity: (Intent) -> Unit = {} + private val tempDir = fs.tempDir.resolve("installer").also { it.deleteRecursively() it.mkdirs() @@ -115,6 +127,18 @@ class PatcherViewModel( downloadProgress, patchesProgress, setInputFile = { inputFile = it }, + handleUserInteractionRequest = { + withContext(Dispatchers.Main) { + if (activeInteractionRequest) throw Exception("Another request is already pending.") + try { + val job = CompletableDeferred() + currentInteractionRequest = job + job.await() + } finally { + currentInteractionRequest = null + } + } + }, onProgress = { name, state, message -> viewModelScope.launch { steps[currentStepIndex] = steps[currentStepIndex].run { @@ -214,6 +238,35 @@ class PatcherViewModel( tempDir.deleteRecursively() } + fun rejectInteraction() { + currentInteractionRequest?.complete(null) + currentInteractionRequest = null + } + + fun allowInteraction() { + currentInteractionRequest?.complete(ActivityLaunchPermit { intent -> + withContext(Dispatchers.Main) { + if (launchedActivity != null) throw Exception("An activity has already been launched.") + try { + val job = CompletableDeferred() + launchActivity(intent) + + launchedActivity = job + val result = job.await() + if (result.code != Activity.RESULT_OK) throw UserInteractionException.ActivityCancelled() + result.intent + } finally { + launchedActivity = null + } + } + }) + currentInteractionRequest = null + } + + fun handleActivityResult(result: IntentContract.Result) { + launchedActivity?.complete(result) + } + fun export(uri: Uri?) = viewModelScope.launch { uri?.let { withContext(Dispatchers.IO) { @@ -306,7 +359,8 @@ class PatcherViewModel( selectedApp: SelectedApp, downloadProgress: StateFlow?>? = null ): List { - val needsDownload = selectedApp is SelectedApp.Download + val needsDownload = + selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Downloadable return listOfNotNull( Step( diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt index 1a6653e0..697e0ecb 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt @@ -104,9 +104,9 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { private fun invalidateSelectedAppInfo() = viewModelScope.launch { val info = when (val app = selectedApp) { - is SelectedApp.Download -> null is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) } is SelectedApp.Installed -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.packageName) } + else -> null } selectedAppInfo = info 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 deleted file mode 100644 index 9d7dcf68..00000000 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt +++ /dev/null @@ -1,142 +0,0 @@ -package app.revanced.manager.ui.viewmodel - -import android.content.pm.PackageInfo -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.paging.Pager -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.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.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.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject - -class VersionSelectorViewModel( - val packageName: String -) : ViewModel(), KoinComponent { - private val downloadedAppRepository: DownloadedAppRepository by inject() - private val installedAppRepository: InstalledAppRepository by inject() - private val patchBundleRepository: PatchBundleRepository by inject() - private val downloaderPluginRepository: DownloaderPluginRepository by inject() - private val pm: PM by inject() - val rootInstaller: RootInstaller by inject() - - var installedApp: Pair? by mutableStateOf(null) - private set - var requiredVersion: String? by mutableStateOf(null) - private set - var selectedVersion: SelectedApp? by mutableStateOf(null) - private set - - private var nonSuggestedVersionDialogSubject by mutableStateOf(null) - val showNonSuggestedVersionDialog by derivedStateOf { nonSuggestedVersionDialogSubject != null } - - private var suggestedVersion: String? = null - - init { - viewModelScope.launch { - val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) } - val installedAppDeferred = - async(Dispatchers.IO) { installedAppRepository.get(packageName) } - - installedApp = - packageInfo.await()?.let { - it to installedAppDeferred.await() - } - } - - viewModelScope.launch { - suggestedVersion = patchBundleRepository.suggestedVersions.first()[packageName] - } - } - - val supportedVersions = patchBundleRepository.bundles.map supportedVersions@{ bundles -> - var patchesWithoutVersions = 0 - - bundles.flatMap { (_, bundle) -> - bundle.patches.flatMap { patch -> - patch.compatiblePackages.orEmpty() - .filter { it.packageName == packageName } - .onEach { if (it.versions == null) patchesWithoutVersions++ } - .flatMap { it.versions.orEmpty() } - } - }.groupingBy { it } - .eachCount() - .toMutableMap() - .apply { - replaceAll { _, count -> - count + patchesWithoutVersions - } - } - }.flowOn(Dispatchers.Default) - - val hasInstalledPlugins = downloaderPluginRepository.pluginStates.map { it.isNotEmpty() } - val downloadersFlow = downloaderPluginRepository.loadedPluginsFlow - - private var downloaderPlugin: LoadedDownloaderPlugin? by mutableStateOf(null) - val downloadableApps by derivedStateOf { - downloaderPlugin?.let { plugin -> - Pager( - config = plugin.pagingConfig - ) { - plugin.createVersionPagingSource(packageName, suggestedVersion) - }.flow.map { pagingData -> - pagingData.map { - SelectedApp.Download( - it.packageName, - it.version, - ParceledDownloaderApp(plugin, it) - ) - } - } - }?.flowOn(Dispatchers.Default)?.cachedIn(viewModelScope) - } - - val downloadedVersions = downloadedAppRepository.getAll().map { downloadedApps -> - downloadedApps - .filter { it.packageName == packageName } - .map { - SelectedApp.Local( - it.packageName, - it.version, - downloadedAppRepository.getApkFileForApp(it), - false - ) - } - } - - fun selectDownloaderPlugin(plugin: LoadedDownloaderPlugin) { - downloaderPlugin = plugin - } - - fun dismissNonSuggestedVersionDialog() { - nonSuggestedVersionDialogSubject = null - } - - fun select(app: SelectedApp) { - if (requiredVersion != null && app.version != requiredVersion) { - nonSuggestedVersionDialogSubject = app - return - } - - selectedVersion = app - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/IntentContract.kt b/app/src/main/java/app/revanced/manager/util/IntentContract.kt new file mode 100644 index 00000000..5716e051 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/util/IntentContract.kt @@ -0,0 +1,12 @@ +package app.revanced.manager.util + +import android.content.Context +import android.content.Intent +import androidx.activity.result.contract.ActivityResultContract + +object IntentContract : ActivityResultContract() { + override fun createIntent(context: Context, input: Intent) = input + override fun parseResult(resultCode: Int, intent: Intent?) = Result(resultCode, intent) + + class Result(val code: Int, val intent: Intent?) +} \ No newline at end of file diff --git a/downloader-plugin/api/downloader-plugin.api b/downloader-plugin/api/downloader-plugin.api index 20b61ede..4dbd05ba 100644 --- a/downloader-plugin/api/downloader-plugin.api +++ b/downloader-plugin/api/downloader-plugin.api @@ -1,3 +1,7 @@ +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 @@ -18,20 +22,20 @@ public final class app/revanced/manager/plugin/downloader/App$Creator : android/ } public abstract interface class app/revanced/manager/plugin/downloader/DownloadScope { - public abstract fun getSaveLocation ()Ljava/io/File; + public abstract fun getTargetFile ()Ljava/io/File; public abstract fun reportProgress (ILjava/lang/Integer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public final class app/revanced/manager/plugin/downloader/Downloader : app/revanced/manager/plugin/downloader/DownloaderMarker { +public final class app/revanced/manager/plugin/downloader/Downloader { public final fun getDownload ()Lkotlin/jvm/functions/Function3; - public final fun getGetVersions ()Lkotlin/jvm/functions/Function3; + public final fun getGet ()Lkotlin/jvm/functions/Function4; } 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 fun get (Lkotlin/jvm/functions/Function4;)V } public final class app/revanced/manager/plugin/downloader/DownloaderContext { @@ -40,28 +44,26 @@ public final class app/revanced/manager/plugin/downloader/DownloaderContext { public final fun getTempDirectory ()Ljava/io/File; } +public abstract interface annotation class app/revanced/manager/plugin/downloader/DownloaderDsl : java/lang/annotation/Annotation { +} + 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 abstract interface class app/revanced/manager/plugin/downloader/GetScope { + public abstract fun requestUserInteraction (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -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 abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception { + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V } -public final class app/revanced/manager/plugin/downloader/PaginatedDownloaderBuilder { +public final class app/revanced/manager/plugin/downloader/UserInteractionException$ActivityCancelled : app/revanced/manager/plugin/downloader/UserInteractionException { 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/PaginatedDownloaderKt { - public static final fun paginatedDownloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/PaginatedDownloader; +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/DownloadScope.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloadScope.kt deleted file mode 100644 index 05f139da..00000000 --- a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloadScope.kt +++ /dev/null @@ -1,15 +0,0 @@ -package app.revanced.manager.plugin.downloader - -import java.io.File - -interface DownloadScope { - /** - * The location where the downloaded APK should be saved. - */ - val targetFile: 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 index 835498d1..666bcaa5 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,16 +1,41 @@ 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 +import android.content.Intent +import java.io.File +@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) +@DslMarker +annotation class DownloaderDsl + +@DownloaderDsl +interface GetScope { + suspend fun requestUserInteraction(): ActivityLaunchPermit +} + +fun interface ActivityLaunchPermit { + suspend fun launch(intent: Intent): Intent? +} + +@DownloaderDsl +interface DownloadScope { + /** + * The location where the downloaded APK should be saved. + */ + val targetFile: File + + /** + * A callback for reporting download progress + */ + suspend fun reportProgress(bytesReceived: Int, bytesTotal: Int?) +} + +@DownloaderDsl class DownloaderBuilder { - private var getVersions: (suspend (String, String?) -> List)? = null private var download: (suspend DownloadScope.(A) -> Unit)? = null + private var get: (suspend GetScope.(String, String?) -> A?)? = null - fun getVersions(block: suspend (packageName: String, versionHint: String?) -> List) { - getVersions = block + fun get(block: suspend GetScope.(packageName: String, version: String?) -> A?) { + get = block } fun download(block: suspend DownloadScope.(app: A) -> Unit) { @@ -18,10 +43,21 @@ class DownloaderBuilder { } fun build() = Downloader( - getVersions = getVersions ?: error("getVersions was not declared"), - download = download ?: error("download was not declared") + download = download ?: error("download was not declared"), + get = get ?: error("get was not declared") ) } +class Downloader internal constructor( + val get: suspend GetScope.(packageName: String, version: String?) -> A?, + val download: suspend DownloadScope.(app: A) -> Unit +) + fun downloader(block: DownloaderBuilder.() -> Unit) = - DownloaderBuilder().apply(block).build() \ No newline at end of file + DownloaderBuilder().apply(block).build() + +sealed class UserInteractionException(message: String) : Exception(message) { + class RequestDenied : UserInteractionException("Request was denied") + // TODO: can cancelled activities return an intent? + class ActivityCancelled : UserInteractionException("Interaction was cancelled") +} \ 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 deleted file mode 100644 index 7f384929..00000000 --- a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloaderMarker.kt +++ /dev/null @@ -1,3 +0,0 @@ -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/PaginatedDownloader.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/PaginatedDownloader.kt deleted file mode 100644 index c1fd5962..00000000 --- a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/PaginatedDownloader.kt +++ /dev/null @@ -1,37 +0,0 @@ -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/example-downloader-plugin/build.gradle.kts b/example-downloader-plugin/build.gradle.kts index 4130f96a..130df4f5 100644 --- a/example-downloader-plugin/build.gradle.kts +++ b/example-downloader-plugin/build.gradle.kts @@ -5,15 +5,18 @@ plugins { } android { - namespace = "app.revanced.manager.plugin.downloader.example" + val packageName = "app.revanced.manager.plugin.downloader.example" + + namespace = packageName compileSdk = 34 defaultConfig { - applicationId = "app.revanced.manager.plugin.downloader.example" + applicationId = packageName minSdk = 26 targetSdk = 34 versionCode = 1 versionName = "1.0" + buildConfigField("String", "PLUGIN_PACKAGE_NAME", "\"$packageName\"") } buildTypes { @@ -36,8 +39,19 @@ android { kotlinOptions { jvmTarget = "17" } + composeOptions.kotlinCompilerExtensionVersion = "1.5.10" + buildFeatures { + compose = true + buildConfig = true + } } dependencies { + implementation(libs.compose.activity) + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui) + implementation(libs.compose.ui.tooling) + implementation(libs.compose.material3) + compileOnly(project(":downloader-plugin")) } \ 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 f0a5559f..9d4007a6 100644 --- a/example-downloader-plugin/src/main/AndroidManifest.xml +++ b/example-downloader-plugin/src/main/AndroidManifest.xml @@ -9,8 +9,14 @@ android:label="@string/app_name" tools:targetApi="34"> + + + \ 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 index 4be09a68..d706b60d 100644 --- 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 @@ -1,13 +1,13 @@ @file:Suppress("Unused") + package app.revanced.manager.plugin.downloader.example +import android.content.Intent 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 app.revanced.manager.plugin.downloader.example.BuildConfig.PLUGIN_PACKAGE_NAME import kotlinx.coroutines.delay import kotlinx.parcelize.Parcelize import java.nio.file.Files @@ -23,68 +23,39 @@ class InstalledApp( internal val apkPath: String ) : App(packageName, version) -private fun installedAppDownloader(context: DownloaderContext) = downloader { +fun installedAppDownloader(context: DownloaderContext) = downloader { val pm = context.androidContext.packageManager - getVersions { packageName, _ -> + get { packageName, version -> val packageInfo = try { pm.getPackageInfo(packageName, 0) } catch (_: PackageManager.NameNotFoundException) { - return@getVersions emptyList() + return@get null } - listOf( - InstalledApp( - packageName, - packageInfo.versionName, - packageInfo.applicationInfo.sourceDir + requestUserInteraction().launch(Intent().apply { + setClassName( + PLUGIN_PACKAGE_NAME, + InteractionActivity::class.java.canonicalName!! ) - ) + }) + + InstalledApp( + packageName, + packageInfo.versionName, + packageInfo.applicationInfo.sourceDir + ).takeIf { version == null || it.version == version } } download { + // Simulate download progress + for (i in 0..5) { + reportProgress(i.megaBytes, 5.megaBytes) + delay(1000L) + } + Files.copy(Path(it.apkPath), targetFile.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 +private val Int.megaBytes get() = times(1_000_000) \ No newline at end of file diff --git a/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/InteractionActivity.kt b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/InteractionActivity.kt new file mode 100644 index 00000000..0390f3bd --- /dev/null +++ b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/InteractionActivity.kt @@ -0,0 +1,65 @@ +package app.revanced.manager.plugin.downloader.example + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.Modifier + +class InteractionActivity : ComponentActivity() { + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + val isDarkTheme = isSystemInDarkTheme() + + val colorScheme = if (isDarkTheme) darkColorScheme() else lightColorScheme() + + MaterialTheme(colorScheme) { + Scaffold( + topBar = { + TopAppBar( + title = { Text("User interaction example") } + ) + } + ) { paddingValues -> + Column(modifier = Modifier.padding(paddingValues)) { + Text("This is an example interaction.") + Row { + TextButton( + onClick = { + setResult(RESULT_CANCELED) + finish() + } + ) { + Text("Cancel") + } + + TextButton( + onClick = { + setResult(RESULT_OK) + finish() + } + ) { + Text("Continue") + } + } + } + } + } + + } + } +} \ No newline at end of file