mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-05-01 14:34:24 +02:00
start implementing the new API
This commit is contained in:
parent
7ec3be460b
commit
f9e8d30ff6
@ -83,7 +83,7 @@ android {
|
||||
|
||||
buildFeatures.compose = true
|
||||
buildFeatures.aidl = true
|
||||
buildFeatures.buildConfig=true
|
||||
buildFeatures.buildConfig = true
|
||||
|
||||
android {
|
||||
androidResources {
|
||||
|
@ -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(
|
||||
|
@ -12,7 +12,6 @@ val viewModelModule = module {
|
||||
viewModelOf(::SettingsViewModel)
|
||||
viewModelOf(::AdvancedSettingsViewModel)
|
||||
viewModelOf(::AppSelectorViewModel)
|
||||
viewModelOf(::VersionSelectorViewModel)
|
||||
viewModelOf(::PatcherViewModel)
|
||||
viewModelOf(::UpdateViewModel)
|
||||
viewModelOf(::ChangelogsViewModel)
|
||||
|
@ -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<Nothing, App>() {
|
||||
override fun getRefreshKey(state: PagingState<Nothing, App>) = null
|
||||
|
||||
override suspend fun load(params: LoadParams<Nothing>) = 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
|
||||
}
|
||||
|
@ -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
|
||||
)
|
@ -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<Pair<Float, Float?>?>,
|
||||
val patchesProgress: MutableStateFlow<Pair<Int, Int>>,
|
||||
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) {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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<SelectedApp> { 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<LoadedDownloaderPlugin>,
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@ -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<ActivityLaunchPermit?>? by mutableStateOf(
|
||||
null
|
||||
)
|
||||
val activeInteractionRequest by derivedStateOf { currentInteractionRequest != null }
|
||||
private var launchedActivity: CompletableDeferred<IntentContract.Result>? = 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<ActivityLaunchPermit?>()
|
||||
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<IntentContract.Result>()
|
||||
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<Pair<Float, Float?>?>? = null
|
||||
): List<Step> {
|
||||
val needsDownload = selectedApp is SelectedApp.Download
|
||||
val needsDownload =
|
||||
selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Downloadable
|
||||
|
||||
return listOfNotNull(
|
||||
Step(
|
||||
|
@ -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
|
||||
|
@ -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<PackageInfo, InstalledApp?>? 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<SelectedApp?>(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
|
||||
}
|
||||
}
|
@ -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<Intent, IntentContract.Result>() {
|
||||
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?)
|
||||
}
|
@ -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 <init> (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 <init> ()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 <init> (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 <init> ()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 <init> ()V
|
||||
}
|
||||
|
||||
|
@ -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?)
|
||||
}
|
@ -1,16 +1,41 @@
|
||||
package app.revanced.manager.plugin.downloader
|
||||
|
||||
class Downloader<A : App> internal constructor(
|
||||
val getVersions: suspend (packageName: String, versionHint: String?) -> List<A>,
|
||||
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<A : App> {
|
||||
private var getVersions: (suspend (String, String?) -> List<A>)? = 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<A>) {
|
||||
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<A : App> {
|
||||
}
|
||||
|
||||
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<A : App> internal constructor(
|
||||
val get: suspend GetScope.(packageName: String, version: String?) -> A?,
|
||||
val download: suspend DownloadScope.(app: A) -> Unit
|
||||
)
|
||||
|
||||
fun <A : App> downloader(block: DownloaderBuilder<A>.() -> Unit) =
|
||||
DownloaderBuilder<A>().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")
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
package app.revanced.manager.plugin.downloader
|
||||
|
||||
sealed interface DownloaderMarker
|
@ -1,37 +0,0 @@
|
||||
package app.revanced.manager.plugin.downloader
|
||||
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingSource
|
||||
|
||||
class PaginatedDownloader<A : App> internal constructor(
|
||||
val versionPager: (packageName: String, versionHint: String?) -> PagingSource<*, A>,
|
||||
val pagingConfig: PagingConfig,
|
||||
val download: suspend DownloadScope.(app: A) -> Unit
|
||||
) : DownloaderMarker
|
||||
|
||||
class PaginatedDownloaderBuilder<A : App> {
|
||||
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 <A : App> paginatedDownloader(block: PaginatedDownloaderBuilder<A>.() -> Unit) =
|
||||
PaginatedDownloaderBuilder<A>().apply(block).build()
|
@ -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"))
|
||||
}
|
@ -9,8 +9,14 @@
|
||||
android:label="@string/app_name"
|
||||
tools:targetApi="34">
|
||||
|
||||
<activity
|
||||
android:name=".InteractionActivity"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.DeviceDefault" />
|
||||
|
||||
<meta-data
|
||||
android:name="app.revanced.manager.plugin.downloader.class"
|
||||
android:value="app.revanced.manager.plugin.downloader.example.ExamplePluginsKt" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -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<InstalledApp> {
|
||||
fun installedAppDownloader(context: DownloaderContext) = downloader<InstalledApp> {
|
||||
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<Int, App>() {
|
||||
override fun getRefreshKey(state: PagingState<Int, App>) = state.anchorPosition?.let {
|
||||
state.closestPageToPosition(it)?.prevKey?.plus(1)
|
||||
?: state.closestPageToPosition(it)?.nextKey?.minus(1)
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, App> {
|
||||
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")
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user