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") proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
} }
packagingOptions { packaging {
resources { resources {
excludes += "/prebuilt/**" excludes += "/prebuilt/**"
} }
@ -51,6 +52,10 @@ android {
composeOptions.kotlinCompilerExtensionVersion = "1.4.7" composeOptions.kotlinCompilerExtensionVersion = "1.4.7"
} }
kotlin {
jvmToolchain(11)
}
dependencies { dependencies {
// AndroidX Core // AndroidX Core
@ -58,7 +63,7 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1") implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.core:core-splashscreen:1.0.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.paging:paging-common-ktx:3.1.1")
implementation("androidx.work:work-runtime-ktx:2.8.1") implementation("androidx.work:work-runtime-ktx:2.8.1")
@ -78,10 +83,12 @@ dependencies {
//implementation("com.google.accompanist:accompanist-permissions:$accompanistVersion") //implementation("com.google.accompanist:accompanist-permissions:$accompanistVersion")
// Coil (async image loading, network image) // 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 // KotlinX
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5")
// Room // Room
val roomVersion = "2.5.1" val roomVersion = "2.5.1"
@ -94,7 +101,7 @@ dependencies {
implementation("app.revanced:revanced-patcher:9.0.0") implementation("app.revanced:revanced-patcher:9.0.0")
// Signing // 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") implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
// Koin // Koin

View File

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

View File

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

View File

@ -1,10 +1,11 @@
package app.revanced.manager.compose.di 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.patcher.SignerService
import app.revanced.manager.compose.util.PM
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
val managerModule = module { val managerModule = module {
singleOf(::SignerService) 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.domain.repository.ReVancedRepository
import app.revanced.manager.compose.network.api.ManagerAPI 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.SourcePersistenceRepository
import app.revanced.manager.compose.domain.repository.SourceRepository import app.revanced.manager.compose.domain.repository.SourceRepository
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
@ -11,7 +10,6 @@ import org.koin.dsl.module
val repositoryModule = module { val repositoryModule = module {
singleOf(::ReVancedRepository) singleOf(::ReVancedRepository)
singleOf(::ManagerAPI) singleOf(::ManagerAPI)
singleOf(::BundleRepository)
singleOf(::SourcePersistenceRepository) singleOf(::SourcePersistenceRepository)
singleOf(::SourceRepository) singleOf(::SourceRepository)
} }

View File

@ -5,6 +5,7 @@ import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.dsl.module import org.koin.dsl.module
val viewModelModule = module { val viewModelModule = module {
viewModelOf(::MainViewModel)
viewModelOf(::PatchesSelectorViewModel) viewModelOf(::PatchesSelectorViewModel)
viewModelOf(::SettingsViewModel) viewModelOf(::SettingsViewModel)
viewModelOf(::AppSelectorViewModel) 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 android.util.Log
import app.revanced.manager.compose.data.room.sources.SourceEntity import app.revanced.manager.compose.data.room.sources.SourceEntity
import app.revanced.manager.compose.data.room.sources.SourceLocation 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.LocalSource
import app.revanced.manager.compose.domain.sources.RemoteSource
import app.revanced.manager.compose.domain.sources.Source import app.revanced.manager.compose.domain.sources.Source
import app.revanced.manager.compose.util.tag import app.revanced.manager.compose.util.tag
import io.ktor.http.* import io.ktor.http.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
@ -20,6 +23,18 @@ import java.io.InputStream
class SourceRepository(app: Application, private val persistenceRepo: SourcePersistenceRepository) { class SourceRepository(app: Application, private val persistenceRepo: SourcePersistenceRepository) {
private val sourcesDir = app.dataDir.resolve("sources").also { it.mkdirs() } 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. * 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() persistenceRepo.clear()
_sources.emit(emptyMap()) _sources.emit(emptyMap())
sourcesDir.apply { sourcesDir.apply {
delete() deleteRecursively()
mkdirs() mkdirs()
} }
@ -58,7 +73,7 @@ class SourceRepository(app: Application, private val persistenceRepo: SourcePers
suspend fun remove(source: Source) = withContext(Dispatchers.Default) { suspend fun remove(source: Source) = withContext(Dispatchers.Default) {
persistenceRepo.delete(source.id) persistenceRepo.delete(source.id)
directoryOf(source.id).delete() directoryOf(source.id).deleteRecursively()
_sources.update { _sources.update {
it.filterValues { value -> it.filterValues { value ->
@ -84,9 +99,6 @@ class SourceRepository(app: Application, private val persistenceRepo: SourcePers
addSource(name, RemoteSource(id, directoryOf(id))) addSource(name, RemoteSource(id, directoryOf(id)))
} }
private val _sources: MutableStateFlow<Map<String, Source>> = MutableStateFlow(emptyMap())
val sources = _sources.asStateFlow()
suspend fun redownloadRemoteSources() = suspend fun redownloadRemoteSources() =
sources.value.values.filterIsInstance<RemoteSource>().forEach { it.downloadLatest() } 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,18 +3,16 @@ package app.revanced.manager.compose.domain.sources
import app.revanced.manager.compose.network.api.ManagerAPI import app.revanced.manager.compose.network.api.ManagerAPI
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.inject import org.koin.core.component.get
import java.io.File import java.io.File
class RemoteSource(id: Int, directory: File) : Source(id, directory) { 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) { suspend fun downloadLatest() = withContext(Dispatchers.IO) {
api.downloadBundle(patchesJar, integrations).also { (patchesVer, integrationsVer) -> api.downloadBundle(patchesJar, integrations).also { (patchesVer, integrationsVer) ->
saveVersion(patchesVer, integrationsVer) saveVersion(patchesVer, integrationsVer)
withContext(Dispatchers.Main) {
_bundle.emit(loadBundle { err -> throw err }) _bundle.emit(loadBundle { err -> throw err })
} }
}
return@withContext return@withContext
} }

View File

@ -42,5 +42,5 @@ class PatchBundle(private val loader: Iterable<PatchClass>, val integrations: Fi
true 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 android.util.Log
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters 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.Session
import app.revanced.manager.compose.patcher.aapt.Aapt import app.revanced.manager.compose.patcher.aapt.Aapt
import app.revanced.manager.compose.util.PatchesSelection import app.revanced.manager.compose.util.PatchesSelection
import app.revanced.manager.compose.util.tag import app.revanced.manager.compose.util.tag
import app.revanced.patcher.extensions.PatchExtensions.patchName import app.revanced.patcher.extensions.PatchExtensions.patchName
import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -20,7 +21,7 @@ import java.io.FileNotFoundException
// TODO: setup wakelock + notification so android doesn't murder us. // TODO: setup wakelock + notification so android doesn't murder us.
class PatcherWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters), class PatcherWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters),
KoinComponent { KoinComponent {
private val bundleRepository: BundleRepository by inject() private val sourceRepository: SourceRepository by inject()
@Serializable @Serializable
data class Args( data class Args(
@ -50,7 +51,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) : CoroutineW
val args = Json.decodeFromString<Args>(inputData.getString(ARGS_KEY)!!) 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 integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations }
val patchList = args.selectedPatches.flatMap { (bundleName, selected) -> 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.Build
import android.os.IBinder import android.os.IBinder
@Suppress("DEPRECATION")
class InstallService : Service() { class InstallService : Service() {
override fun onStartCommand( override fun onStartCommand(

View File

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

View File

@ -1,6 +1,5 @@
package app.revanced.manager.compose.ui.component package app.revanced.manager.compose.ui.component
import android.graphics.drawable.Drawable
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons 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.ColorFilter
import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter import app.revanced.manager.compose.util.AppInfo
import coil.compose.AsyncImage
@Composable @Composable
fun AppIcon( fun AppIcon(
drawable: Drawable?, app: AppInfo,
contentDescription: String?, contentDescription: String?,
size: Int = 48 modifier: Modifier = Modifier
) { ) {
if (drawable == null) { if (app.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(size.dp), Modifier.size(36.dp).then(modifier),
colorFilter = colorFilter colorFilter = colorFilter
) )
} else { } else {
val image = rememberAsyncImagePainter(drawable) AsyncImage(
app.packageInfo,
Image(
image,
contentDescription, 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.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.compose.R
@Composable @Composable
fun LoadingIndicator(progress: Float? = null, text: Int? = R.string.loading_body) { fun LoadingIndicator(progress: Float? = null, text: Int? = null) {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,

View File

@ -1,7 +1,7 @@
package app.revanced.manager.compose.ui.destination package app.revanced.manager.compose.ui.destination
import android.os.Parcelable 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 app.revanced.manager.compose.util.PatchesSelection
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -17,8 +17,8 @@ sealed interface Destination : Parcelable {
object Settings : Destination object Settings : Destination
@Parcelize @Parcelize
data class PatchesSelector(val input: PackageInfo) : Destination data class PatchesSelector(val input: AppInfo) : Destination
@Parcelize @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.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.compose.R import app.revanced.manager.compose.R
import app.revanced.manager.compose.ui.component.AppIcon import app.revanced.manager.compose.ui.component.AppIcon
import app.revanced.manager.compose.ui.component.AppTopBar import app.revanced.manager.compose.ui.component.AppTopBar
import app.revanced.manager.compose.ui.component.LoadingIndicator import app.revanced.manager.compose.ui.component.LoadingIndicator
import app.revanced.manager.compose.ui.viewmodel.AppSelectorViewModel import app.revanced.manager.compose.ui.viewmodel.AppSelectorViewModel
import app.revanced.manager.compose.util.APK_MIMETYPE import app.revanced.manager.compose.util.APK_MIMETYPE
import app.revanced.manager.compose.util.PM import app.revanced.manager.compose.util.AppInfo
import app.revanced.manager.compose.util.PackageInfo
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: (PackageInfo) -> Unit, onAppClick: (AppInfo) -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: AppSelectorViewModel = getViewModel() vm: AppSelectorViewModel = getViewModel()
) { ) {
val pickApkLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { apkUri -> val pickApkLauncher =
vm.loadSelectedFile(apkUri!!).let(onAppClick) rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
it?.let { apkUri -> onAppClick(vm.loadSelectedFile(apkUri)) }
} }
var filterText by rememberSaveable { mutableStateOf("") } var filterText by rememberSaveable { mutableStateOf("") }
var search by rememberSaveable { mutableStateOf(false) } 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 // TODO: find something better for this
if (search) { if (search) {
SearchBar( SearchBar(
@ -63,34 +75,30 @@ fun AppSelectorScreen(
) )
} }
}, },
shape = SearchBarDefaults.inputFieldShape,
content = { content = {
if (PM.appList.isNotEmpty()) {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
if (appList.isNotEmpty()) {
items( items(
PM.appList items = filteredAppList,
.filter { app -> key = { it.packageName }
(app.label.contains(
filterText,
true
) or app.packageName.contains(filterText, true))
}
) { app -> ) { app ->
ListItem( ListItem(
modifier = Modifier.clickable { onAppClick(PackageInfo(app)) }, modifier = Modifier.clickable {
leadingContent = { AppIcon(app.icon, null, 36) }, app.packageInfo?.let { onAppClick(app) }
headlineContent = { Text(app.label) }, },
leadingContent = { AppIcon(app, null) },
headlineContent = { Text(vm.loadLabel(app.packageInfo)) },
supportingContent = { Text(app.packageName) }, 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 { } else {
LoadingIndicator() item { LoadingIndicator() }
}
} }
} }
) )
@ -112,18 +120,10 @@ fun AppSelectorScreen(
) )
} }
) { paddingValues -> ) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
) {
if (PM.supportedAppList.isNotEmpty()) {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize().padding(paddingValues)
) { ) {
item { item {
ListItem( ListItem(
modifier = Modifier.clickable { modifier = Modifier.clickable {
pickApkLauncher.launch(APK_MIMETYPE) pickApkLauncher.launch(APK_MIMETYPE)
@ -139,72 +139,29 @@ fun AppSelectorScreen(
}, },
headlineContent = { Text(stringResource(R.string.select_from_storage)) } headlineContent = { Text(stringResource(R.string.select_from_storage)) }
) )
Divider() Divider()
} }
(PM.appList.ifEmpty { PM.supportedAppList }).also { list -> if (appList.isNotEmpty()) {
items( items(
count = list.size, items = appList,
key = { list[it].packageName } key = { it.packageName }
) { index -> ) { app ->
val app = list[index]
ListItem( ListItem(
modifier = Modifier.clickable { onAppClick(PackageInfo(app)) }, modifier = Modifier.clickable {
leadingContent = { AppIcon(app.icon, null, 36) }, app.packageInfo?.let { onAppClick(app) }
headlineContent = { Text(app.label) }, },
leadingContent = { AppIcon(app, null) },
headlineContent = { Text(vm.loadLabel(app.packageInfo)) },
supportingContent = { Text(app.packageName) }, supportingContent = { Text(app.packageName) },
trailingContent = { trailingContent = if (app.patches > 0) { { Text(pluralStringResource(R.plurals.patches_count, app.patches, app.patches)) } } else null
Text("420 Patches")
}
) )
} }
if (PM.appList.isEmpty()) {
item {
Box(Modifier.fillMaxWidth(), Alignment.Center) {
CircularProgressIndicator(
Modifier.padding(vertical = 15.dp).size(24.dp),
strokeWidth = 3.dp
)
}
}
}
}
}
} else { } 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 package app.revanced.manager.compose.ui.viewmodel
import android.app.Application import android.app.Application
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.compose.util.PM import app.revanced.manager.compose.util.PM
import java.io.File import java.io.File
import java.nio.file.Files 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) = 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() if (it.exists()) it.delete()
Files.copy(stream, it.toPath()) 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.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageInfo
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.derivedStateOf 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.patcher.worker.StepGroup
import app.revanced.manager.compose.service.InstallService import app.revanced.manager.compose.service.InstallService
import app.revanced.manager.compose.service.UninstallService 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.PM
import app.revanced.manager.compose.util.PackageInfo
import app.revanced.manager.compose.util.PatchesSelection import app.revanced.manager.compose.util.PatchesSelection
import app.revanced.manager.compose.util.toast import app.revanced.manager.compose.util.toast
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
@ -33,11 +34,13 @@ import java.io.File
import java.nio.file.Files import java.nio.file.Files
class InstallerScreenViewModel( class InstallerScreenViewModel(
input: PackageInfo, input: AppInfo,
selectedPatches: PatchesSelection selectedPatches: PatchesSelection
) : ViewModel(), KoinComponent { ) : ViewModel(), KoinComponent {
private val signerService: SignerService by inject() private val signerService: SignerService by inject()
private val app: Application by inject() private val app: Application by inject()
private val pm: PM by inject()
var stepGroups by mutableStateOf<List<StepGroup>>( var stepGroups by mutableStateOf<List<StepGroup>>(
PatcherProgressManager.generateGroupsList( PatcherProgressManager.generateGroupsList(
app, app,
@ -45,7 +48,7 @@ class InstallerScreenViewModel(
) )
private set private set
val packageName = input.packageName val packageName: String = input.packageName
private val workManager = WorkManager.getInstance(app) private val workManager = WorkManager.getInstance(app)
@ -69,11 +72,11 @@ class InstallerScreenViewModel(
PatcherWorker.ARGS_KEY to PatcherWorker.ARGS_KEY to
Json.Default.encodeToString( Json.Default.encodeToString(
PatcherWorker.Args( PatcherWorker.Args(
input.apk.path, input.path!!.absolutePath,
outputFile.path, outputFile.path,
selectedPatches, selectedPatches,
input.packageName, input.packageName,
input.version, input.packageInfo!!.versionName,
) )
) )
) )
@ -141,7 +144,7 @@ class InstallerScreenViewModel(
isInstalling = true isInstalling = true
try { try {
if (!signApk()) return if (!signApk()) return
PM.installApp(listOf(signedFile), app) pm.installApp(listOf(signedFile))
} finally { } finally {
isInstalling = false 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 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 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.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 app.revanced.manager.compose.util.PatchesSelection
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
class PatchesSelectorViewModel(packageInfo: PackageInfo) : ViewModel(), KoinComponent { class PatchesSelectorViewModel(appInfo: AppInfo) : ViewModel(), KoinComponent {
val bundlesFlow = get<BundleRepository>().bundles.map { bundles -> val bundlesFlow = get<SourceRepository>().bundles.map { bundles ->
bundles.mapValues { (_, bundle) -> bundle.patches }.map { (name, patches) -> bundles.mapValues { (_, bundle) -> bundle.patches }.map { (name, patches) ->
val supported = mutableListOf<PatchInfo>() val supported = mutableListOf<PatchInfo>()
val unsupported = mutableListOf<PatchInfo>() val unsupported = mutableListOf<PatchInfo>()
patches.filter { it.compatibleWith(packageInfo.packageName) }.forEach { patches.filter { it.compatibleWith(appInfo.packageName) }.forEach {
val targetList = if (it.supportsVersion(packageInfo.packageName)) supported else unsupported val targetList = if (it.supportsVersion(appInfo.packageInfo!!.versionName)) supported else unsupported
targetList.add(it) targetList.add(it)
} }

View File

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

View File

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