mirror of
https://github.com/revanced/revanced-manager-compose
synced 2025-02-21 18:41:10 +01:00
feat: download apps in patcher screen (#73)
This commit is contained in:
parent
e0eedf59db
commit
6cfdcaba7d
@ -96,6 +96,7 @@ dependencies {
|
|||||||
// Accompanist
|
// Accompanist
|
||||||
implementation(libs.accompanist.drawablepainter)
|
implementation(libs.accompanist.drawablepainter)
|
||||||
implementation(libs.accompanist.webview)
|
implementation(libs.accompanist.webview)
|
||||||
|
implementation(libs.accompanist.placeholder)
|
||||||
|
|
||||||
// HTML Scraper
|
// HTML Scraper
|
||||||
implementation(libs.skrapeit.dsl)
|
implementation(libs.skrapeit.dsl)
|
||||||
|
@ -9,7 +9,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.ui.destination.Destination
|
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.AppSelectorScreen
|
||||||
import app.revanced.manager.ui.screen.DashboardScreen
|
import app.revanced.manager.ui.screen.DashboardScreen
|
||||||
import app.revanced.manager.ui.screen.InstallerScreen
|
import app.revanced.manager.ui.screen.InstallerScreen
|
||||||
@ -83,29 +83,29 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
is Destination.AppSelector -> AppSelectorScreen(
|
is Destination.AppSelector -> AppSelectorScreen(
|
||||||
onAppClick = { navController.navigate(Destination.PatchesSelector(it)) },
|
onAppClick = { navController.navigate(Destination.VersionSelector(it)) },
|
||||||
onDownloaderClick = { navController.navigate(Destination.AppDownloader(it)) },
|
onStorageClick = { navController.navigate(Destination.PatchesSelector(it)) },
|
||||||
onBackClick = { navController.pop() }
|
onBackClick = { navController.pop() }
|
||||||
)
|
)
|
||||||
|
|
||||||
is Destination.AppDownloader -> AppDownloaderScreen(
|
is Destination.VersionSelector -> VersionSelectorScreen(
|
||||||
onBackClick = { navController.pop() },
|
onBackClick = { navController.pop() },
|
||||||
onApkClick = { navController.navigate(Destination.PatchesSelector(it)) },
|
onAppClick = { navController.navigate(Destination.PatchesSelector(it)) },
|
||||||
viewModel = getViewModel { parametersOf(destination.app) }
|
viewModel = getViewModel { parametersOf(destination.packageName) }
|
||||||
)
|
)
|
||||||
|
|
||||||
is Destination.PatchesSelector -> PatchesSelectorScreen(
|
is Destination.PatchesSelector -> PatchesSelectorScreen(
|
||||||
onBackClick = { navController.popUpTo { it is Destination.AppSelector } },
|
onBackClick = { navController.pop() },
|
||||||
onPatchClick = { patches, options ->
|
onPatchClick = { patches, options ->
|
||||||
navController.navigate(
|
navController.navigate(
|
||||||
Destination.Installer(
|
Destination.Installer(
|
||||||
destination.input,
|
destination.selectedApp,
|
||||||
patches,
|
patches,
|
||||||
options
|
options
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
vm = getViewModel { parametersOf(destination.input) }
|
vm = getViewModel { parametersOf(destination.selectedApp) }
|
||||||
)
|
)
|
||||||
|
|
||||||
is Destination.Installer -> InstallerScreen(
|
is Destination.Installer -> InstallerScreen(
|
||||||
|
@ -9,7 +9,7 @@ val viewModelModule = module {
|
|||||||
viewModelOf(::PatchesSelectorViewModel)
|
viewModelOf(::PatchesSelectorViewModel)
|
||||||
viewModelOf(::SettingsViewModel)
|
viewModelOf(::SettingsViewModel)
|
||||||
viewModelOf(::AppSelectorViewModel)
|
viewModelOf(::AppSelectorViewModel)
|
||||||
viewModelOf(::AppDownloaderViewModel)
|
viewModelOf(::VersionSelectorViewModel)
|
||||||
viewModelOf(::SourcesViewModel)
|
viewModelOf(::SourcesViewModel)
|
||||||
viewModelOf(::InstallerViewModel)
|
viewModelOf(::InstallerViewModel)
|
||||||
viewModelOf(::UpdateProgressViewModel)
|
viewModelOf(::UpdateProgressViewModel)
|
||||||
|
@ -12,11 +12,12 @@ import it.skrape.selects.html5.h5
|
|||||||
import it.skrape.selects.html5.input
|
import it.skrape.selects.html5.input
|
||||||
import it.skrape.selects.html5.p
|
import it.skrape.selects.html5.p
|
||||||
import it.skrape.selects.html5.span
|
import it.skrape.selects.html5.span
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
|
import org.koin.core.component.inject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class APKMirror : AppDownloader, KoinComponent {
|
class APKMirror : AppDownloader, KoinComponent {
|
||||||
@ -33,11 +34,6 @@ class APKMirror : AppDownloader, KoinComponent {
|
|||||||
val link: String
|
val link: String
|
||||||
)
|
)
|
||||||
|
|
||||||
private val _downloadProgress: MutableStateFlow<Pair<Float, Float>?> = MutableStateFlow(null)
|
|
||||||
override val downloadProgress = _downloadProgress.asStateFlow()
|
|
||||||
|
|
||||||
private val versionMap = HashMap<String, String>()
|
|
||||||
|
|
||||||
private suspend fun getAppLink(packageName: String): String {
|
private suspend fun getAppLink(packageName: String): String {
|
||||||
val searchResults = httpClient.getHtml { url("$apkMirror/?post_type=app_release&searchtype=app&s=$packageName") }
|
val searchResults = httpClient.getHtml { url("$apkMirror/?post_type=app_release&searchtype=app&s=$packageName") }
|
||||||
.div {
|
.div {
|
||||||
@ -92,7 +88,7 @@ class APKMirror : AppDownloader, KoinComponent {
|
|||||||
} ?: throw Exception("App isn't available for download")
|
} ?: throw Exception("App isn't available for download")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAvailableVersions(packageName: String, versionFilter: Set<String>) = flow {
|
override fun getAvailableVersions(packageName: String, versionFilter: Set<String>) = flow<AppDownloader.App> {
|
||||||
|
|
||||||
// Vanced music uses the same package name so we have to hardcode...
|
// Vanced music uses the same package name so we have to hardcode...
|
||||||
val appCategory = if (packageName == "com.google.android.apps.youtube.music")
|
val appCategory = if (packageName == "com.google.android.apps.youtube.music")
|
||||||
@ -102,9 +98,11 @@ class APKMirror : AppDownloader, KoinComponent {
|
|||||||
|
|
||||||
var page = 1
|
var page = 1
|
||||||
|
|
||||||
|
val versions = mutableListOf<String>()
|
||||||
|
|
||||||
while (
|
while (
|
||||||
if (versionFilter.isNotEmpty())
|
if (versionFilter.isNotEmpty())
|
||||||
versionMap.filterKeys { it in versionFilter }.size < versionFilter.size && page <= 7
|
versions.size < versionFilter.size && page <= 7
|
||||||
else
|
else
|
||||||
page <= 1
|
page <= 1
|
||||||
) {
|
) {
|
||||||
@ -119,7 +117,10 @@ class APKMirror : AppDownloader, KoinComponent {
|
|||||||
findFirst {
|
findFirst {
|
||||||
children.mapNotNull { element ->
|
children.mapNotNull { element ->
|
||||||
if (element.className.isEmpty()) {
|
if (element.className.isEmpty()) {
|
||||||
val version = element.div {
|
|
||||||
|
APKMirrorApp(
|
||||||
|
packageName = packageName,
|
||||||
|
version = element.div {
|
||||||
withClass = "infoSlide"
|
withClass = "infoSlide"
|
||||||
findFirst {
|
findFirst {
|
||||||
p {
|
p {
|
||||||
@ -133,9 +134,11 @@ class APKMirror : AppDownloader, KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}.also {
|
||||||
|
if (it in versionFilter)
|
||||||
val link = element.findFirst {
|
versions.add(it)
|
||||||
|
},
|
||||||
|
downloadLink = element.findFirst {
|
||||||
a {
|
a {
|
||||||
withClass = "downloadLink"
|
withClass = "downloadLink"
|
||||||
findFirst {
|
findFirst {
|
||||||
@ -143,9 +146,8 @@ class APKMirror : AppDownloader, KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
versionMap[version] = link
|
|
||||||
version
|
|
||||||
} else null
|
} else null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,12 +159,20 @@ class APKMirror : AppDownloader, KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun downloadApp(
|
@Parcelize
|
||||||
version: String,
|
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,
|
saveDirectory: File,
|
||||||
preferSplit: Boolean
|
preferSplit: Boolean,
|
||||||
|
onDownload: suspend (downloadProgress: Pair<Float, Float>?) -> Unit
|
||||||
): File {
|
): File {
|
||||||
val variants = httpClient.getHtml { url(apkMirror + versionMap[version]) }
|
val variants = httpClient.getHtml { url(apkMirror + downloadLink) }
|
||||||
.div {
|
.div {
|
||||||
withClass = "variants-table"
|
withClass = "variants-table"
|
||||||
findFirst { // list of variants
|
findFirst { // list of variants
|
||||||
@ -205,7 +215,7 @@ class APKMirror : AppDownloader, KoinComponent {
|
|||||||
}
|
}
|
||||||
} ?: throw Exception("No compatible variant found")
|
} ?: throw Exception("No compatible variant found")
|
||||||
|
|
||||||
if (variant.apkType == APKType.BUNDLE) TODO("\nSplit apks are not supported yet")
|
if (variant.apkType == APKType.BUNDLE) throw Exception("Split apks are not supported yet") // TODO
|
||||||
|
|
||||||
val downloadPage = httpClient.getHtml { url(apkMirror + variant.link) }
|
val downloadPage = httpClient.getHtml { url(apkMirror + variant.link) }
|
||||||
.a {
|
.a {
|
||||||
@ -250,7 +260,7 @@ class APKMirror : AppDownloader, KoinComponent {
|
|||||||
httpClient.download(downloadLocation) {
|
httpClient.download(downloadLocation) {
|
||||||
url(apkMirror + downloadLink)
|
url(apkMirror + downloadLink)
|
||||||
onDownload { bytesSentTotal, contentLength ->
|
onDownload { bytesSentTotal, contentLength ->
|
||||||
_downloadProgress.emit(bytesSentTotal.div(100000).toFloat().div(10) to contentLength.div(100000).toFloat().div(10))
|
onDownload(bytesSentTotal.div(100000).toFloat().div(10) to contentLength.div(100000).toFloat().div(10))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,11 +273,12 @@ class APKMirror : AppDownloader, KoinComponent {
|
|||||||
saveLocation.deleteRecursively()
|
saveLocation.deleteRecursively()
|
||||||
throw e
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
_downloadProgress.emit(null)
|
onDownload(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
return saveLocation
|
return saveLocation
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val apkMirror = "https://www.apkmirror.com"
|
const val apkMirror = "https://www.apkmirror.com"
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
package app.revanced.manager.network.downloader
|
package app.revanced.manager.network.downloader
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
interface AppDownloader {
|
interface AppDownloader {
|
||||||
val downloadProgress: StateFlow<Pair<Float, Float>?>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all downloadable apps.
|
* Returns all downloadable apps.
|
||||||
@ -13,19 +12,17 @@ interface AppDownloader {
|
|||||||
* @param packageName The package name of the app.
|
* @param packageName The package name of the app.
|
||||||
* @param versionFilter A set of versions to filter.
|
* @param versionFilter A set of versions to filter.
|
||||||
*/
|
*/
|
||||||
fun getAvailableVersions(packageName: String, versionFilter: Set<String>): Flow<String>
|
fun getAvailableVersions(packageName: String, versionFilter: Set<String>): Flow<App>
|
||||||
|
|
||||||
/**
|
interface App : Parcelable {
|
||||||
* Downloads the specific app version.
|
val packageName: String
|
||||||
*
|
val version: String
|
||||||
* @param version The version to download.
|
|
||||||
* @param saveDirectory The folder where the downloaded app should be stored.
|
suspend fun download(
|
||||||
* @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,
|
saveDirectory: File,
|
||||||
preferSplit: Boolean = false
|
preferSplit: Boolean,
|
||||||
|
onDownload: suspend (downloadProgress: Pair<Float, Float>?) -> Unit = {}
|
||||||
): File
|
): File
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
@ -29,7 +29,7 @@ data class PatchInfo(
|
|||||||
patch.compatiblePackages?.map { CompatiblePackage(it) }?.toImmutableList(),
|
patch.compatiblePackages?.map { CompatiblePackage(it) }?.toImmutableList(),
|
||||||
patch.options?.map { Option(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) =
|
fun supportsVersion(versionName: String) =
|
||||||
compatiblePackages?.any { compatiblePackages.any { it.versions.isEmpty() || it.versions.any { version -> version == versionName } } }
|
compatiblePackages?.any { compatiblePackages.any { it.versions.isEmpty() || it.versions.any { version -> version == versionName } } }
|
||||||
@ -38,7 +38,7 @@ data class PatchInfo(
|
|||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class CompatiblePackage(
|
data class CompatiblePackage(
|
||||||
val name: String,
|
val packageName: String,
|
||||||
val versions: ImmutableList<String>
|
val versions: ImmutableList<String>
|
||||||
) {
|
) {
|
||||||
constructor(pkg: Package) : this(pkg.name, pkg.versions.toList().toImmutableList())
|
constructor(pkg: Package) : this(pkg.name, pkg.versions.toList().toImmutableList())
|
||||||
|
@ -3,11 +3,14 @@ package app.revanced.manager.patcher.worker
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
sealed class Progress {
|
sealed class Progress {
|
||||||
|
object Downloading : Progress()
|
||||||
object Unpacking : Progress()
|
object Unpacking : Progress()
|
||||||
object Merging : Progress()
|
object Merging : Progress()
|
||||||
object PatchingStart : Progress()
|
object PatchingStart : Progress()
|
||||||
@ -24,23 +27,24 @@ enum class State {
|
|||||||
class SubStep(
|
class SubStep(
|
||||||
val name: String,
|
val name: String,
|
||||||
val state: State = State.WAITING,
|
val state: State = State.WAITING,
|
||||||
val message: String? = null
|
val message: String? = null,
|
||||||
|
val progress: StateFlow<Pair<Float, Float>?>? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
class Step(
|
class Step(
|
||||||
@StringRes val name: Int,
|
@StringRes val name: Int,
|
||||||
val substeps: ImmutableList<SubStep>,
|
val subSteps: ImmutableList<SubStep>,
|
||||||
val state: State = State.WAITING
|
val state: State = State.WAITING
|
||||||
)
|
)
|
||||||
|
|
||||||
class PatcherProgressManager(context: Context, selectedPatches: List<String>) {
|
class PatcherProgressManager(context: Context, selectedPatches: List<String>, selectedApp: SelectedApp, downloadProgress: StateFlow<Pair<Float, Float>?>) {
|
||||||
val steps = generateSteps(context, selectedPatches)
|
val steps = generateSteps(context, selectedPatches, selectedApp, downloadProgress)
|
||||||
private var currentStep: StepKey? = StepKey(0, 0)
|
private var currentStep: StepKey? = StepKey(0, 0)
|
||||||
|
|
||||||
private fun update(key: StepKey, state: State, message: String? = null) {
|
private fun update(key: StepKey, state: State, message: String? = null) {
|
||||||
val isLastSubStep: Boolean
|
val isLastSubStep: Boolean
|
||||||
steps[key.step] = steps[key.step].let { step ->
|
steps[key.step] = steps[key.step].let { step ->
|
||||||
isLastSubStep = key.substep == step.substeps.lastIndex
|
isLastSubStep = key.substep == step.subSteps.lastIndex
|
||||||
|
|
||||||
val newStepState = when {
|
val newStepState = when {
|
||||||
// This step failed because one of its sub-steps failed.
|
// This step failed because one of its sub-steps failed.
|
||||||
@ -51,7 +55,7 @@ class PatcherProgressManager(context: Context, selectedPatches: List<String>) {
|
|||||||
else -> step.state
|
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)
|
if (index != key.substep) subStep else SubStep(subStep.name, state, message)
|
||||||
}.toImmutableList(), newStepState)
|
}.toImmutableList(), newStepState)
|
||||||
}
|
}
|
||||||
@ -100,10 +104,11 @@ class PatcherProgressManager(context: Context, selectedPatches: List<String>) {
|
|||||||
* A map of [Progress] to the corresponding position in [steps]
|
* A map of [Progress] to the corresponding position in [steps]
|
||||||
*/
|
*/
|
||||||
private val stepKeyMap = mapOf(
|
private val stepKeyMap = mapOf(
|
||||||
Progress.Unpacking to StepKey(0, 1),
|
//Progress.Downloading to StepKey(0, 1),
|
||||||
Progress.Merging to StepKey(0, 2),
|
//Progress.Unpacking to StepKey(0, 2),
|
||||||
|
//Progress.Merging to StepKey(0, 3),
|
||||||
Progress.PatchingStart to StepKey(1, 0),
|
Progress.PatchingStart to StepKey(1, 0),
|
||||||
Progress.Saving to StepKey(2, 0),
|
//Progress.Saving to StepKey(2, 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun generatePatchesStep(selectedPatches: List<String>) = Step(
|
private fun generatePatchesStep(selectedPatches: List<String>) = Step(
|
||||||
@ -111,14 +116,15 @@ class PatcherProgressManager(context: Context, selectedPatches: List<String>) {
|
|||||||
selectedPatches.map { SubStep(it) }.toImmutableList()
|
selectedPatches.map { SubStep(it) }.toImmutableList()
|
||||||
)
|
)
|
||||||
|
|
||||||
fun generateSteps(context: Context, selectedPatches: List<String>) = mutableListOf(
|
fun generateSteps(context: Context, selectedPatches: List<String>, selectedApp: SelectedApp, downloadProgress: StateFlow<Pair<Float, Float>?>? = null) = mutableListOf(
|
||||||
Step(
|
Step(
|
||||||
R.string.patcher_step_group_prepare,
|
R.string.patcher_step_group_prepare,
|
||||||
persistentListOf(
|
listOfNotNull(
|
||||||
SubStep(context.getString(R.string.patcher_step_load_patches)),
|
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_unpack)),
|
||||||
SubStep(context.getString(R.string.patcher_step_integrations))
|
SubStep(context.getString(R.string.patcher_step_integrations))
|
||||||
)
|
).toImmutableList()
|
||||||
),
|
),
|
||||||
generatePatchesStep(selectedPatches),
|
generatePatchesStep(selectedPatches),
|
||||||
Step(
|
Step(
|
||||||
|
@ -14,12 +14,16 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.work.ForegroundInfo
|
import androidx.work.ForegroundInfo
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import app.revanced.manager.R
|
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.repository.SourceRepository
|
||||||
import app.revanced.manager.domain.worker.Worker
|
import app.revanced.manager.domain.worker.Worker
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
import app.revanced.manager.domain.worker.WorkerRepository
|
||||||
import app.revanced.manager.patcher.Session
|
import app.revanced.manager.patcher.Session
|
||||||
import app.revanced.manager.patcher.aapt.Aapt
|
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.Options
|
||||||
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.PatchesSelection
|
import app.revanced.manager.util.PatchesSelection
|
||||||
import app.revanced.manager.util.tag
|
import app.revanced.manager.util.tag
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.options
|
import app.revanced.patcher.extensions.PatchExtensions.options
|
||||||
@ -34,14 +38,19 @@ import org.koin.core.component.inject
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
class PatcherWorker(context: Context, parameters: WorkerParameters) :
|
class PatcherWorker(
|
||||||
Worker<PatcherWorker.Args>(context, parameters),
|
context: Context,
|
||||||
KoinComponent {
|
parameters: WorkerParameters
|
||||||
|
) : Worker<PatcherWorker.Args>(context, parameters), KoinComponent {
|
||||||
|
|
||||||
private val sourceRepository: SourceRepository by inject()
|
private val sourceRepository: SourceRepository by inject()
|
||||||
private val workerRepository: WorkerRepository 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(
|
data class Args(
|
||||||
val input: String,
|
val input: SelectedApp,
|
||||||
val output: String,
|
val output: String,
|
||||||
val selectedPatches: PatchesSelection,
|
val selectedPatches: PatchesSelection,
|
||||||
val options: Options,
|
val options: Options,
|
||||||
@ -118,8 +127,10 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
|
|||||||
val bundles = sourceRepository.bundles.first()
|
val bundles = sourceRepository.bundles.first()
|
||||||
val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations }
|
val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations }
|
||||||
|
|
||||||
|
val downloadProgress = MutableStateFlow<Pair<Float, Float>?>(null)
|
||||||
|
|
||||||
val progressManager =
|
val progressManager =
|
||||||
PatcherProgressManager(applicationContext, args.selectedPatches.flatMap { it.value })
|
PatcherProgressManager(applicationContext, args.selectedPatches.flatMap { it.value }, args.input, downloadProgress)
|
||||||
|
|
||||||
val progressFlow = args.progress
|
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.
|
// Ensure they are in the correct order so we can track progress properly.
|
||||||
progressManager.replacePatchesList(patches.map { it.patchName })
|
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)
|
updateProgress(Progress.Unpacking)
|
||||||
|
|
||||||
Session(
|
Session(
|
||||||
@ -162,10 +195,9 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
|
|||||||
frameworkPath,
|
frameworkPath,
|
||||||
aaptPath,
|
aaptPath,
|
||||||
args.logger,
|
args.logger,
|
||||||
File(args.input)
|
inputFile,
|
||||||
) {
|
onProgress = { updateProgress(it) }
|
||||||
updateProgress(it)
|
).use { session ->
|
||||||
}.use { session ->
|
|
||||||
session.run(File(args.output), patches, integrations)
|
session.run(File(args.output), patches, integrations)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +205,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
|
|||||||
progressManager.success()
|
progressManager.success()
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(tag, "Got exception while patching".logFmt(), e)
|
Log.e(tag, "Exception while patching".logFmt(), e)
|
||||||
progressManager.failure(e)
|
progressManager.failure(e)
|
||||||
Result.failure()
|
Result.failure()
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -1,39 +1,49 @@
|
|||||||
package app.revanced.manager.ui.component
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
import androidx.compose.foundation.Image
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.Android
|
import androidx.compose.material.icons.filled.Android
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
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.Modifier
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import app.revanced.manager.util.AppInfo
|
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import com.google.accompanist.placeholder.placeholder
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppIcon(
|
fun AppIcon(
|
||||||
app: AppInfo,
|
packageInfo: PackageInfo?,
|
||||||
contentDescription: String?,
|
contentDescription: String?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
if (app.packageInfo == null) {
|
var showPlaceHolder by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
|
if (packageInfo == null) {
|
||||||
val image = rememberVectorPainter(Icons.Default.Android)
|
val image = rememberVectorPainter(Icons.Default.Android)
|
||||||
val colorFilter = ColorFilter.tint(LocalContentColor.current)
|
val colorFilter = ColorFilter.tint(LocalContentColor.current)
|
||||||
|
|
||||||
Image(
|
Image(
|
||||||
image,
|
image,
|
||||||
contentDescription,
|
contentDescription,
|
||||||
Modifier.size(36.dp).then(modifier),
|
Modifier.placeholder(visible = showPlaceHolder, color = MaterialTheme.colorScheme.inverseOnSurface, shape = RoundedCornerShape(100)).then(modifier),
|
||||||
colorFilter = colorFilter
|
colorFilter = colorFilter
|
||||||
)
|
)
|
||||||
|
|
||||||
|
showPlaceHolder = false
|
||||||
} else {
|
} else {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
app.packageInfo,
|
packageInfo,
|
||||||
contentDescription,
|
contentDescription,
|
||||||
Modifier.size(36.dp).then(modifier)
|
Modifier.placeholder(visible = showPlaceHolder, color = MaterialTheme.colorScheme.inverseOnSurface, shape = RoundedCornerShape(100)).then(modifier),
|
||||||
|
onSuccess = { showPlaceHolder = false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
package app.revanced.manager.ui.destination
|
package app.revanced.manager.ui.destination
|
||||||
|
|
||||||
import android.os.Parcelable
|
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.Options
|
||||||
import app.revanced.manager.util.PatchesSelection
|
import app.revanced.manager.util.PatchesSelection
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
@ -19,11 +19,11 @@ sealed interface Destination : Parcelable {
|
|||||||
object Settings : Destination
|
object Settings : Destination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AppDownloader(val app: AppInfo) : Destination
|
data class VersionSelector(val packageName: String) : Destination
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class PatchesSelector(val input: AppInfo) : Destination
|
data class PatchesSelector(val selectedApp: SelectedApp) : Destination
|
||||||
|
|
||||||
@Parcelize
|
@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
|
||||||
}
|
}
|
@ -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()
|
||||||
|
}
|
@ -26,19 +26,20 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.AppIcon
|
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.AppTopBar
|
||||||
import app.revanced.manager.ui.component.LoadingIndicator
|
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.ui.viewmodel.AppSelectorViewModel
|
||||||
import app.revanced.manager.util.APK_MIMETYPE
|
import app.revanced.manager.util.APK_MIMETYPE
|
||||||
import app.revanced.manager.util.AppInfo
|
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppSelectorScreen(
|
fun AppSelectorScreen(
|
||||||
onAppClick: (AppInfo) -> Unit,
|
onAppClick: (packageName: String) -> Unit,
|
||||||
onDownloaderClick: (AppInfo) -> Unit,
|
onStorageClick: (SelectedApp.Local) -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: AppSelectorViewModel = getViewModel()
|
vm: AppSelectorViewModel = getViewModel()
|
||||||
) {
|
) {
|
||||||
@ -47,7 +48,7 @@ fun AppSelectorScreen(
|
|||||||
val pickApkLauncher =
|
val pickApkLauncher =
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||||
uri?.let { apkUri ->
|
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
|
// TODO: find something better for this
|
||||||
if (search) {
|
if (search) {
|
||||||
SearchBar(
|
SearchBar(
|
||||||
@ -104,13 +94,11 @@ fun AppSelectorScreen(
|
|||||||
) { app ->
|
) { app ->
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable { onAppClick(app.packageName) },
|
||||||
app.packageInfo?.let { onAppClick(app) }
|
leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) },
|
||||||
},
|
headlineContent = { AppLabel(app.packageInfo) },
|
||||||
leadingContent = { AppIcon(app, null) },
|
|
||||||
headlineContent = { Text(vm.loadLabel(app.packageInfo)) },
|
|
||||||
supportingContent = { Text(app.packageName) },
|
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 ->
|
) { app ->
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable { selectedApp = app },
|
modifier = Modifier.clickable { onAppClick(app.packageName) },
|
||||||
leadingContent = { AppIcon(app, null) },
|
leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) },
|
||||||
headlineContent = { Text(vm.loadLabel(app.packageInfo)) },
|
headlineContent = { AppLabel(app.packageInfo) },
|
||||||
supportingContent = { Text(app.packageName) },
|
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)) } }
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -183,50 +171,3 @@ 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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
@ -1,5 +1,6 @@
|
|||||||
package app.revanced.manager.ui.screen
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
@ -49,6 +50,8 @@ fun InstallerScreen(
|
|||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: InstallerViewModel
|
vm: InstallerViewModel
|
||||||
) {
|
) {
|
||||||
|
BackHandler(onBack = onBackClick)
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val exportApkLauncher =
|
val exportApkLauncher =
|
||||||
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export)
|
rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export)
|
||||||
@ -142,7 +145,7 @@ fun InstallStep(step: Step) {
|
|||||||
.padding(start = 16.dp, end = 16.dp)
|
.padding(start = 16.dp, end = 16.dp)
|
||||||
.background(if (expanded) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
|
.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)
|
Text(text = stringResource(step.name), style = MaterialTheme.typography.titleMedium)
|
||||||
|
|
||||||
@ -162,18 +165,19 @@ fun InstallStep(step: Step) {
|
|||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.padding(start = 4.dp)
|
.padding(start = 4.dp)
|
||||||
) {
|
) {
|
||||||
step.substeps.forEach {
|
step.subSteps.forEach { subStep ->
|
||||||
var messageExpanded by rememberSaveable { mutableStateOf(true) }
|
var messageExpanded by rememberSaveable { mutableStateOf(true) }
|
||||||
val stacktrace = it.message
|
val stacktrace = subStep.message
|
||||||
|
val downloadProgress = subStep.progress?.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
StepIcon(it.state, size = 18.dp)
|
StepIcon(subStep.state, downloadProgress?.value, size = 18.dp)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = it.name,
|
text = subStep.name,
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
@ -184,6 +188,13 @@ fun InstallStep(step: Step) {
|
|||||||
ArrowButton(expanded = messageExpanded) {
|
ArrowButton(expanded = messageExpanded) {
|
||||||
messageExpanded = !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
|
@Composable
|
||||||
fun StepIcon(status: State, size: Dp) {
|
fun StepIcon(status: State, downloadProgress: Pair<Float, Float>? = null, size: Dp) {
|
||||||
val strokeWidth = Dp(floor(size.value / 10) + 1)
|
val strokeWidth = Dp(floor(size.value / 10) + 1)
|
||||||
|
|
||||||
when (status) {
|
when (status) {
|
||||||
@ -219,7 +230,20 @@ fun StepIcon(status: State, size: Dp) {
|
|||||||
modifier = Modifier.size(size)
|
modifier = Modifier.size(size)
|
||||||
)
|
)
|
||||||
|
|
||||||
State.WAITING -> CircularProgressIndicator(
|
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,
|
strokeWidth = strokeWidth,
|
||||||
modifier = stringResource(R.string.step_running).let { description ->
|
modifier = stringResource(R.string.step_running).let { description ->
|
||||||
Modifier
|
Modifier
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package app.revanced.manager.ui.screen
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@ -66,8 +65,6 @@ fun PatchesSelectorScreen(
|
|||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: PatchesSelectorViewModel
|
vm: PatchesSelectorViewModel
|
||||||
) {
|
) {
|
||||||
BackHandler(onBack = onBackClick)
|
|
||||||
|
|
||||||
val pagerState = rememberPagerState()
|
val pagerState = rememberPagerState()
|
||||||
val composableScope = rememberCoroutineScope()
|
val composableScope = rememberCoroutineScope()
|
||||||
|
|
||||||
@ -75,7 +72,7 @@ fun PatchesSelectorScreen(
|
|||||||
|
|
||||||
if (vm.compatibleVersions.isNotEmpty())
|
if (vm.compatibleVersions.isNotEmpty())
|
||||||
UnsupportedDialog(
|
UnsupportedDialog(
|
||||||
appVersion = vm.appInfo.packageInfo!!.versionName,
|
appVersion = vm.selectedApp.version,
|
||||||
supportedVersions = vm.compatibleVersions,
|
supportedVersions = vm.compatibleVersions,
|
||||||
onDismissRequest = vm::dismissDialogs
|
onDismissRequest = vm::dismissDialogs
|
||||||
)
|
)
|
||||||
|
@ -18,7 +18,6 @@ import androidx.compose.material3.ListItem
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.SideEffect
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@ -31,33 +30,28 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.LoadingIndicator
|
import app.revanced.manager.ui.component.LoadingIndicator
|
||||||
import app.revanced.manager.ui.viewmodel.AppDownloaderViewModel
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.util.AppInfo
|
import app.revanced.manager.ui.viewmodel.VersionSelectorViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppDownloaderScreen(
|
fun VersionSelectorScreen(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onApkClick: (AppInfo) -> Unit,
|
onAppClick: (SelectedApp) -> Unit,
|
||||||
viewModel: AppDownloaderViewModel
|
viewModel: VersionSelectorViewModel
|
||||||
) {
|
) {
|
||||||
SideEffect {
|
val supportedVersions by viewModel.supportedVersions.collectAsStateWithLifecycle(emptyMap())
|
||||||
viewModel.onComplete = onApkClick
|
|
||||||
}
|
|
||||||
|
|
||||||
val downloadProgress by viewModel.appDownloader.downloadProgress.collectAsStateWithLifecycle()
|
|
||||||
val compatibleVersions by viewModel.compatibleVersions.collectAsStateWithLifecycle(emptyMap())
|
|
||||||
val downloadedVersions by viewModel.downloadedVersions.collectAsStateWithLifecycle(emptyList())
|
val downloadedVersions by viewModel.downloadedVersions.collectAsStateWithLifecycle(emptyList())
|
||||||
|
|
||||||
val list by remember {
|
val list by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
(downloadedVersions + viewModel.availableVersions)
|
(downloadedVersions + viewModel.downloadableVersions)
|
||||||
.distinct()
|
.distinctBy { it.version }
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareByDescending<String> {
|
compareByDescending<SelectedApp> {
|
||||||
downloadedVersions.contains(it)
|
it is SelectedApp.Local
|
||||||
}.thenByDescending { compatibleVersions[it] }
|
}.thenByDescending { supportedVersions[it.version] }
|
||||||
.thenByDescending { it }
|
.thenByDescending { it.version }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,18 +86,15 @@ fun AppDownloaderScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
list.forEach { version ->
|
list.forEach { selectedApp ->
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable { onAppClick(selectedApp) },
|
||||||
viewModel.downloadApp(version)
|
headlineContent = { Text(selectedApp.version) },
|
||||||
},
|
|
||||||
headlineContent = { Text(version) },
|
|
||||||
supportingContent =
|
supportingContent =
|
||||||
if (downloadedVersions.contains(version)) {
|
if (selectedApp is SelectedApp.Local) {
|
||||||
{ Text(stringResource(R.string.already_downloaded)) }
|
{ Text(stringResource(R.string.already_downloaded)) }
|
||||||
} else null,
|
} else null,
|
||||||
trailingContent = compatibleVersions[version]?.let {
|
trailingContent = supportedVersions[selectedApp.version]?.let { {
|
||||||
{
|
|
||||||
Text(
|
Text(
|
||||||
pluralStringResource(
|
pluralStringResource(
|
||||||
R.plurals.patches_count,
|
R.plurals.patches_count,
|
||||||
@ -140,10 +131,7 @@ fun AppDownloaderScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
LoadingIndicator(
|
LoadingIndicator()
|
||||||
progress = downloadProgress?.let { (it.first / it.second) },
|
|
||||||
text = downloadProgress?.let { stringResource(R.string.downloading_app, it.first, it.second) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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<String>()
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,13 +4,14 @@ import android.app.Application
|
|||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
|
|
||||||
class AppSelectorViewModel(
|
class AppSelectorViewModel(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val pm: PM
|
pm: PM
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val packageManager = app.packageManager
|
private val packageManager = app.packageManager
|
||||||
|
|
||||||
@ -18,11 +19,17 @@ class AppSelectorViewModel(
|
|||||||
|
|
||||||
fun loadLabel(app: PackageInfo?) = (app?.applicationInfo?.loadLabel(packageManager) ?: "Not installed").toString()
|
fun loadLabel(app: PackageInfo?) = (app?.applicationInfo?.loadLabel(packageManager) ?: "Not installed").toString()
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
fun loadSelectedFile(uri: Uri) =
|
fun loadSelectedFile(uri: Uri) =
|
||||||
app.contentResolver.openInputStream(uri)!!.use { stream ->
|
app.contentResolver.openInputStream(uri)?.use { stream ->
|
||||||
File(app.cacheDir, "input.apk").also {
|
File(app.cacheDir, "input.apk").also {
|
||||||
if (it.exists()) it.delete()
|
it.delete()
|
||||||
Files.copy(stream, it.toPath())
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -51,7 +51,7 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
|
|||||||
private val pm: PM by inject()
|
private val pm: PM by inject()
|
||||||
private val workerRepository: WorkerRepository 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 outputFile = File(app.cacheDir, "output.apk")
|
||||||
private val signedFile = File(app.cacheDir, "signed.apk").also { if (it.exists()) it.delete() }
|
private val signedFile = File(app.cacheDir, "signed.apk").also { if (it.exists()) it.delete() }
|
||||||
private var hasSigned = false
|
private var hasSigned = false
|
||||||
@ -69,21 +69,22 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
|
|||||||
private val logger = ManagerLogger()
|
private val logger = ManagerLogger()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val (appInfo, patches, options) = input
|
val (selectedApp, patches, options) = input
|
||||||
|
|
||||||
_progress = MutableStateFlow(PatcherProgressManager.generateSteps(
|
_progress = MutableStateFlow(PatcherProgressManager.generateSteps(
|
||||||
app,
|
app,
|
||||||
patches.flatMap { (_, selected) -> selected }
|
patches.flatMap { (_, selected) -> selected },
|
||||||
|
input.selectedApp
|
||||||
).toImmutableList())
|
).toImmutableList())
|
||||||
patcherWorkerId =
|
patcherWorkerId =
|
||||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||||
"patching", PatcherWorker.Args(
|
"patching", PatcherWorker.Args(
|
||||||
appInfo.path!!.absolutePath,
|
selectedApp,
|
||||||
outputFile.path,
|
outputFile.path,
|
||||||
patches,
|
patches,
|
||||||
options,
|
options,
|
||||||
packageName,
|
packageName,
|
||||||
appInfo.packageInfo!!.versionName,
|
selectedApp.version,
|
||||||
_progress,
|
_progress,
|
||||||
logger
|
logger
|
||||||
)
|
)
|
||||||
|
@ -3,25 +3,16 @@ package app.revanced.manager.ui.viewmodel
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.domain.repository.SourceRepository
|
import app.revanced.manager.domain.repository.SourceRepository
|
||||||
import app.revanced.manager.util.PM
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MainViewModel(
|
class MainViewModel(
|
||||||
sourceRepository: SourceRepository,
|
sourceRepository: SourceRepository
|
||||||
pm: PM
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
init {
|
init {
|
||||||
with(viewModelScope) {
|
with(viewModelScope) {
|
||||||
launch {
|
launch {
|
||||||
sourceRepository.loadSources()
|
sourceRepository.loadSources()
|
||||||
}
|
}
|
||||||
launch {
|
|
||||||
pm.getCompatibleApps()
|
|
||||||
}
|
|
||||||
launch(Dispatchers.IO) {
|
|
||||||
pm.getInstalledApps()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -17,7 +17,7 @@ import app.revanced.manager.domain.manager.PreferencesManager
|
|||||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||||
import app.revanced.manager.domain.repository.SourceRepository
|
import app.revanced.manager.domain.repository.SourceRepository
|
||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
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.Options
|
||||||
import app.revanced.manager.util.PatchesSelection
|
import app.revanced.manager.util.PatchesSelection
|
||||||
import app.revanced.manager.util.SnapshotStateSet
|
import app.revanced.manager.util.SnapshotStateSet
|
||||||
@ -37,7 +37,7 @@ import org.koin.core.component.get
|
|||||||
@Stable
|
@Stable
|
||||||
@OptIn(SavedStateHandleSaveableApi::class)
|
@OptIn(SavedStateHandleSaveableApi::class)
|
||||||
class PatchesSelectorViewModel(
|
class PatchesSelectorViewModel(
|
||||||
val appInfo: AppInfo
|
val selectedApp: SelectedApp
|
||||||
) : ViewModel(), KoinComponent {
|
) : ViewModel(), KoinComponent {
|
||||||
private val selectionRepository: PatchSelectionRepository = get()
|
private val selectionRepository: PatchSelectionRepository = get()
|
||||||
private val savedStateHandle: SavedStateHandle = get()
|
private val savedStateHandle: SavedStateHandle = get()
|
||||||
@ -52,12 +52,12 @@ class PatchesSelectorViewModel(
|
|||||||
val unsupported = mutableListOf<PatchInfo>()
|
val unsupported = mutableListOf<PatchInfo>()
|
||||||
val universal = mutableListOf<PatchInfo>()
|
val universal = mutableListOf<PatchInfo>()
|
||||||
|
|
||||||
bundle.patches.filter { it.compatibleWith(appInfo.packageName) }.forEach {
|
bundle.patches.filter { it.compatibleWith(selectedApp.packageName) }.forEach {
|
||||||
val targetList =
|
val targetList =
|
||||||
if (it.compatiblePackages == null) universal else if (it.supportsVersion(
|
if (it.compatiblePackages == null) universal else if (it.supportsVersion(selectedApp.version))
|
||||||
appInfo.packageInfo!!.versionName
|
supported
|
||||||
)
|
else
|
||||||
) supported else unsupported
|
unsupported
|
||||||
|
|
||||||
targetList.add(it)
|
targetList.add(it)
|
||||||
}
|
}
|
||||||
@ -71,7 +71,7 @@ class PatchesSelectorViewModel(
|
|||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
val bundles = bundlesFlow.first()
|
val bundles = bundlesFlow.first()
|
||||||
val filteredSelection =
|
val filteredSelection =
|
||||||
selectionRepository.getSelection(appInfo.packageName).mapValues { (uid, patches) ->
|
selectionRepository.getSelection(selectedApp.packageName).mapValues { (uid, patches) ->
|
||||||
// Filter out patches that don't exist.
|
// Filter out patches that don't exist.
|
||||||
val filteredPatches = bundles.singleOrNull { it.uid == uid }
|
val filteredPatches = bundles.singleOrNull { it.uid == uid }
|
||||||
?.let { bundle ->
|
?.let { bundle ->
|
||||||
@ -117,7 +117,7 @@ class PatchesSelectorViewModel(
|
|||||||
suspend fun getAndSaveSelection(): PatchesSelection =
|
suspend fun getAndSaveSelection(): PatchesSelection =
|
||||||
selectedPatches.also {
|
selectedPatches.also {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
selectionRepository.updateSelection(appInfo.packageName, it)
|
selectionRepository.updateSelection(selectedApp.packageName, it)
|
||||||
}
|
}
|
||||||
}.mapValues { it.value.toMutableSet() }.apply {
|
}.mapValues { it.value.toMutableSet() }.apply {
|
||||||
if (allowExperimental.get()) {
|
if (allowExperimental.get()) {
|
||||||
@ -150,7 +150,7 @@ class PatchesSelectorViewModel(
|
|||||||
val set = HashSet<String>()
|
val set = HashSet<String>()
|
||||||
|
|
||||||
unsupportedVersions.forEach { patch ->
|
unsupportedVersions.forEach { patch ->
|
||||||
patch.compatiblePackages?.find { it.name == appInfo.packageName }
|
patch.compatiblePackages?.find { it.packageName == selectedApp.packageName }
|
||||||
?.let { compatiblePackage ->
|
?.let { compatiblePackage ->
|
||||||
set.addAll(compatiblePackage.versions)
|
set.addAll(compatiblePackage.versions)
|
||||||
}
|
}
|
||||||
|
@ -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<SelectedApp.Download>()
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,16 +9,18 @@ import android.content.pm.PackageInfo
|
|||||||
import android.content.pm.PackageInstaller
|
import android.content.pm.PackageInstaller
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||||
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import app.revanced.manager.domain.repository.SourceRepository
|
import app.revanced.manager.domain.repository.SourceRepository
|
||||||
import app.revanced.manager.service.InstallService
|
import app.revanced.manager.service.InstallService
|
||||||
import app.revanced.manager.service.UninstallService
|
import app.revanced.manager.service.UninstallService
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -29,7 +31,7 @@ private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readab
|
|||||||
@Parcelize
|
@Parcelize
|
||||||
data class AppInfo(
|
data class AppInfo(
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val patches: Int,
|
val patches: Int?,
|
||||||
val packageInfo: PackageInfo?,
|
val packageInfo: PackageInfo?,
|
||||||
val path: File? = null
|
val path: File? = null
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
@ -38,68 +40,64 @@ data class AppInfo(
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
class PM(
|
class PM(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val sourceRepository: SourceRepository
|
sourceRepository: SourceRepository
|
||||||
) {
|
) {
|
||||||
private val installedApps = MutableStateFlow(emptyList<AppInfo>())
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
private val compatibleApps = MutableStateFlow(emptyList<AppInfo>())
|
|
||||||
|
|
||||||
val appList: Flow<List<AppInfo>> = compatibleApps.combine(installedApps) { compatibleApps, installedApps ->
|
val appList = sourceRepository.bundles.map { bundles ->
|
||||||
if (compatibleApps.isNotEmpty()) {
|
val compatibleApps = scope.async {
|
||||||
(compatibleApps + installedApps)
|
val compatiblePackages = bundles.values
|
||||||
.distinctBy { it.packageName }
|
.flatMap { it.patches }
|
||||||
.sortedWith(
|
.flatMap { it.compatiblePackages.orEmpty() }
|
||||||
compareByDescending<AppInfo> {
|
.groupingBy { it.packageName }
|
||||||
it.patches
|
.eachCount()
|
||||||
}.thenBy { it.packageInfo?.applicationInfo?.loadLabel(app.packageManager).toString() }.thenBy { it.packageName }
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getCompatibleApps() {
|
|
||||||
sourceRepository.bundles.collect { bundles ->
|
|
||||||
val compatiblePackages = HashMap<String, Int>()
|
|
||||||
|
|
||||||
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 ->
|
compatiblePackages.keys.map { pkg ->
|
||||||
try {
|
try {
|
||||||
val packageInfo = app.packageManager.getPackageInfo(pkg, 0)
|
val packageInfo = app.packageManager.getPackageInfo(pkg, 0)
|
||||||
AppInfo(
|
AppInfo(
|
||||||
pkg,
|
pkg,
|
||||||
compatiblePackages[pkg] ?: 0,
|
compatiblePackages[pkg],
|
||||||
packageInfo,
|
packageInfo,
|
||||||
File(packageInfo.applicationInfo.sourceDir)
|
File(packageInfo.applicationInfo.sourceDir)
|
||||||
)
|
)
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
} catch (e: NameNotFoundException) {
|
||||||
AppInfo(
|
AppInfo(
|
||||||
pkg,
|
pkg,
|
||||||
compatiblePackages[pkg] ?: 0,
|
compatiblePackages[pkg],
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getInstalledApps() {
|
val installedApps = scope.async {
|
||||||
installedApps.emit(app.packageManager.getInstalledPackages(MATCH_UNINSTALLED_PACKAGES).map { packageInfo ->
|
app.packageManager.getInstalledPackages(MATCH_UNINSTALLED_PACKAGES).map { packageInfo ->
|
||||||
AppInfo(
|
AppInfo(
|
||||||
packageInfo.packageName,
|
packageInfo.packageName,
|
||||||
0,
|
0,
|
||||||
packageInfo,
|
packageInfo,
|
||||||
File(packageInfo.applicationInfo.sourceDir)
|
File(packageInfo.applicationInfo.sourceDir)
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compatibleApps.await().isNotEmpty()) {
|
||||||
|
(compatibleApps.await() + installedApps.await())
|
||||||
|
.distinctBy { it.packageName }
|
||||||
|
.sortedWith(
|
||||||
|
compareByDescending<AppInfo> {
|
||||||
|
it.patches
|
||||||
|
}.thenBy { it.packageInfo?.applicationInfo?.loadLabel(app.packageManager).toString() }.thenBy { it.packageName }
|
||||||
|
)
|
||||||
|
} else { emptyList() }
|
||||||
|
}.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
|
fun getPackageInfo(packageName: String): PackageInfo? =
|
||||||
|
try {
|
||||||
|
app.packageManager.getPackageInfo(packageName, 0)
|
||||||
|
} catch (e: NameNotFoundException) {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
|
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
|
||||||
@ -119,15 +117,6 @@ class PM(
|
|||||||
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
app.startActivity(it)
|
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) {
|
private fun PackageInstaller.Session.writeApk(apk: File) {
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
package app.revanced.manager.util
|
package app.revanced.manager.util
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.StringRes
|
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) {
|
fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
|
||||||
Toast.makeText(this, string, duration).show()
|
Toast.makeText(this, string, duration).show()
|
||||||
}
|
}
|
||||||
|
@ -128,10 +128,10 @@
|
|||||||
<string name="download_application">Download application?</string>
|
<string name="download_application">Download application?</string>
|
||||||
<string name="app_not_installed">The app you selected isn\'t installed. Do you want to download it?</string>
|
<string name="app_not_installed">The app you selected isn\'t installed. Do you want to download it?</string>
|
||||||
<string name="failed_to_load_apk">Failed to load apk</string>
|
<string name="failed_to_load_apk">Failed to load apk</string>
|
||||||
|
<string name="loading">Loading…</string>
|
||||||
|
<string name="not_installed">Not installed</string>
|
||||||
|
|
||||||
<string name="error_occurred">An error occurred</string>
|
|
||||||
<string name="already_downloaded">Already downloaded</string>
|
<string name="already_downloaded">Already downloaded</string>
|
||||||
<string name="downloading_app">Downloading app… (%1$s MB/%2$s MB)</string>
|
|
||||||
|
|
||||||
<string name="select_file">Select file</string>
|
<string name="select_file">Select file</string>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user