refactor: PackageManager (#31)

* refactor: refactor `PM`

* feat: use plurals for patch count

* fix: support apk's from storage

* feat: use ViewModel for loading apps and bundles

* fix: fix file selector that has no reason to be broken

* refactor: rename parameter

* refactor: `MainViewModel`

* feat: make all apps use `path`

* build: target java 11
This commit is contained in:
Robert 2023-06-03 18:03:14 +00:00 committed by GitHub
parent a6b86691db
commit cd0144b563
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 317 additions and 281 deletions

View File

@ -27,12 +27,13 @@ android {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
packagingOptions {
packaging {
resources {
excludes += "/prebuilt/**"
}
@ -51,6 +52,10 @@ android {
composeOptions.kotlinCompilerExtensionVersion = "1.4.7"
}
kotlin {
jvmToolchain(11)
}
dependencies {
// AndroidX Core
@ -58,7 +63,7 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.activity:activity-compose:1.7.1")
implementation("androidx.activity:activity-compose:1.7.2")
implementation("androidx.paging:paging-common-ktx:3.1.1")
implementation("androidx.work:work-runtime-ktx:2.8.1")
@ -78,10 +83,12 @@ dependencies {
//implementation("com.google.accompanist:accompanist-permissions:$accompanistVersion")
// Coil (async image loading, network image)
implementation("io.coil-kt:coil-compose:2.3.0")
implementation("io.coil-kt:coil-compose:2.4.0")
implementation("me.zhanghai.android.appiconloader:appiconloader-coil:1.5.0")
// KotlinX
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5")
// Room
val roomVersion = "2.5.1"
@ -94,7 +101,7 @@ dependencies {
implementation("app.revanced:revanced-patcher:9.0.0")
// Signing
implementation("com.android.tools.build:apksig:8.1.0-beta02")
implementation("com.android.tools.build:apksig:8.2.0-alpha05")
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
// Koin

View File

@ -28,6 +28,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ReVancedManager"
android:enableOnBackInvokedCallback="true"
tools:targetApi="33">
<activity

View File

@ -7,24 +7,32 @@ import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import app.revanced.manager.compose.domain.manager.PreferencesManager
import app.revanced.manager.compose.domain.repository.BundleRepository
import app.revanced.manager.compose.ui.destination.Destination
import app.revanced.manager.compose.ui.screen.*
import app.revanced.manager.compose.ui.screen.AppSelectorScreen
import app.revanced.manager.compose.ui.screen.DashboardScreen
import app.revanced.manager.compose.ui.screen.InstallerScreen
import app.revanced.manager.compose.ui.screen.PatchesSelectorScreen
import app.revanced.manager.compose.ui.screen.SettingsScreen
import app.revanced.manager.compose.ui.theme.ReVancedManagerTheme
import app.revanced.manager.compose.ui.theme.Theme
import app.revanced.manager.compose.util.PM
import dev.olshevski.navigation.reimagined.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.androidx.compose.getViewModel
import app.revanced.manager.compose.ui.viewmodel.MainViewModel
import coil.Coil
import coil.ImageLoader
import dev.olshevski.navigation.reimagined.AnimatedNavHost
import dev.olshevski.navigation.reimagined.NavBackHandler
import dev.olshevski.navigation.reimagined.navigate
import dev.olshevski.navigation.reimagined.pop
import dev.olshevski.navigation.reimagined.popAll
import dev.olshevski.navigation.reimagined.rememberNavController
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.getViewModel
import org.koin.core.parameter.parametersOf
import kotlin.math.roundToInt
class MainActivity : ComponentActivity() {
private val prefs: PreferencesManager by inject()
private val bundleRepository: BundleRepository by inject()
private val mainScope = MainScope()
private val prefs: PreferencesManager = get()
@ExperimentalAnimationApi
override fun onCreate(savedInstanceState: Bundle?) {
@ -32,12 +40,18 @@ class MainActivity : ComponentActivity() {
installSplashScreen()
bundleRepository.onAppStart(this@MainActivity)
getViewModel<MainViewModel>()
val context = this
mainScope.launch(Dispatchers.IO) {
PM.loadApps(context)
}
val scale = this.resources.displayMetrics.density
val pixels = (36 * scale).roundToInt()
Coil.setImageLoader(
ImageLoader.Builder(this)
.components {
add(AppIconKeyer())
add(AppIconFetcher.Factory(pixels, true, this@MainActivity))
}
.build()
)
setContent {
ReVancedManagerTheme(

View File

@ -1,10 +1,11 @@
package app.revanced.manager.compose.di
import app.revanced.manager.compose.domain.repository.SourceRepository
import app.revanced.manager.compose.patcher.SignerService
import app.revanced.manager.compose.util.PM
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val managerModule = module {
singleOf(::SignerService)
singleOf(::PM)
}

View File

@ -2,7 +2,6 @@ package app.revanced.manager.compose.di
import app.revanced.manager.compose.domain.repository.ReVancedRepository
import app.revanced.manager.compose.network.api.ManagerAPI
import app.revanced.manager.compose.domain.repository.BundleRepository
import app.revanced.manager.compose.domain.repository.SourcePersistenceRepository
import app.revanced.manager.compose.domain.repository.SourceRepository
import org.koin.core.module.dsl.singleOf
@ -11,7 +10,6 @@ import org.koin.dsl.module
val repositoryModule = module {
singleOf(::ReVancedRepository)
singleOf(::ManagerAPI)
singleOf(::BundleRepository)
singleOf(::SourcePersistenceRepository)
singleOf(::SourceRepository)
}

View File

@ -5,6 +5,7 @@ import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.dsl.module
val viewModelModule = module {
viewModelOf(::MainViewModel)
viewModelOf(::PatchesSelectorViewModel)
viewModelOf(::SettingsViewModel)
viewModelOf(::AppSelectorViewModel)

View File

@ -1,50 +0,0 @@
package app.revanced.manager.compose.domain.repository
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import app.revanced.manager.compose.patcher.patch.PatchBundle
import app.revanced.manager.compose.util.launchAndRepeatWithViewLifecycle
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
class BundleRepository(private val sourceRepository: SourceRepository) {
/**
* A [Flow] that emits whenever the sources change.
*
* The outer flow emits whenever the sources configuration changes.
* The inner flow emits whenever one of the bundles update.
*/
private val sourceUpdates = sourceRepository.sources.map { sources ->
sources.map { (name, source) ->
source.bundle.map { bundle ->
name to bundle
}
}.merge().buffer()
}
private val _bundles = MutableStateFlow<Map<String, PatchBundle>>(emptyMap())
/**
* A [Flow] that gives you all loaded [PatchBundle]s.
* This is only synced when the app is in the foreground.
*/
val bundles = _bundles.asStateFlow()
fun onAppStart(lifecycleOwner: LifecycleOwner) {
lifecycleOwner.lifecycleScope.launch {
sourceRepository.loadSources()
}
lifecycleOwner.launchAndRepeatWithViewLifecycle {
sourceUpdates.collect { events ->
val map = HashMap<String, PatchBundle>()
_bundles.emit(map)
events.collect { (name, new) ->
map[name] = new
_bundles.emit(map)
}
}
}
}
}

View File

@ -4,14 +4,17 @@ import android.app.Application
import android.util.Log
import app.revanced.manager.compose.data.room.sources.SourceEntity
import app.revanced.manager.compose.data.room.sources.SourceLocation
import app.revanced.manager.compose.domain.sources.RemoteSource
import app.revanced.manager.compose.domain.sources.LocalSource
import app.revanced.manager.compose.domain.sources.RemoteSource
import app.revanced.manager.compose.domain.sources.Source
import app.revanced.manager.compose.util.tag
import io.ktor.http.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import java.io.File
@ -20,6 +23,18 @@ import java.io.InputStream
class SourceRepository(app: Application, private val persistenceRepo: SourcePersistenceRepository) {
private val sourcesDir = app.dataDir.resolve("sources").also { it.mkdirs() }
private val _sources: MutableStateFlow<Map<String, Source>> = MutableStateFlow(emptyMap())
val sources = _sources.asStateFlow()
@OptIn(ExperimentalCoroutinesApi::class)
val bundles = sources.flatMapLatest { sources ->
combine(
sources.map { (_, source) -> source.bundle }
) { bundles ->
sources.keys.zip(bundles).toMap()
}
}
/**
* Get the directory of the [Source] with the specified [uid], creating it if needed.
*/
@ -49,7 +64,7 @@ class SourceRepository(app: Application, private val persistenceRepo: SourcePers
persistenceRepo.clear()
_sources.emit(emptyMap())
sourcesDir.apply {
delete()
deleteRecursively()
mkdirs()
}
@ -58,7 +73,7 @@ class SourceRepository(app: Application, private val persistenceRepo: SourcePers
suspend fun remove(source: Source) = withContext(Dispatchers.Default) {
persistenceRepo.delete(source.id)
directoryOf(source.id).delete()
directoryOf(source.id).deleteRecursively()
_sources.update {
it.filterValues { value ->
@ -84,9 +99,6 @@ class SourceRepository(app: Application, private val persistenceRepo: SourcePers
addSource(name, RemoteSource(id, directoryOf(id)))
}
private val _sources: MutableStateFlow<Map<String, Source>> = MutableStateFlow(emptyMap())
val sources = _sources.asStateFlow()
suspend fun redownloadRemoteSources() =
sources.value.values.filterIsInstance<RemoteSource>().forEach { it.downloadLatest() }
}

View File

@ -18,8 +18,6 @@ class LocalSource(id: Int, directory: File) : Source(id, directory) {
}
}
withContext(Dispatchers.Main) {
_bundle.emit(loadBundle { throw it })
}
_bundle.emit(loadBundle { throw it })
}
}

View File

@ -3,17 +3,15 @@ package app.revanced.manager.compose.domain.sources
import app.revanced.manager.compose.network.api.ManagerAPI
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.component.inject
import org.koin.core.component.get
import java.io.File
class RemoteSource(id: Int, directory: File) : Source(id, directory) {
private val api: ManagerAPI by inject()
private val api: ManagerAPI = get()
suspend fun downloadLatest() = withContext(Dispatchers.IO) {
api.downloadBundle(patchesJar, integrations).also { (patchesVer, integrationsVer) ->
saveVersion(patchesVer, integrationsVer)
withContext(Dispatchers.Main) {
_bundle.emit(loadBundle { err -> throw err })
}
_bundle.emit(loadBundle { err -> throw err })
}
return@withContext

View File

@ -42,5 +42,5 @@ class PatchBundle(private val loader: Iterable<PatchClass>, val integrations: Fi
true
}
fun loadAllPatches() = loader.toList()
private fun loadAllPatches() = loader.toList()
}

View File

@ -4,12 +4,13 @@ import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import app.revanced.manager.compose.domain.repository.BundleRepository
import app.revanced.manager.compose.domain.repository.SourceRepository
import app.revanced.manager.compose.patcher.Session
import app.revanced.manager.compose.patcher.aapt.Aapt
import app.revanced.manager.compose.util.PatchesSelection
import app.revanced.manager.compose.util.tag
import app.revanced.patcher.extensions.PatchExtensions.patchName
import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.koin.core.component.KoinComponent
@ -20,7 +21,7 @@ import java.io.FileNotFoundException
// TODO: setup wakelock + notification so android doesn't murder us.
class PatcherWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters),
KoinComponent {
private val bundleRepository: BundleRepository by inject()
private val sourceRepository: SourceRepository by inject()
@Serializable
data class Args(
@ -50,7 +51,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) : CoroutineW
val args = Json.decodeFromString<Args>(inputData.getString(ARGS_KEY)!!)
val bundles = bundleRepository.bundles.value
val bundles = sourceRepository.bundles.first()
val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations }
val patchList = args.selectedPatches.flatMap { (bundleName, selected) ->

View File

@ -6,6 +6,7 @@ import android.content.pm.PackageInstaller
import android.os.Build
import android.os.IBinder
@Suppress("DEPRECATION")
class InstallService : Service() {
override fun onStartCommand(

View File

@ -6,6 +6,7 @@ import android.content.pm.PackageInstaller
import android.os.Build
import android.os.IBinder
@Suppress("DEPRECATION")
class UninstallService : Service() {
override fun onStartCommand(

View File

@ -1,6 +1,5 @@
package app.revanced.manager.compose.ui.component
import android.graphics.drawable.Drawable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
@ -11,31 +10,30 @@ 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 coil.compose.rememberAsyncImagePainter
import app.revanced.manager.compose.util.AppInfo
import coil.compose.AsyncImage
@Composable
fun AppIcon(
drawable: Drawable?,
app: AppInfo,
contentDescription: String?,
size: Int = 48
modifier: Modifier = Modifier
) {
if (drawable == null) {
if (app.packageInfo == null) {
val image = rememberVectorPainter(Icons.Default.Android)
val colorFilter = ColorFilter.tint(LocalContentColor.current)
Image(
image,
contentDescription,
Modifier.size(size.dp),
Modifier.size(36.dp).then(modifier),
colorFilter = colorFilter
)
} else {
val image = rememberAsyncImagePainter(drawable)
Image(
image,
AsyncImage(
app.packageInfo,
contentDescription,
Modifier.size(size.dp)
Modifier.size(36.dp).then(modifier)
)
}
}

View File

@ -11,10 +11,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.compose.R
@Composable
fun LoadingIndicator(progress: Float? = null, text: Int? = R.string.loading_body) {
fun LoadingIndicator(progress: Float? = null, text: Int? = null) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,

View File

@ -1,7 +1,7 @@
package app.revanced.manager.compose.ui.destination
import android.os.Parcelable
import app.revanced.manager.compose.util.PackageInfo
import app.revanced.manager.compose.util.AppInfo
import app.revanced.manager.compose.util.PatchesSelection
import kotlinx.parcelize.Parcelize
@ -17,8 +17,8 @@ sealed interface Destination : Parcelable {
object Settings : Destination
@Parcelize
data class PatchesSelector(val input: PackageInfo) : Destination
data class PatchesSelector(val input: AppInfo) : Destination
@Parcelize
data class Installer(val input: PackageInfo, val selectedPatches: PatchesSelection) : Destination
data class Installer(val input: AppInfo, val selectedPatches: PatchesSelection) : Destination
}

View File

@ -19,32 +19,44 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.compose.R
import app.revanced.manager.compose.ui.component.AppIcon
import app.revanced.manager.compose.ui.component.AppTopBar
import app.revanced.manager.compose.ui.component.LoadingIndicator
import app.revanced.manager.compose.ui.viewmodel.AppSelectorViewModel
import app.revanced.manager.compose.util.APK_MIMETYPE
import app.revanced.manager.compose.util.PM
import app.revanced.manager.compose.util.PackageInfo
import app.revanced.manager.compose.util.AppInfo
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppSelectorScreen(
onAppClick: (PackageInfo) -> Unit,
onAppClick: (AppInfo) -> Unit,
onBackClick: () -> Unit,
vm: AppSelectorViewModel = getViewModel()
) {
val pickApkLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { apkUri ->
vm.loadSelectedFile(apkUri!!).let(onAppClick)
}
val pickApkLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
it?.let { apkUri -> onAppClick(vm.loadSelectedFile(apkUri)) }
}
var filterText by rememberSaveable { mutableStateOf("") }
var search by rememberSaveable { mutableStateOf(false) }
val appList by vm.appList.collectAsStateWithLifecycle(initialValue = emptyList())
val filteredAppList = rememberSaveable(appList, filterText) {
appList.filter { app ->
(vm.loadLabel(app.packageInfo)).contains(
filterText,
true
) or app.packageName.contains(filterText, true)
}
}
// TODO: find something better for this
if (search) {
SearchBar(
@ -63,34 +75,30 @@ fun AppSelectorScreen(
)
}
},
shape = SearchBarDefaults.inputFieldShape,
content = {
if (PM.appList.isNotEmpty()) {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
if (appList.isNotEmpty()) {
items(
PM.appList
.filter { app ->
(app.label.contains(
filterText,
true
) or app.packageName.contains(filterText, true))
}
items = filteredAppList,
key = { it.packageName }
) { app ->
ListItem(
modifier = Modifier.clickable { onAppClick(PackageInfo(app)) },
leadingContent = { AppIcon(app.icon, null, 36) },
headlineContent = { Text(app.label) },
modifier = Modifier.clickable {
app.packageInfo?.let { onAppClick(app) }
},
leadingContent = { AppIcon(app, null) },
headlineContent = { Text(vm.loadLabel(app.packageInfo)) },
supportingContent = { Text(app.packageName) },
trailingContent = { Text("420 Patches") }
trailingContent = if (app.patches > 0) { { Text(pluralStringResource(R.plurals.patches_count, app.patches, app.patches)) } } else null
)
}
} else {
item { LoadingIndicator() }
}
} else {
LoadingIndicator()
}
}
)
@ -112,99 +120,48 @@ fun AppSelectorScreen(
)
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
LazyColumn(
modifier = Modifier.fillMaxSize().padding(paddingValues)
) {
if (PM.supportedAppList.isNotEmpty()) {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item {
ListItem(
modifier = Modifier.clickable {
pickApkLauncher.launch(APK_MIMETYPE)
},
leadingContent = {
Box(Modifier.size(36.dp), Alignment.Center) {
Icon(
Icons.Default.Storage,
null,
modifier = Modifier.size(24.dp)
)
}
},
headlineContent = { Text(stringResource(R.string.select_from_storage)) }
)
Divider()
}
(PM.appList.ifEmpty { PM.supportedAppList }).also { list ->
items(
count = list.size,
key = { list[it].packageName }
) { index ->
val app = list[index]
ListItem(
modifier = Modifier.clickable { onAppClick(PackageInfo(app)) },
leadingContent = { AppIcon(app.icon, null, 36) },
headlineContent = { Text(app.label) },
supportingContent = { Text(app.packageName) },
trailingContent = {
Text("420 Patches")
}
item {
ListItem(
modifier = Modifier.clickable {
pickApkLauncher.launch(APK_MIMETYPE)
},
leadingContent = {
Box(Modifier.size(36.dp), Alignment.Center) {
Icon(
Icons.Default.Storage,
null,
modifier = Modifier.size(24.dp)
)
}
},
headlineContent = { Text(stringResource(R.string.select_from_storage)) }
)
Divider()
}
if (appList.isNotEmpty()) {
items(
items = appList,
key = { it.packageName }
) { app ->
ListItem(
modifier = Modifier.clickable {
app.packageInfo?.let { onAppClick(app) }
},
leadingContent = { AppIcon(app, null) },
headlineContent = { Text(vm.loadLabel(app.packageInfo)) },
supportingContent = { Text(app.packageName) },
trailingContent = if (app.patches > 0) { { Text(pluralStringResource(R.plurals.patches_count, app.patches, app.patches)) } } else null
)
if (PM.appList.isEmpty()) {
item {
Box(Modifier.fillMaxWidth(), Alignment.Center) {
CircularProgressIndicator(
Modifier.padding(vertical = 15.dp).size(24.dp),
strokeWidth = 3.dp
)
}
}
}
}
}
} else {
LoadingIndicator()
item { LoadingIndicator() }
}
}
}
}
/*Row(
modifier = Modifier.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
FilterChip(
selected = false,
onClick = {},
label = { Text("Patched apps") },
leadingIcon = { Icon(Icons.Default.Check, null) },
enabled = false
)
FilterChip(
selected = false,
onClick = {},
label = { Text("User apps") },
leadingIcon = { Icon(Icons.Default.Android, null) }
)
FilterChip(
selected = filterSystemApps,
onClick = { filterSystemApps = !filterSystemApps },
label = { Text("System apps") },
leadingIcon = { Icon(Icons.Default.Apps, null) }
)
}*/

View File

@ -1,18 +1,28 @@
package app.revanced.manager.compose.ui.viewmodel
import android.app.Application
import android.content.pm.PackageInfo
import android.net.Uri
import androidx.lifecycle.ViewModel
import app.revanced.manager.compose.util.PM
import java.io.File
import java.nio.file.Files
class AppSelectorViewModel(private val app: Application) : ViewModel() {
class AppSelectorViewModel(
private val app: Application,
private val pm: PM
) : ViewModel() {
private val packageManager = app.packageManager
val appList = pm.appList
fun loadLabel(app: PackageInfo?) = (app?.applicationInfo?.loadLabel(packageManager) ?: "Not installed").toString()
fun loadSelectedFile(uri: Uri) =
app.contentResolver.openInputStream(uri)!!.use { stream ->
File(app.cacheDir, "input.apk").also {
if (it.exists()) it.delete()
Files.copy(stream, it.toPath())
}.let { PM.getApkInfo(it, app) }
}.let { pm.getApkInfo(it) }
}
}

View File

@ -5,6 +5,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInfo
import android.content.pm.PackageInstaller
import android.net.Uri
import androidx.compose.runtime.derivedStateOf
@ -21,8 +22,8 @@ import app.revanced.manager.compose.patcher.worker.PatcherWorker
import app.revanced.manager.compose.patcher.worker.StepGroup
import app.revanced.manager.compose.service.InstallService
import app.revanced.manager.compose.service.UninstallService
import app.revanced.manager.compose.util.AppInfo
import app.revanced.manager.compose.util.PM
import app.revanced.manager.compose.util.PackageInfo
import app.revanced.manager.compose.util.PatchesSelection
import app.revanced.manager.compose.util.toast
import kotlinx.serialization.encodeToString
@ -33,11 +34,13 @@ import java.io.File
import java.nio.file.Files
class InstallerScreenViewModel(
input: PackageInfo,
input: AppInfo,
selectedPatches: PatchesSelection
) : ViewModel(), KoinComponent {
private val signerService: SignerService by inject()
private val app: Application by inject()
private val pm: PM by inject()
var stepGroups by mutableStateOf<List<StepGroup>>(
PatcherProgressManager.generateGroupsList(
app,
@ -45,7 +48,7 @@ class InstallerScreenViewModel(
)
private set
val packageName = input.packageName
val packageName: String = input.packageName
private val workManager = WorkManager.getInstance(app)
@ -69,11 +72,11 @@ class InstallerScreenViewModel(
PatcherWorker.ARGS_KEY to
Json.Default.encodeToString(
PatcherWorker.Args(
input.apk.path,
input.path!!.absolutePath,
outputFile.path,
selectedPatches,
input.packageName,
input.version,
input.packageInfo!!.versionName,
)
)
)
@ -141,7 +144,7 @@ class InstallerScreenViewModel(
isInstalling = true
try {
if (!signApk()) return
PM.installApp(listOf(signedFile), app)
pm.installApp(listOf(signedFile))
} finally {
isInstalling = false
}

View File

@ -0,0 +1,27 @@
package app.revanced.manager.compose.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.compose.domain.repository.SourceRepository
import app.revanced.manager.compose.util.PM
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainViewModel(
sourceRepository: SourceRepository,
pm: PM
) : ViewModel() {
init {
with(viewModelScope) {
launch {
sourceRepository.loadSources()
}
launch {
pm.getCompatibleApps()
}
launch(Dispatchers.IO) {
pm.getInstalledApps()
}
}
}
}

View File

@ -1,23 +1,26 @@
package app.revanced.manager.compose.ui.viewmodel
import androidx.compose.runtime.*
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import app.revanced.manager.compose.domain.repository.BundleRepository
import app.revanced.manager.compose.domain.repository.SourceRepository
import app.revanced.manager.compose.patcher.patch.PatchInfo
import app.revanced.manager.compose.util.PackageInfo
import app.revanced.manager.compose.util.AppInfo
import app.revanced.manager.compose.util.PatchesSelection
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
class PatchesSelectorViewModel(packageInfo: PackageInfo) : ViewModel(), KoinComponent {
val bundlesFlow = get<BundleRepository>().bundles.map { bundles ->
class PatchesSelectorViewModel(appInfo: AppInfo) : ViewModel(), KoinComponent {
val bundlesFlow = get<SourceRepository>().bundles.map { bundles ->
bundles.mapValues { (_, bundle) -> bundle.patches }.map { (name, patches) ->
val supported = mutableListOf<PatchInfo>()
val unsupported = mutableListOf<PatchInfo>()
patches.filter { it.compatibleWith(packageInfo.packageName) }.forEach {
val targetList = if (it.supportsVersion(packageInfo.packageName)) supported else unsupported
patches.filter { it.compatibleWith(appInfo.packageName) }.forEach {
val targetList = if (it.supportsVersion(appInfo.packageInfo!!.versionName)) supported else unsupported
targetList.add(it)
}

View File

@ -1,18 +1,17 @@
package app.revanced.manager.compose.ui.viewmodel
import android.app.Application
import android.os.Environment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.compose.network.api.ManagerAPI
import app.revanced.manager.compose.util.PM
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import app.revanced.manager.compose.util.PM
import java.io.File
class UpdateSettingsViewModel(
private val managerAPI: ManagerAPI,
private val app: Application,
private val pm: PM
) : ViewModel() {
val downloadProgress get() = (managerAPI.downloadProgress?.times(100)) ?: 0f
val downloadedSize get() = managerAPI.downloadedSize ?: 0L
@ -23,18 +22,16 @@ class UpdateSettingsViewModel(
}
}
fun installUpdate() {
PM.installApp(
pm.installApp(
apks = listOf(
File(
(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS + "/revanced-manager.apk")
.toString())
),
),
context = app,
)
)
}
init {
downloadLatestManager()
}

View File

@ -1,74 +1,133 @@
package app.revanced.manager.compose.util
import android.annotation.SuppressLint
import android.app.Application
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
import android.os.Build
import android.os.Parcelable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.Immutable
import app.revanced.manager.compose.domain.repository.SourceRepository
import app.revanced.manager.compose.service.InstallService
import app.revanced.manager.compose.service.UninstallService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
import java.io.File
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
@Immutable
@Parcelize
data class PackageInfo(val packageName: String, val version: String, val apk: File) : Parcelable {
constructor(appInfo: PM.AppInfo) : this(appInfo.packageName, appInfo.versionName, appInfo.apk)
}
data class AppInfo(
val packageName: String,
val patches: Int,
val packageInfo: PackageInfo?,
val path: File? = null
) : Parcelable
@SuppressLint("QueryPermissionsNeeded")
@Suppress("DEPRECATION")
object PM {
val appList = mutableStateListOf<AppInfo>()
val supportedAppList = mutableStateListOf<AppInfo>()
class PM(
private val app: Application,
private val sourceRepository: SourceRepository
) {
private val coroutineScope = CoroutineScope(Dispatchers.Default)
suspend fun loadApps(context: Context) {
val packageManager = context.packageManager
private val installedApps = MutableStateFlow(emptyList<AppInfo>())
private val compatibleApps = MutableStateFlow(emptyList<AppInfo>())
val localAppList = mutableListOf<AppInfo>()
val appList: StateFlow<List<AppInfo>> = compatibleApps.combine(installedApps) { compatibleApps, installedApps ->
if (compatibleApps.isNotEmpty()) {
(compatibleApps + installedApps)
.distinctBy { it.packageName }
.sortedWith(
compareByDescending<AppInfo> {
it.patches
}.thenBy { it.packageInfo?.applicationInfo?.loadLabel(app.packageManager).toString() }.thenBy { it.packageName }
)
} else {
emptyList()
}
}.stateIn(coroutineScope, SharingStarted.Eagerly, emptyList())
packageManager.getInstalledApplications(PackageManager.GET_META_DATA).map {
AppInfo(
it.packageName,
"0.69.420",
it.loadLabel(packageManager).toString(),
it.loadIcon(packageManager),
File("h")
)
}.also { localAppList.addAll(it) }.also { supportedAppList.addAll(it) }
}
suspend fun getCompatibleApps() {
sourceRepository.bundles.collect { bundles ->
val compatiblePackages = HashMap<String, Int>()
@Parcelize
data class AppInfo(
val packageName: String,
val versionName: String,
val label: String,
val icon: @RawValue Drawable? = null,
val apk: File,
) : Parcelable
bundles.flatMap { it.value.patches }.forEach {
it.compatiblePackages?.forEach { pkg ->
compatiblePackages[pkg.name] = compatiblePackages.getOrDefault(pkg.name, 0) + 1
}
}
fun installApp(apks: List<File>, context: Context) {
val packageInstaller = context.packageManager.packageInstaller
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
apks.forEach { apk -> session.writeApk(apk) }
session.commit(context.installIntentSender)
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
)
}
}
)
}
}
}
fun uninstallPackage(pkg: String, context: Context) {
val packageInstaller = context.packageManager.packageInstaller
packageInstaller.uninstall(pkg, context.uninstallIntentSender)
suspend fun getInstalledApps() {
installedApps.emit(app.packageManager.getInstalledPackages(MATCH_UNINSTALLED_PACKAGES).map { packageInfo ->
AppInfo(
packageInfo.packageName,
0,
packageInfo,
File(packageInfo.applicationInfo.sourceDir)
)
})
}
fun getApkInfo(apk: File, context: Context) = context.packageManager.getPackageArchiveInfo(apk.path, 0)!!
.let { PackageInfo(it.packageName, it.versionName, apk) }
fun installApp(apks: List<File>) {
val packageInstaller = app.packageManager.packageInstaller
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
apks.forEach { apk -> session.writeApk(apk) }
session.commit(app.installIntentSender)
}
}
fun uninstallPackage(pkg: String) {
val packageInstaller = app.packageManager.packageInstaller
packageInstaller.uninstall(pkg, app.uninstallIntentSender)
}
fun getApkInfo(apk: File) = app.packageManager.getPackageArchiveInfo(apk.path, 0)!!.let {
AppInfo(
it.packageName,
0,
it,
apk
)
}
}
private fun PackageInstaller.Session.writeApk(apk: File) {

View File

@ -62,7 +62,7 @@
<string name="no_patched_apps_found">No patched apps found</string>
<string name="unsupported_app">Unsupported app</string>
<string name="unsupported_patches">Unsupported patches</string>
<string name="app_not_supported">Some of the patches do not support this app version (%s). The patches only support the following versions: %s.</string>
<string name="app_not_supported">Some of the patches do not support this app version (%1$s). The patches only support the following versions: %2$s.</string>
<string name="installer">Installer</string>
<string name="install_app">Install</string>