From aec8cec9b8eceeb6d882898228e84b982846f82b Mon Sep 17 00:00:00 2001 From: Robert <72943079+CnC-Robert@users.noreply.github.com> Date: Sun, 30 Jul 2023 10:29:22 +0000 Subject: [PATCH] feat: download apps in patcher screen (#73) --- app/build.gradle.kts | 1 + .../java/app/revanced/manager/MainActivity.kt | 18 +- .../revanced/manager/di/ViewModelModule.kt | 2 +- .../manager/network/downloader/APKMirror.kt | 259 +++++++++--------- .../network/downloader/AppDownloader.kt | 29 +- .../manager/patcher/patch/PatchInfo.kt | 4 +- .../patcher/worker/PatcherProgressManager.kt | 30 +- .../manager/patcher/worker/PatcherWorker.kt | 52 +++- .../revanced/manager/ui/component/AppIcon.kt | 26 +- .../revanced/manager/ui/component/AppLabel.kt | 52 ++++ .../manager/ui/destination/Destination.kt | 8 +- .../revanced/manager/ui/model/SelectedApp.kt | 20 ++ .../manager/ui/screen/AppSelectorScreen.kt | 87 +----- .../manager/ui/screen/InstallerScreen.kt | 54 +++- .../ui/screen/PatchesSelectorScreen.kt | 5 +- ...aderScreen.kt => VersionSelectorScreen.kt} | 48 ++-- .../ui/viewmodel/AppDownloaderViewModel.kt | 144 ---------- .../ui/viewmodel/AppSelectorViewModel.kt | 15 +- .../ui/viewmodel/InstallerViewModel.kt | 11 +- .../manager/ui/viewmodel/MainViewModel.kt | 11 +- .../ui/viewmodel/PatchesSelectorViewModel.kt | 20 +- .../ui/viewmodel/VersionSelectorViewModel.kt | 96 +++++++ .../main/java/app/revanced/manager/util/PM.kt | 123 ++++----- .../java/app/revanced/manager/util/Util.kt | 12 - app/src/main/res/values/strings.xml | 4 +- 25 files changed, 569 insertions(+), 562 deletions(-) create mode 100644 app/src/main/java/app/revanced/manager/ui/component/AppLabel.kt create mode 100644 app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt rename app/src/main/java/app/revanced/manager/ui/screen/{AppDownloaderScreen.kt => VersionSelectorScreen.kt} (75%) delete mode 100644 app/src/main/java/app/revanced/manager/ui/viewmodel/AppDownloaderViewModel.kt create mode 100644 app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6de93405..4328aa93 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -96,6 +96,7 @@ dependencies { // Accompanist implementation(libs.accompanist.drawablepainter) implementation(libs.accompanist.webview) + implementation(libs.accompanist.placeholder) // HTML Scraper implementation(libs.skrapeit.dsl) diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt index 33065e02..439be037 100644 --- a/app/src/main/java/app/revanced/manager/MainActivity.kt +++ b/app/src/main/java/app/revanced/manager/MainActivity.kt @@ -9,7 +9,7 @@ import androidx.compose.runtime.getValue import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.ui.destination.Destination -import app.revanced.manager.ui.screen.AppDownloaderScreen +import app.revanced.manager.ui.screen.VersionSelectorScreen import app.revanced.manager.ui.screen.AppSelectorScreen import app.revanced.manager.ui.screen.DashboardScreen import app.revanced.manager.ui.screen.InstallerScreen @@ -83,29 +83,29 @@ class MainActivity : ComponentActivity() { ) is Destination.AppSelector -> AppSelectorScreen( - onAppClick = { navController.navigate(Destination.PatchesSelector(it)) }, - onDownloaderClick = { navController.navigate(Destination.AppDownloader(it)) }, + onAppClick = { navController.navigate(Destination.VersionSelector(it)) }, + onStorageClick = { navController.navigate(Destination.PatchesSelector(it)) }, onBackClick = { navController.pop() } ) - is Destination.AppDownloader -> AppDownloaderScreen( + is Destination.VersionSelector -> VersionSelectorScreen( onBackClick = { navController.pop() }, - onApkClick = { navController.navigate(Destination.PatchesSelector(it)) }, - viewModel = getViewModel { parametersOf(destination.app) } + onAppClick = { navController.navigate(Destination.PatchesSelector(it)) }, + viewModel = getViewModel { parametersOf(destination.packageName) } ) is Destination.PatchesSelector -> PatchesSelectorScreen( - onBackClick = { navController.popUpTo { it is Destination.AppSelector } }, + onBackClick = { navController.pop() }, onPatchClick = { patches, options -> navController.navigate( Destination.Installer( - destination.input, + destination.selectedApp, patches, options ) ) }, - vm = getViewModel { parametersOf(destination.input) } + vm = getViewModel { parametersOf(destination.selectedApp) } ) is Destination.Installer -> InstallerScreen( 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 870c1571..d33c3541 100644 --- a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt +++ b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt @@ -9,7 +9,7 @@ val viewModelModule = module { viewModelOf(::PatchesSelectorViewModel) viewModelOf(::SettingsViewModel) viewModelOf(::AppSelectorViewModel) - viewModelOf(::AppDownloaderViewModel) + viewModelOf(::VersionSelectorViewModel) viewModelOf(::SourcesViewModel) viewModelOf(::InstallerViewModel) viewModelOf(::UpdateProgressViewModel) diff --git a/app/src/main/java/app/revanced/manager/network/downloader/APKMirror.kt b/app/src/main/java/app/revanced/manager/network/downloader/APKMirror.kt index 55682fb9..3a055ef5 100644 --- a/app/src/main/java/app/revanced/manager/network/downloader/APKMirror.kt +++ b/app/src/main/java/app/revanced/manager/network/downloader/APKMirror.kt @@ -12,11 +12,12 @@ import it.skrape.selects.html5.h5 import it.skrape.selects.html5.input import it.skrape.selects.html5.p import it.skrape.selects.html5.span -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flow +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize import org.koin.core.component.KoinComponent import org.koin.core.component.get +import org.koin.core.component.inject import java.io.File class APKMirror : AppDownloader, KoinComponent { @@ -33,11 +34,6 @@ class APKMirror : AppDownloader, KoinComponent { val link: String ) - private val _downloadProgress: MutableStateFlow?> = MutableStateFlow(null) - override val downloadProgress = _downloadProgress.asStateFlow() - - private val versionMap = HashMap() - private suspend fun getAppLink(packageName: String): String { val searchResults = httpClient.getHtml { url("$apkMirror/?post_type=app_release&searchtype=app&s=$packageName") } .div { @@ -92,7 +88,7 @@ class APKMirror : AppDownloader, KoinComponent { } ?: throw Exception("App isn't available for download") } - override fun getAvailableVersions(packageName: String, versionFilter: Set) = flow { + override fun getAvailableVersions(packageName: String, versionFilter: Set) = flow { // Vanced music uses the same package name so we have to hardcode... val appCategory = if (packageName == "com.google.android.apps.youtube.music") @@ -102,9 +98,11 @@ class APKMirror : AppDownloader, KoinComponent { var page = 1 + val versions = mutableListOf() + while ( if (versionFilter.isNotEmpty()) - versionMap.filterKeys { it in versionFilter }.size < versionFilter.size && page <= 7 + versions.size < versionFilter.size && page <= 7 else page <= 1 ) { @@ -119,33 +117,37 @@ class APKMirror : AppDownloader, KoinComponent { findFirst { children.mapNotNull { element -> if (element.className.isEmpty()) { - val version = element.div { - withClass = "infoSlide" - findFirst { - p { - findFirst { - span { - withClass = "infoSlide-value" - findFirst { - text + + APKMirrorApp( + packageName = packageName, + version = element.div { + withClass = "infoSlide" + findFirst { + p { + findFirst { + span { + withClass = "infoSlide-value" + findFirst { + text + } } } } } - } - } - - val link = element.findFirst { - a { - withClass = "downloadLink" - findFirst { - attribute("href") + }.also { + if (it in versionFilter) + versions.add(it) + }, + downloadLink = element.findFirst { + a { + withClass = "downloadLink" + findFirst { + attribute("href") + } } } - } + ) - versionMap[version] = link - version } else null } } @@ -157,116 +159,125 @@ class APKMirror : AppDownloader, KoinComponent { } } - override suspend fun downloadApp( - version: String, - saveDirectory: File, - preferSplit: Boolean - ): File { - val variants = httpClient.getHtml { url(apkMirror + versionMap[version]) } - .div { - withClass = "variants-table" - findFirst { // list of variants - children.drop(1).map { - Variant( - apkType = it.div { - findFirst { - span { - findFirst { - enumValueOf(text) - } - } - } - }, - arch = it.div { - findSecond { - text - } - }, - link = it.div { - findFirst { - a { - findFirst { - attribute("href") + @Parcelize + private class APKMirrorApp( + override val packageName: String, + override val version: String, + private val downloadLink: String, + ) : AppDownloader.App, KoinComponent { + @IgnoredOnParcel private val httpClient: HttpService by inject() + + override suspend fun download( + saveDirectory: File, + preferSplit: Boolean, + onDownload: suspend (downloadProgress: Pair?) -> Unit + ): File { + val variants = httpClient.getHtml { url(apkMirror + downloadLink) } + .div { + withClass = "variants-table" + findFirst { // list of variants + children.drop(1).map { + Variant( + apkType = it.div { + findFirst { + span { + findFirst { + enumValueOf(text) + } + } + } + }, + arch = it.div { + findSecond { + text + } + }, + link = it.div { + findFirst { + a { + findFirst { + attribute("href") + } } } } + ) + } + } + } + + val orderedAPKTypes = mutableListOf(APKType.APK, APKType.BUNDLE) + .also { if (preferSplit) it.reverse() } + + val variant = orderedAPKTypes.firstNotNullOfOrNull { apkType -> + supportedArches.firstNotNullOfOrNull { arch -> + variants.find { it.arch == arch && it.apkType == apkType } + } + } ?: throw Exception("No compatible variant found") + + if (variant.apkType == APKType.BUNDLE) throw Exception("Split apks are not supported yet") // TODO + + val downloadPage = httpClient.getHtml { url(apkMirror + variant.link) } + .a { + withClass = "downloadButton" + findFirst { + attribute("href") + } + } + + val downloadLink = httpClient.getHtml { url(apkMirror + downloadPage) } + .form { + withId = "filedownload" + findFirst { + val apkLink = attribute("action") + val id = input { + withAttribute = "name" to "id" + findFirst { + attribute("value") } - ) - } - } - } - - val orderedAPKTypes = mutableListOf(APKType.APK, APKType.BUNDLE) - .also { if (preferSplit) it.reverse() } - - val variant = orderedAPKTypes.firstNotNullOfOrNull { apkType -> - supportedArches.firstNotNullOfOrNull { arch -> - variants.find { it.arch == arch && it.apkType == apkType } - } - } ?: throw Exception("No compatible variant found") - - if (variant.apkType == APKType.BUNDLE) TODO("\nSplit apks are not supported yet") - - val downloadPage = httpClient.getHtml { url(apkMirror + variant.link) } - .a { - withClass = "downloadButton" - findFirst { - attribute("href") - } - } - - val downloadLink = httpClient.getHtml { url(apkMirror + downloadPage) } - .form { - withId = "filedownload" - findFirst { - val apkLink = attribute("action") - val id = input { - withAttribute = "name" to "id" - findFirst { - attribute("value") } - } - val key = input { - withAttribute = "name" to "key" - findFirst { - attribute("value") + val key = input { + withAttribute = "name" to "key" + findFirst { + attribute("value") + } } + "$apkLink?id=$id&key=$key" } - "$apkLink?id=$id&key=$key" } - } - val saveLocation = if (variant.apkType == APKType.BUNDLE) - saveDirectory.resolve(version).also { it.mkdirs() } - else - saveDirectory.resolve("$version.apk") - - try { - val downloadLocation = if (variant.apkType == APKType.BUNDLE) - saveLocation.resolve("temp.zip") + val saveLocation = if (variant.apkType == APKType.BUNDLE) + saveDirectory.resolve(version).also { it.mkdirs() } else - saveLocation + saveDirectory.resolve("$version.apk") - httpClient.download(downloadLocation) { - url(apkMirror + downloadLink) - onDownload { bytesSentTotal, contentLength -> - _downloadProgress.emit(bytesSentTotal.div(100000).toFloat().div(10) to contentLength.div(100000).toFloat().div(10)) + try { + val downloadLocation = if (variant.apkType == APKType.BUNDLE) + saveLocation.resolve("temp.zip") + else + saveLocation + + httpClient.download(downloadLocation) { + url(apkMirror + downloadLink) + onDownload { bytesSentTotal, contentLength -> + onDownload(bytesSentTotal.div(100000).toFloat().div(10) to contentLength.div(100000).toFloat().div(10)) + } } + + if (variant.apkType == APKType.BUNDLE) { + // TODO: Extract temp.zip + + downloadLocation.delete() + } + } catch (e: Exception) { + saveLocation.deleteRecursively() + throw e + } finally { + onDownload(null) } - if (variant.apkType == APKType.BUNDLE) { - // TODO: Extract temp.zip - - downloadLocation.delete() - } - } catch (e: Exception) { - saveLocation.deleteRecursively() - throw e - } finally { - _downloadProgress.emit(null) + return saveLocation } - - return saveLocation } companion object { diff --git a/app/src/main/java/app/revanced/manager/network/downloader/AppDownloader.kt b/app/src/main/java/app/revanced/manager/network/downloader/AppDownloader.kt index fd657d16..a6a17622 100644 --- a/app/src/main/java/app/revanced/manager/network/downloader/AppDownloader.kt +++ b/app/src/main/java/app/revanced/manager/network/downloader/AppDownloader.kt @@ -1,11 +1,10 @@ package app.revanced.manager.network.downloader +import android.os.Parcelable import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow import java.io.File interface AppDownloader { - val downloadProgress: StateFlow?> /** * Returns all downloadable apps. @@ -13,19 +12,17 @@ interface AppDownloader { * @param packageName The package name of the app. * @param versionFilter A set of versions to filter. */ - fun getAvailableVersions(packageName: String, versionFilter: Set): Flow + fun getAvailableVersions(packageName: String, versionFilter: Set): Flow + + interface App : Parcelable { + val packageName: String + val version: String + + suspend fun download( + saveDirectory: File, + preferSplit: Boolean, + onDownload: suspend (downloadProgress: Pair?) -> Unit = {} + ): File + } - /** - * Downloads the specific app version. - * - * @param version The version to download. - * @param saveDirectory The folder where the downloaded app should be stored. - * @param preferSplit Whether it should prefer a split or a full apk. - * @return the downloaded apk or the folder containing all split apks. - */ - suspend fun downloadApp( - version: String, - saveDirectory: File, - preferSplit: Boolean = false - ): File } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt index bcfb06d2..13e37fea 100644 --- a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt +++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt @@ -29,7 +29,7 @@ data class PatchInfo( patch.compatiblePackages?.map { CompatiblePackage(it) }?.toImmutableList(), patch.options?.map { Option(it) }?.toImmutableList()) - fun compatibleWith(packageName: String) = compatiblePackages?.any { it.name == packageName } ?: true + fun compatibleWith(packageName: String) = compatiblePackages?.any { it.packageName == packageName } ?: true fun supportsVersion(versionName: String) = compatiblePackages?.any { compatiblePackages.any { it.versions.isEmpty() || it.versions.any { version -> version == versionName } } } @@ -38,7 +38,7 @@ data class PatchInfo( @Immutable data class CompatiblePackage( - val name: String, + val packageName: String, val versions: ImmutableList ) { constructor(pkg: Package) : this(pkg.name, pkg.versions.toList().toImmutableList()) diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherProgressManager.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherProgressManager.kt index baadc916..649c011b 100644 --- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherProgressManager.kt +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherProgressManager.kt @@ -3,11 +3,14 @@ package app.revanced.manager.patcher.worker import android.content.Context import androidx.annotation.StringRes import app.revanced.manager.R +import app.revanced.manager.ui.model.SelectedApp import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.StateFlow sealed class Progress { + object Downloading : Progress() object Unpacking : Progress() object Merging : Progress() object PatchingStart : Progress() @@ -24,23 +27,24 @@ enum class State { class SubStep( val name: String, val state: State = State.WAITING, - val message: String? = null + val message: String? = null, + val progress: StateFlow?>? = null ) class Step( @StringRes val name: Int, - val substeps: ImmutableList, + val subSteps: ImmutableList, val state: State = State.WAITING ) -class PatcherProgressManager(context: Context, selectedPatches: List) { - val steps = generateSteps(context, selectedPatches) +class PatcherProgressManager(context: Context, selectedPatches: List, selectedApp: SelectedApp, downloadProgress: StateFlow?>) { + val steps = generateSteps(context, selectedPatches, selectedApp, downloadProgress) private var currentStep: StepKey? = StepKey(0, 0) private fun update(key: StepKey, state: State, message: String? = null) { val isLastSubStep: Boolean steps[key.step] = steps[key.step].let { step -> - isLastSubStep = key.substep == step.substeps.lastIndex + isLastSubStep = key.substep == step.subSteps.lastIndex val newStepState = when { // This step failed because one of its sub-steps failed. @@ -51,7 +55,7 @@ class PatcherProgressManager(context: Context, selectedPatches: List) { else -> step.state } - Step(step.name, step.substeps.mapIndexed { index, subStep -> + Step(step.name, step.subSteps.mapIndexed { index, subStep -> if (index != key.substep) subStep else SubStep(subStep.name, state, message) }.toImmutableList(), newStepState) } @@ -100,10 +104,11 @@ class PatcherProgressManager(context: Context, selectedPatches: List) { * A map of [Progress] to the corresponding position in [steps] */ private val stepKeyMap = mapOf( - Progress.Unpacking to StepKey(0, 1), - Progress.Merging to StepKey(0, 2), + //Progress.Downloading to StepKey(0, 1), + //Progress.Unpacking to StepKey(0, 2), + //Progress.Merging to StepKey(0, 3), Progress.PatchingStart to StepKey(1, 0), - Progress.Saving to StepKey(2, 0), + //Progress.Saving to StepKey(2, 0), ) private fun generatePatchesStep(selectedPatches: List) = Step( @@ -111,14 +116,15 @@ class PatcherProgressManager(context: Context, selectedPatches: List) { selectedPatches.map { SubStep(it) }.toImmutableList() ) - fun generateSteps(context: Context, selectedPatches: List) = mutableListOf( + fun generateSteps(context: Context, selectedPatches: List, selectedApp: SelectedApp, downloadProgress: StateFlow?>? = null) = mutableListOf( Step( R.string.patcher_step_group_prepare, - persistentListOf( + listOfNotNull( SubStep(context.getString(R.string.patcher_step_load_patches)), + SubStep("Download apk", progress = downloadProgress).takeIf { selectedApp is SelectedApp.Download }, SubStep(context.getString(R.string.patcher_step_unpack)), SubStep(context.getString(R.string.patcher_step_integrations)) - ) + ).toImmutableList() ), generatePatchesStep(selectedPatches), Step( 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 1c81a7e7..024674db 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 @@ -14,12 +14,16 @@ import androidx.core.content.ContextCompat import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import app.revanced.manager.R +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.domain.repository.DownloadedAppRepository import app.revanced.manager.domain.repository.SourceRepository import app.revanced.manager.domain.worker.Worker import app.revanced.manager.domain.worker.WorkerRepository import app.revanced.manager.patcher.Session import app.revanced.manager.patcher.aapt.Aapt +import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.util.Options +import app.revanced.manager.util.PM import app.revanced.manager.util.PatchesSelection import app.revanced.manager.util.tag import app.revanced.patcher.extensions.PatchExtensions.options @@ -34,14 +38,19 @@ import org.koin.core.component.inject import java.io.File import java.io.FileNotFoundException -class PatcherWorker(context: Context, parameters: WorkerParameters) : - Worker(context, parameters), - KoinComponent { +class PatcherWorker( + context: Context, + parameters: WorkerParameters +) : Worker(context, parameters), KoinComponent { + private val sourceRepository: SourceRepository by inject() private val workerRepository: WorkerRepository by inject() + private val prefs: PreferencesManager by inject() + private val downloadedAppRepository: DownloadedAppRepository by inject() + private val pm: PM by inject() data class Args( - val input: String, + val input: SelectedApp, val output: String, val selectedPatches: PatchesSelection, val options: Options, @@ -118,8 +127,10 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) : val bundles = sourceRepository.bundles.first() val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations } + val downloadProgress = MutableStateFlow?>(null) + val progressManager = - PatcherProgressManager(applicationContext, args.selectedPatches.flatMap { it.value }) + PatcherProgressManager(applicationContext, args.selectedPatches.flatMap { it.value }, args.input, downloadProgress) val progressFlow = args.progress @@ -155,6 +166,28 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) : // Ensure they are in the correct order so we can track progress properly. progressManager.replacePatchesList(patches.map { it.patchName }) + val inputFile = when (val selectedApp = args.input) { + is SelectedApp.Download -> { + updateProgress(Progress.Downloading) + + val savePath = applicationContext.filesDir.resolve("downloaded-apps").resolve(args.input.packageName).also { it.mkdirs() } + + selectedApp.app.download( + savePath, + prefs.preferSplits.get(), + onDownload = { downloadProgress.emit(it) } + ).also { + downloadedAppRepository.add( + args.input.packageName, + args.input.version, + it + ) + } + } + is SelectedApp.Local -> selectedApp.file + is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo.sourceDir) + } + updateProgress(Progress.Unpacking) Session( @@ -162,10 +195,9 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) : frameworkPath, aaptPath, args.logger, - File(args.input) - ) { - updateProgress(it) - }.use { session -> + inputFile, + onProgress = { updateProgress(it) } + ).use { session -> session.run(File(args.output), patches, integrations) } @@ -173,7 +205,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) : progressManager.success() Result.success() } catch (e: Exception) { - Log.e(tag, "Got exception while patching".logFmt(), e) + Log.e(tag, "Exception while patching".logFmt(), e) progressManager.failure(e) Result.failure() } finally { diff --git a/app/src/main/java/app/revanced/manager/ui/component/AppIcon.kt b/app/src/main/java/app/revanced/manager/ui/component/AppIcon.kt index 7164f9bd..6eb101f9 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/AppIcon.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/AppIcon.kt @@ -1,39 +1,49 @@ package app.revanced.manager.ui.component +import android.content.pm.PackageInfo import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Android import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.unit.dp -import app.revanced.manager.util.AppInfo import coil.compose.AsyncImage +import com.google.accompanist.placeholder.placeholder @Composable fun AppIcon( - app: AppInfo, + packageInfo: PackageInfo?, contentDescription: String?, modifier: Modifier = Modifier ) { - if (app.packageInfo == null) { + var showPlaceHolder by rememberSaveable { mutableStateOf(true) } + + if (packageInfo == null) { val image = rememberVectorPainter(Icons.Default.Android) val colorFilter = ColorFilter.tint(LocalContentColor.current) Image( image, contentDescription, - Modifier.size(36.dp).then(modifier), + Modifier.placeholder(visible = showPlaceHolder, color = MaterialTheme.colorScheme.inverseOnSurface, shape = RoundedCornerShape(100)).then(modifier), colorFilter = colorFilter ) + + showPlaceHolder = false } else { AsyncImage( - app.packageInfo, + packageInfo, contentDescription, - Modifier.size(36.dp).then(modifier) + Modifier.placeholder(visible = showPlaceHolder, color = MaterialTheme.colorScheme.inverseOnSurface, shape = RoundedCornerShape(100)).then(modifier), + onSuccess = { showPlaceHolder = false } ) } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/AppLabel.kt b/app/src/main/java/app/revanced/manager/ui/component/AppLabel.kt new file mode 100644 index 00000000..238f3673 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/AppLabel.kt @@ -0,0 +1,52 @@ +package app.revanced.manager.ui.component + +import android.content.pm.PackageInfo +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import app.revanced.manager.R +import com.google.accompanist.placeholder.placeholder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +fun AppLabel( + packageInfo: PackageInfo?, + modifier: Modifier = Modifier, + style: TextStyle = LocalTextStyle.current, + defaultText: String = stringResource(R.string.not_installed) +) { + val context = LocalContext.current + + var label: String? by rememberSaveable { mutableStateOf(null) } + + LaunchedEffect(packageInfo) { + label = withContext(Dispatchers.IO) { + packageInfo?.applicationInfo?.loadLabel(context.packageManager)?.toString() + ?: defaultText + } + } + + Text( + label ?: stringResource(R.string.loading), + modifier = Modifier + .placeholder( + visible = label == null, + color = MaterialTheme.colorScheme.inverseOnSurface, + shape = RoundedCornerShape(100) + ) + .then(modifier), + style = style + ) +} \ No newline at end of file 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 b90ac27e..f9b7b9c0 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 @@ -1,7 +1,7 @@ package app.revanced.manager.ui.destination import android.os.Parcelable -import app.revanced.manager.util.AppInfo +import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.util.Options import app.revanced.manager.util.PatchesSelection import kotlinx.parcelize.Parcelize @@ -19,11 +19,11 @@ sealed interface Destination : Parcelable { object Settings : Destination @Parcelize - data class AppDownloader(val app: AppInfo) : Destination + data class VersionSelector(val packageName: String) : Destination @Parcelize - data class PatchesSelector(val input: AppInfo) : Destination + data class PatchesSelector(val selectedApp: SelectedApp) : Destination @Parcelize - data class Installer(val app: AppInfo, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination + data class Installer(val selectedApp: SelectedApp, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination } \ 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 new file mode 100644 index 00000000..424d07e7 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/model/SelectedApp.kt @@ -0,0 +1,20 @@ +package app.revanced.manager.ui.model + +import android.os.Parcelable +import app.revanced.manager.network.downloader.AppDownloader +import kotlinx.parcelize.Parcelize +import java.io.File + +sealed class SelectedApp : Parcelable { + abstract val packageName: String + abstract val version: String + + @Parcelize + data class Download(override val packageName: String, override val version: String, val app: AppDownloader.App) : SelectedApp() + + @Parcelize + data class Local(override val packageName: String, override val version: String, val file: File) : 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 d69c4459..a46534a6 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 @@ -26,19 +26,20 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.ui.component.AppIcon +import app.revanced.manager.ui.component.AppLabel import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.LoadingIndicator +import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.viewmodel.AppSelectorViewModel import app.revanced.manager.util.APK_MIMETYPE -import app.revanced.manager.util.AppInfo import app.revanced.manager.util.toast import org.koin.androidx.compose.getViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable fun AppSelectorScreen( - onAppClick: (AppInfo) -> Unit, - onDownloaderClick: (AppInfo) -> Unit, + onAppClick: (packageName: String) -> Unit, + onStorageClick: (SelectedApp.Local) -> Unit, onBackClick: () -> Unit, vm: AppSelectorViewModel = getViewModel() ) { @@ -47,7 +48,7 @@ fun AppSelectorScreen( val pickApkLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> uri?.let { apkUri -> - vm.loadSelectedFile(apkUri)?.let(onAppClick) ?: context.toast(context.getString(R.string.failed_to_load_apk)) + vm.loadSelectedFile(apkUri)?.let(onStorageClick) ?: context.toast(context.getString(R.string.failed_to_load_apk)) } } @@ -64,17 +65,6 @@ fun AppSelectorScreen( } } - var selectedApp: AppInfo? by rememberSaveable { mutableStateOf(null) } - - selectedApp?.let { - VersionDialog( - selectedApp = it, - onDismissRequest = { selectedApp = null }, - onSelectVersionClick = onDownloaderClick, - onContinueClick = onAppClick - ) - } - // TODO: find something better for this if (search) { SearchBar( @@ -104,13 +94,11 @@ fun AppSelectorScreen( ) { app -> ListItem( - modifier = Modifier.clickable { - app.packageInfo?.let { onAppClick(app) } - }, - leadingContent = { AppIcon(app, null) }, - headlineContent = { Text(vm.loadLabel(app.packageInfo)) }, + modifier = Modifier.clickable { onAppClick(app.packageName) }, + leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) }, + headlineContent = { AppLabel(app.packageInfo) }, supportingContent = { Text(app.packageName) }, - trailingContent = if (app.patches > 0) { { Text(pluralStringResource(R.plurals.patches_count, app.patches, app.patches)) } } else null + trailingContent = app.patches?.let { { Text(pluralStringResource(R.plurals.patches_count, it, it)) } } ) } @@ -169,11 +157,11 @@ fun AppSelectorScreen( ) { app -> ListItem( - modifier = Modifier.clickable { selectedApp = app }, - leadingContent = { AppIcon(app, null) }, - headlineContent = { Text(vm.loadLabel(app.packageInfo)) }, + modifier = Modifier.clickable { onAppClick(app.packageName) }, + leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) }, + headlineContent = { AppLabel(app.packageInfo) }, supportingContent = { Text(app.packageName) }, - trailingContent = if (app.patches > 0) { { Text(pluralStringResource(R.plurals.patches_count, app.patches, app.patches)) } } else null + trailingContent = app.patches?.let { { Text(pluralStringResource(R.plurals.patches_count, it, it)) } } ) } @@ -182,51 +170,4 @@ fun AppSelectorScreen( } } } -} - -@Composable -fun VersionDialog( - selectedApp: AppInfo, - onDismissRequest: () -> Unit, - onSelectVersionClick: (AppInfo) -> Unit, - onContinueClick: (AppInfo) -> Unit -) = if (selectedApp.packageInfo != null) AlertDialog( - onDismissRequest = onDismissRequest, - title = { Text(stringResource(R.string.continue_with_version)) }, - text = { Text(stringResource(R.string.version_not_supported, selectedApp.packageInfo.versionName)) }, - confirmButton = { - Column( - horizontalAlignment = Alignment.End - ) { - TextButton(onClick = { - onSelectVersionClick(selectedApp) - onDismissRequest() - }) { - Text(stringResource(R.string.download_another_version)) - } - TextButton(onClick = { - onContinueClick(selectedApp) - onDismissRequest() - }) { - Text(stringResource(R.string.continue_anyways)) - } - } - } -) else AlertDialog( - onDismissRequest = onDismissRequest, - title = { Text(stringResource(R.string.download_application)) }, - text = { Text(stringResource(R.string.app_not_installed)) }, - confirmButton = { - TextButton(onClick = { - onDismissRequest() - }) { - Text(stringResource(R.string.cancel)) - } - TextButton(onClick = { - onSelectVersionClick(selectedApp) - onDismissRequest() - }) { - Text(stringResource(R.string.download_app)) - } - } -) \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/InstallerScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/InstallerScreen.kt index 0728a0a2..69d9c074 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/InstallerScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/InstallerScreen.kt @@ -1,5 +1,6 @@ package app.revanced.manager.ui.screen +import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.compose.animation.AnimatedVisibility @@ -49,6 +50,8 @@ fun InstallerScreen( onBackClick: () -> Unit, vm: InstallerViewModel ) { + BackHandler(onBack = onBackClick) + val context = LocalContext.current val exportApkLauncher = rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export) @@ -142,7 +145,7 @@ fun InstallStep(step: Step) { .padding(start = 16.dp, end = 16.dp) .background(if (expanded) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface) ) { - StepIcon(step.state, 24.dp) + StepIcon(step.state, size = 24.dp) Text(text = stringResource(step.name), style = MaterialTheme.typography.titleMedium) @@ -162,18 +165,19 @@ fun InstallStep(step: Step) { .padding(16.dp) .padding(start = 4.dp) ) { - step.substeps.forEach { + step.subSteps.forEach { subStep -> var messageExpanded by rememberSaveable { mutableStateOf(true) } - val stacktrace = it.message + val stacktrace = subStep.message + val downloadProgress = subStep.progress?.collectAsStateWithLifecycle() Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp), ) { - StepIcon(it.state, size = 18.dp) + StepIcon(subStep.state, downloadProgress?.value, size = 18.dp) Text( - text = it.name, + text = subStep.name, style = MaterialTheme.typography.titleSmall, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -184,6 +188,13 @@ fun InstallStep(step: Step) { ArrowButton(expanded = messageExpanded) { messageExpanded = !messageExpanded } + } else { + downloadProgress?.value?.let { (downloaded, total) -> + Text( + "$downloaded/$total MB", + style = MaterialTheme.typography.labelSmall + ) + } } } @@ -201,7 +212,7 @@ fun InstallStep(step: Step) { } @Composable -fun StepIcon(status: State, size: Dp) { +fun StepIcon(status: State, downloadProgress: Pair? = null, size: Dp) { val strokeWidth = Dp(floor(size.value / 10) + 1) when (status) { @@ -219,15 +230,28 @@ fun StepIcon(status: State, size: Dp) { modifier = Modifier.size(size) ) - State.WAITING -> CircularProgressIndicator( - strokeWidth = strokeWidth, - modifier = stringResource(R.string.step_running).let { description -> - Modifier - .size(size) - .semantics { - contentDescription = description + State.WAITING -> + downloadProgress?.let { (downloaded, total) -> + CircularProgressIndicator( + progress = downloaded / total, + strokeWidth = strokeWidth, + modifier = stringResource(R.string.step_running).let { description -> + Modifier + .size(size) + .semantics { + contentDescription = description + } } - } - ) + ) + } ?: CircularProgressIndicator( + strokeWidth = strokeWidth, + modifier = stringResource(R.string.step_running).let { description -> + Modifier + .size(size) + .semantics { + contentDescription = description + } + } + ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt index cd3771c8..1c7fea53 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt @@ -1,6 +1,5 @@ package app.revanced.manager.ui.screen -import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -66,8 +65,6 @@ fun PatchesSelectorScreen( onBackClick: () -> Unit, vm: PatchesSelectorViewModel ) { - BackHandler(onBack = onBackClick) - val pagerState = rememberPagerState() val composableScope = rememberCoroutineScope() @@ -75,7 +72,7 @@ fun PatchesSelectorScreen( if (vm.compatibleVersions.isNotEmpty()) UnsupportedDialog( - appVersion = vm.appInfo.packageInfo!!.versionName, + appVersion = vm.selectedApp.version, supportedVersions = vm.compatibleVersions, onDismissRequest = vm::dismissDialogs ) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/AppDownloaderScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt similarity index 75% rename from app/src/main/java/app/revanced/manager/ui/screen/AppDownloaderScreen.kt rename to app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt index 2b71e879..b35d750b 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/AppDownloaderScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.material3.ListItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -31,33 +30,28 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.LoadingIndicator -import app.revanced.manager.ui.viewmodel.AppDownloaderViewModel -import app.revanced.manager.util.AppInfo +import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.ui.viewmodel.VersionSelectorViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AppDownloaderScreen( +fun VersionSelectorScreen( onBackClick: () -> Unit, - onApkClick: (AppInfo) -> Unit, - viewModel: AppDownloaderViewModel + onAppClick: (SelectedApp) -> Unit, + viewModel: VersionSelectorViewModel ) { - SideEffect { - viewModel.onComplete = onApkClick - } - - val downloadProgress by viewModel.appDownloader.downloadProgress.collectAsStateWithLifecycle() - val compatibleVersions by viewModel.compatibleVersions.collectAsStateWithLifecycle(emptyMap()) + val supportedVersions by viewModel.supportedVersions.collectAsStateWithLifecycle(emptyMap()) val downloadedVersions by viewModel.downloadedVersions.collectAsStateWithLifecycle(emptyList()) val list by remember { derivedStateOf { - (downloadedVersions + viewModel.availableVersions) - .distinct() + (downloadedVersions + viewModel.downloadableVersions) + .distinctBy { it.version } .sortedWith( - compareByDescending { - downloadedVersions.contains(it) - }.thenByDescending { compatibleVersions[it] } - .thenByDescending { it } + compareByDescending { + it is SelectedApp.Local + }.thenByDescending { supportedVersions[it.version] } + .thenByDescending { it.version } ) } } @@ -92,18 +86,15 @@ fun AppDownloaderScreen( .fillMaxSize() .verticalScroll(rememberScrollState()) ) { - list.forEach { version -> + list.forEach { selectedApp -> ListItem( - modifier = Modifier.clickable { - viewModel.downloadApp(version) - }, - headlineContent = { Text(version) }, + modifier = Modifier.clickable { onAppClick(selectedApp) }, + headlineContent = { Text(selectedApp.version) }, supportingContent = - if (downloadedVersions.contains(version)) { + if (selectedApp is SelectedApp.Local) { { Text(stringResource(R.string.already_downloaded)) } } else null, - trailingContent = compatibleVersions[version]?.let { - { + trailingContent = supportedVersions[selectedApp.version]?.let { { Text( pluralStringResource( R.plurals.patches_count, @@ -140,10 +131,7 @@ fun AppDownloaderScreen( } else -> { - LoadingIndicator( - progress = downloadProgress?.let { (it.first / it.second) }, - text = downloadProgress?.let { stringResource(R.string.downloading_app, it.first, it.second) } - ) + LoadingIndicator() } } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppDownloaderViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppDownloaderViewModel.kt deleted file mode 100644 index c01cc91a..00000000 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppDownloaderViewModel.kt +++ /dev/null @@ -1,144 +0,0 @@ -package app.revanced.manager.ui.viewmodel - -import android.app.Application -import android.util.Log -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import app.revanced.manager.domain.manager.PreferencesManager -import app.revanced.manager.domain.repository.DownloadedAppRepository -import app.revanced.manager.domain.repository.SourceRepository -import app.revanced.manager.network.downloader.APKMirror -import app.revanced.manager.network.downloader.AppDownloader -import app.revanced.manager.util.AppInfo -import app.revanced.manager.util.PM -import app.revanced.manager.util.mutableStateSetOf -import app.revanced.manager.util.simpleMessage -import app.revanced.manager.util.tag -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.koin.core.component.KoinComponent -import org.koin.core.component.get - -class AppDownloaderViewModel( - private val selectedApp: AppInfo -) : ViewModel(), KoinComponent { - private val app: Application = get() - private val downloadedAppRepository: DownloadedAppRepository = get() - private val sourceRepository: SourceRepository = get() - private val pm: PM = get() - private val prefs: PreferencesManager = get() - val appDownloader: AppDownloader = APKMirror() - - var isDownloading: Boolean by mutableStateOf(false) - private set - var isLoading by mutableStateOf(true) - private set - var errorMessage: String? by mutableStateOf(null) - private set - - val availableVersions = mutableStateSetOf() - - val compatibleVersions = sourceRepository.bundles.map { bundles -> - var patchesWithoutVersions = 0 - - bundles.flatMap { (_, bundle) -> - bundle.patches.flatMap { patch -> - patch.compatiblePackages - .orEmpty() - .filter { it.name == selectedApp.packageName } - .onEach { - if (it.versions.isEmpty()) patchesWithoutVersions += 1 - } - .flatMap { it.versions } - } - }.groupingBy { it } - .eachCount() - .toMutableMap() - .apply { - replaceAll { _, count -> - count + patchesWithoutVersions - } - } - } - - val downloadedVersions = downloadedAppRepository.getAll().map { downloadedApps -> - downloadedApps.mapNotNull { - if (it.packageName == selectedApp.packageName) - it.version - else - null - } - } - - private val job = viewModelScope.launch(Dispatchers.IO) { - try { - val compatibleVersions = compatibleVersions.first() - - appDownloader.getAvailableVersions( - selectedApp.packageName, - compatibleVersions.keys - ).collect { - if (it in compatibleVersions || compatibleVersions.isEmpty()) { - availableVersions.add(it) - } - } - - withContext(Dispatchers.Main) { - isLoading = false - } - } catch (e: Exception) { - withContext(Dispatchers.Main) { - Log.e(tag, "Failed to load apps", e) - errorMessage = e.simpleMessage() - } - } - } - - lateinit var onComplete: (AppInfo) -> Unit - - fun downloadApp( - version: String - ) { - isDownloading = true - - job.cancel() - - viewModelScope.launch(Dispatchers.IO) { - try { - val savePath = app.filesDir.resolve("downloaded-apps").resolve(selectedApp.packageName).also { it.mkdirs() } - - val downloadedFile = - downloadedAppRepository.get(selectedApp.packageName, version)?.file - ?: appDownloader.downloadApp( - version, - savePath, - preferSplit = prefs.preferSplits.get() - ).also { - downloadedAppRepository.add( - selectedApp.packageName, - version, - it - ) - } - - val apkInfo = pm.getApkInfo(downloadedFile) - ?: throw Exception("Failed to load apk info") - - withContext(Dispatchers.Main) { - onComplete(apkInfo) - } - } catch (e: Throwable) { - withContext(Dispatchers.Main) { - Log.e(tag, "Failed to download apk", e) - errorMessage = e.simpleMessage() - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt index 2daeacbf..7b9cac2f 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt @@ -4,13 +4,14 @@ import android.app.Application import android.content.pm.PackageInfo import android.net.Uri import androidx.lifecycle.ViewModel +import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.util.PM import java.io.File import java.nio.file.Files class AppSelectorViewModel( private val app: Application, - private val pm: PM + pm: PM ) : ViewModel() { private val packageManager = app.packageManager @@ -18,11 +19,17 @@ class AppSelectorViewModel( fun loadLabel(app: PackageInfo?) = (app?.applicationInfo?.loadLabel(packageManager) ?: "Not installed").toString() + @Suppress("DEPRECATION") fun loadSelectedFile(uri: Uri) = - app.contentResolver.openInputStream(uri)!!.use { stream -> + app.contentResolver.openInputStream(uri)?.use { stream -> File(app.cacheDir, "input.apk").also { - if (it.exists()) it.delete() + it.delete() Files.copy(stream, it.toPath()) - }.let { pm.getApkInfo(it) } + }.let { file -> + packageManager.getPackageArchiveInfo(file.absolutePath, 0) + ?.let { packageInfo -> + SelectedApp.Local(packageName = packageInfo.packageName, version = packageInfo.versionName, file = file) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt index 44484b36..c98128ec 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt @@ -51,7 +51,7 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon private val pm: PM by inject() private val workerRepository: WorkerRepository by inject() - val packageName: String = input.app.packageName + val packageName: String = input.selectedApp.packageName private val outputFile = File(app.cacheDir, "output.apk") private val signedFile = File(app.cacheDir, "signed.apk").also { if (it.exists()) it.delete() } private var hasSigned = false @@ -69,21 +69,22 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon private val logger = ManagerLogger() init { - val (appInfo, patches, options) = input + val (selectedApp, patches, options) = input _progress = MutableStateFlow(PatcherProgressManager.generateSteps( app, - patches.flatMap { (_, selected) -> selected } + patches.flatMap { (_, selected) -> selected }, + input.selectedApp ).toImmutableList()) patcherWorkerId = workerRepository.launchExpedited( "patching", PatcherWorker.Args( - appInfo.path!!.absolutePath, + selectedApp, outputFile.path, patches, options, packageName, - appInfo.packageInfo!!.versionName, + selectedApp.version, _progress, logger ) diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt index d040b96d..f985bbff 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt @@ -3,25 +3,16 @@ package app.revanced.manager.ui.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.revanced.manager.domain.repository.SourceRepository -import app.revanced.manager.util.PM -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class MainViewModel( - sourceRepository: SourceRepository, - pm: PM + sourceRepository: SourceRepository ) : ViewModel() { init { with(viewModelScope) { launch { sourceRepository.loadSources() } - launch { - pm.getCompatibleApps() - } - launch(Dispatchers.IO) { - pm.getInstalledApps() - } } } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt index 987d9712..023a5359 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt @@ -17,7 +17,7 @@ import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.SourceRepository import app.revanced.manager.patcher.patch.PatchInfo -import app.revanced.manager.util.AppInfo +import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.util.Options import app.revanced.manager.util.PatchesSelection import app.revanced.manager.util.SnapshotStateSet @@ -37,7 +37,7 @@ import org.koin.core.component.get @Stable @OptIn(SavedStateHandleSaveableApi::class) class PatchesSelectorViewModel( - val appInfo: AppInfo + val selectedApp: SelectedApp ) : ViewModel(), KoinComponent { private val selectionRepository: PatchSelectionRepository = get() private val savedStateHandle: SavedStateHandle = get() @@ -52,12 +52,12 @@ class PatchesSelectorViewModel( val unsupported = mutableListOf() val universal = mutableListOf() - bundle.patches.filter { it.compatibleWith(appInfo.packageName) }.forEach { + bundle.patches.filter { it.compatibleWith(selectedApp.packageName) }.forEach { val targetList = - if (it.compatiblePackages == null) universal else if (it.supportsVersion( - appInfo.packageInfo!!.versionName - ) - ) supported else unsupported + if (it.compatiblePackages == null) universal else if (it.supportsVersion(selectedApp.version)) + supported + else + unsupported targetList.add(it) } @@ -71,7 +71,7 @@ class PatchesSelectorViewModel( viewModelScope.launch(Dispatchers.Default) { val bundles = bundlesFlow.first() val filteredSelection = - selectionRepository.getSelection(appInfo.packageName).mapValues { (uid, patches) -> + selectionRepository.getSelection(selectedApp.packageName).mapValues { (uid, patches) -> // Filter out patches that don't exist. val filteredPatches = bundles.singleOrNull { it.uid == uid } ?.let { bundle -> @@ -117,7 +117,7 @@ class PatchesSelectorViewModel( suspend fun getAndSaveSelection(): PatchesSelection = selectedPatches.also { withContext(Dispatchers.Default) { - selectionRepository.updateSelection(appInfo.packageName, it) + selectionRepository.updateSelection(selectedApp.packageName, it) } }.mapValues { it.value.toMutableSet() }.apply { if (allowExperimental.get()) { @@ -150,7 +150,7 @@ class PatchesSelectorViewModel( val set = HashSet() unsupportedVersions.forEach { patch -> - patch.compatiblePackages?.find { it.name == appInfo.packageName } + patch.compatiblePackages?.find { it.packageName == selectedApp.packageName } ?.let { compatiblePackage -> set.addAll(compatiblePackage.versions) } 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 new file mode 100644 index 00000000..044a5c81 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt @@ -0,0 +1,96 @@ +package app.revanced.manager.ui.viewmodel + +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.domain.repository.DownloadedAppRepository +import app.revanced.manager.domain.repository.SourceRepository +import app.revanced.manager.network.downloader.APKMirror +import app.revanced.manager.network.downloader.AppDownloader +import app.revanced.manager.ui.model.SelectedApp +import app.revanced.manager.util.mutableStateSetOf +import app.revanced.manager.util.simpleMessage +import app.revanced.manager.util.tag +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.koin.core.component.KoinComponent +import org.koin.core.component.get + +class VersionSelectorViewModel( + val packageName: String +) : ViewModel(), KoinComponent { + private val downloadedAppRepository: DownloadedAppRepository = get() + private val sourceRepository: SourceRepository = get() + private val appDownloader: AppDownloader = APKMirror() + + var isDownloading: Boolean by mutableStateOf(false) + private set + var isLoading by mutableStateOf(true) + private set + var errorMessage: String? by mutableStateOf(null) + private set + + val downloadableVersions = mutableStateSetOf() + + val supportedVersions = sourceRepository.bundles.map { bundles -> + var patchesWithoutVersions = 0 + + bundles.flatMap { (_, bundle) -> + bundle.patches.flatMap { patch -> + patch.compatiblePackages.orEmpty() + .filter { it.packageName == packageName } + .onEach { if (it.versions.isEmpty()) patchesWithoutVersions++ } + .flatMap { it.versions } + } + }.groupingBy { it } + .eachCount() + .toMutableMap() + .apply { + replaceAll { _, count -> + count + patchesWithoutVersions + } + } + } + + val downloadedVersions = downloadedAppRepository.getAll().map { downloadedApps -> + downloadedApps.filter { it.packageName == packageName }.map { SelectedApp.Local(it.packageName, it.version, it.file) } + } + + init { + viewModelScope.launch(Dispatchers.IO) { + try { + val compatibleVersions = supportedVersions.first() + + appDownloader.getAvailableVersions( + packageName, + compatibleVersions.keys + ).collect { + if (it.version in compatibleVersions || compatibleVersions.isEmpty()) { + downloadableVersions.add( + SelectedApp.Download( + packageName, + it.version, + it + ) + ) + } + } + + withContext(Dispatchers.Main) { + isLoading = false + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + Log.e(tag, "Failed to load apps", e) + errorMessage = e.simpleMessage() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/PM.kt b/app/src/main/java/app/revanced/manager/util/PM.kt index 7b57b83e..c1e0d477 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -9,16 +9,18 @@ import android.content.pm.PackageInfo import android.content.pm.PackageInstaller import android.content.pm.PackageManager import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES +import android.content.pm.PackageManager.NameNotFoundException import android.os.Build import android.os.Parcelable import androidx.compose.runtime.Immutable import app.revanced.manager.domain.repository.SourceRepository import app.revanced.manager.service.InstallService import app.revanced.manager.service.UninstallService +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import java.io.File @@ -29,7 +31,7 @@ private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readab @Parcelize data class AppInfo( val packageName: String, - val patches: Int, + val patches: Int?, val packageInfo: PackageInfo?, val path: File? = null ) : Parcelable @@ -38,69 +40,65 @@ data class AppInfo( @Suppress("DEPRECATION") class PM( private val app: Application, - private val sourceRepository: SourceRepository + sourceRepository: SourceRepository ) { - private val installedApps = MutableStateFlow(emptyList()) - private val compatibleApps = MutableStateFlow(emptyList()) + private val scope = CoroutineScope(Dispatchers.IO) - val appList: Flow> = compatibleApps.combine(installedApps) { compatibleApps, installedApps -> - if (compatibleApps.isNotEmpty()) { - (compatibleApps + installedApps) + val appList = sourceRepository.bundles.map { bundles -> + val compatibleApps = scope.async { + val compatiblePackages = bundles.values + .flatMap { it.patches } + .flatMap { it.compatiblePackages.orEmpty() } + .groupingBy { it.packageName } + .eachCount() + + compatiblePackages.keys.map { pkg -> + try { + val packageInfo = app.packageManager.getPackageInfo(pkg, 0) + AppInfo( + pkg, + compatiblePackages[pkg], + packageInfo, + File(packageInfo.applicationInfo.sourceDir) + ) + } catch (e: NameNotFoundException) { + AppInfo( + pkg, + compatiblePackages[pkg], + null + ) + } + } + } + + val installedApps = scope.async { + app.packageManager.getInstalledPackages(MATCH_UNINSTALLED_PACKAGES).map { packageInfo -> + AppInfo( + packageInfo.packageName, + 0, + packageInfo, + File(packageInfo.applicationInfo.sourceDir) + ) + } + } + + if (compatibleApps.await().isNotEmpty()) { + (compatibleApps.await() + installedApps.await()) .distinctBy { it.packageName } .sortedWith( compareByDescending { it.patches }.thenBy { it.packageInfo?.applicationInfo?.loadLabel(app.packageManager).toString() }.thenBy { it.packageName } ) - } else { - emptyList() + } else { emptyList() } + }.flowOn(Dispatchers.IO) + + fun getPackageInfo(packageName: String): PackageInfo? = + try { + app.packageManager.getPackageInfo(packageName, 0) + } catch (e: NameNotFoundException) { + null } - } - - suspend fun getCompatibleApps() { - sourceRepository.bundles.collect { bundles -> - val compatiblePackages = HashMap() - - bundles.flatMap { it.value.patches }.forEach { - it.compatiblePackages?.forEach { pkg -> - compatiblePackages[pkg.name] = compatiblePackages.getOrDefault(pkg.name, 0) + 1 - } - } - - withContext(Dispatchers.IO) { - compatibleApps.emit( - compatiblePackages.keys.map { pkg -> - try { - val packageInfo = app.packageManager.getPackageInfo(pkg, 0) - AppInfo( - pkg, - compatiblePackages[pkg] ?: 0, - packageInfo, - File(packageInfo.applicationInfo.sourceDir) - ) - } catch (e: PackageManager.NameNotFoundException) { - AppInfo( - pkg, - compatiblePackages[pkg] ?: 0, - null - ) - } - } - ) - } - } - } - - suspend fun getInstalledApps() { - installedApps.emit(app.packageManager.getInstalledPackages(MATCH_UNINSTALLED_PACKAGES).map { packageInfo -> - AppInfo( - packageInfo.packageName, - 0, - packageInfo, - File(packageInfo.applicationInfo.sourceDir) - ) - }) - } suspend fun installApp(apks: List) = withContext(Dispatchers.IO) { val packageInstaller = app.packageManager.packageInstaller @@ -119,15 +117,6 @@ class PM( it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) app.startActivity(it) } - - fun getApkInfo(apk: File) = app.packageManager.getPackageArchiveInfo(apk.path, 0)?.let { - AppInfo( - it.packageName, - 0, - it, - apk - ) - } } private fun PackageInstaller.Session.writeApk(apk: File) { diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt index 5409bfa1..a35797f7 100644 --- a/app/src/main/java/app/revanced/manager/util/Util.kt +++ b/app/src/main/java/app/revanced/manager/util/Util.kt @@ -1,11 +1,7 @@ package app.revanced.manager.util -import android.app.Activity import android.content.Context -import android.content.ContextWrapper import android.content.Intent -import android.content.pm.PackageManager.NameNotFoundException -import android.graphics.drawable.Drawable import android.util.Log import android.widget.Toast import androidx.annotation.StringRes @@ -33,14 +29,6 @@ fun Context.openUrl(url: String) { }) } -fun Context.loadIcon(string: String): Drawable? { - return try { - packageManager.getApplicationIcon(string) - } catch (e: NameNotFoundException) { - null - } -} - fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) { Toast.makeText(this, string, duration).show() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 41e6cbb4..d57ca41f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -128,10 +128,10 @@ Download application? The app you selected isn\'t installed. Do you want to download it? Failed to load apk + Loading… + Not installed - An error occurred Already downloaded - Downloading app… (%1$s MB/%2$s MB) Select file