diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 672f57c..fb6a651 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c41b87c..76aafb9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,6 +28,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.ReVancedManager" + android:enableOnBackInvokedCallback="true" tools:targetApi="33"> () - 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( diff --git a/app/src/main/java/app/revanced/manager/compose/di/ManagerModule.kt b/app/src/main/java/app/revanced/manager/compose/di/ManagerModule.kt index 21098ec..f2765ed 100644 --- a/app/src/main/java/app/revanced/manager/compose/di/ManagerModule.kt +++ b/app/src/main/java/app/revanced/manager/compose/di/ManagerModule.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/di/RepositoryModule.kt b/app/src/main/java/app/revanced/manager/compose/di/RepositoryModule.kt index 6dbe896..f93c456 100644 --- a/app/src/main/java/app/revanced/manager/compose/di/RepositoryModule.kt +++ b/app/src/main/java/app/revanced/manager/compose/di/RepositoryModule.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/di/ViewModelModule.kt b/app/src/main/java/app/revanced/manager/compose/di/ViewModelModule.kt index 42e0945..db485ac 100644 --- a/app/src/main/java/app/revanced/manager/compose/di/ViewModelModule.kt +++ b/app/src/main/java/app/revanced/manager/compose/di/ViewModelModule.kt @@ -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) diff --git a/app/src/main/java/app/revanced/manager/compose/domain/repository/BundleRepository.kt b/app/src/main/java/app/revanced/manager/compose/domain/repository/BundleRepository.kt deleted file mode 100644 index 5a8c70e..0000000 --- a/app/src/main/java/app/revanced/manager/compose/domain/repository/BundleRepository.kt +++ /dev/null @@ -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>(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() - _bundles.emit(map) - - events.collect { (name, new) -> - map[name] = new - _bundles.emit(map) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/domain/repository/SourceRepository.kt b/app/src/main/java/app/revanced/manager/compose/domain/repository/SourceRepository.kt index 01a5494..166f89e 100644 --- a/app/src/main/java/app/revanced/manager/compose/domain/repository/SourceRepository.kt +++ b/app/src/main/java/app/revanced/manager/compose/domain/repository/SourceRepository.kt @@ -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> = 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> = MutableStateFlow(emptyMap()) - val sources = _sources.asStateFlow() - suspend fun redownloadRemoteSources() = sources.value.values.filterIsInstance().forEach { it.downloadLatest() } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/domain/sources/LocalSource.kt b/app/src/main/java/app/revanced/manager/compose/domain/sources/LocalSource.kt index c117182..449ce38 100644 --- a/app/src/main/java/app/revanced/manager/compose/domain/sources/LocalSource.kt +++ b/app/src/main/java/app/revanced/manager/compose/domain/sources/LocalSource.kt @@ -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 }) } } diff --git a/app/src/main/java/app/revanced/manager/compose/domain/sources/RemoteSource.kt b/app/src/main/java/app/revanced/manager/compose/domain/sources/RemoteSource.kt index 0b40efc..a7e762c 100644 --- a/app/src/main/java/app/revanced/manager/compose/domain/sources/RemoteSource.kt +++ b/app/src/main/java/app/revanced/manager/compose/domain/sources/RemoteSource.kt @@ -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 diff --git a/app/src/main/java/app/revanced/manager/compose/patcher/patch/PatchBundle.kt b/app/src/main/java/app/revanced/manager/compose/patcher/patch/PatchBundle.kt index dbfe65b..b1a8de3 100644 --- a/app/src/main/java/app/revanced/manager/compose/patcher/patch/PatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/compose/patcher/patch/PatchBundle.kt @@ -42,5 +42,5 @@ class PatchBundle(private val loader: Iterable, val integrations: Fi true } - fun loadAllPatches() = loader.toList() + private fun loadAllPatches() = loader.toList() } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/compose/patcher/worker/PatcherWorker.kt index 41e8400..10e5b99 100644 --- a/app/src/main/java/app/revanced/manager/compose/patcher/worker/PatcherWorker.kt +++ b/app/src/main/java/app/revanced/manager/compose/patcher/worker/PatcherWorker.kt @@ -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(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) -> diff --git a/app/src/main/java/app/revanced/manager/compose/service/InstallService.kt b/app/src/main/java/app/revanced/manager/compose/service/InstallService.kt index 1cc347c..e12e69c 100644 --- a/app/src/main/java/app/revanced/manager/compose/service/InstallService.kt +++ b/app/src/main/java/app/revanced/manager/compose/service/InstallService.kt @@ -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( diff --git a/app/src/main/java/app/revanced/manager/compose/service/UninstallService.kt b/app/src/main/java/app/revanced/manager/compose/service/UninstallService.kt index c5cbfa8..e80dbac 100644 --- a/app/src/main/java/app/revanced/manager/compose/service/UninstallService.kt +++ b/app/src/main/java/app/revanced/manager/compose/service/UninstallService.kt @@ -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( diff --git a/app/src/main/java/app/revanced/manager/compose/ui/component/AppIcon.kt b/app/src/main/java/app/revanced/manager/compose/ui/component/AppIcon.kt index 8d8bdfd..33f4167 100644 --- a/app/src/main/java/app/revanced/manager/compose/ui/component/AppIcon.kt +++ b/app/src/main/java/app/revanced/manager/compose/ui/component/AppIcon.kt @@ -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) ) } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/ui/component/LoadingIndicator.kt b/app/src/main/java/app/revanced/manager/compose/ui/component/LoadingIndicator.kt index 398dd33..5560f25 100644 --- a/app/src/main/java/app/revanced/manager/compose/ui/component/LoadingIndicator.kt +++ b/app/src/main/java/app/revanced/manager/compose/ui/component/LoadingIndicator.kt @@ -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, diff --git a/app/src/main/java/app/revanced/manager/compose/ui/destination/Destination.kt b/app/src/main/java/app/revanced/manager/compose/ui/destination/Destination.kt index 25f7b7b..a1143c5 100644 --- a/app/src/main/java/app/revanced/manager/compose/ui/destination/Destination.kt +++ b/app/src/main/java/app/revanced/manager/compose/ui/destination/Destination.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/ui/screen/AppSelectorScreen.kt b/app/src/main/java/app/revanced/manager/compose/ui/screen/AppSelectorScreen.kt index 34c7b20..4a72ed5 100644 --- a/app/src/main/java/app/revanced/manager/compose/ui/screen/AppSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/compose/ui/screen/AppSelectorScreen.kt @@ -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) } - ) -}*/ diff --git a/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/AppSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/AppSelectorViewModel.kt index c1ff713..315406f 100644 --- a/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/AppSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/AppSelectorViewModel.kt @@ -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) } } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/InstallerScreenViewModel.kt b/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/InstallerScreenViewModel.kt index 5927c5c..deb43af 100644 --- a/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/InstallerScreenViewModel.kt +++ b/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/InstallerScreenViewModel.kt @@ -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>( 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 } diff --git a/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/MainViewModel.kt b/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/MainViewModel.kt new file mode 100644 index 0000000..7930082 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/MainViewModel.kt @@ -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() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/PatchesSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/PatchesSelectorViewModel.kt index 7590feb..e1d40ca 100644 --- a/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/PatchesSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/PatchesSelectorViewModel.kt @@ -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().bundles.map { bundles -> +class PatchesSelectorViewModel(appInfo: AppInfo) : ViewModel(), KoinComponent { + val bundlesFlow = get().bundles.map { bundles -> bundles.mapValues { (_, bundle) -> bundle.patches }.map { (name, patches) -> val supported = mutableListOf() val unsupported = mutableListOf() - 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) } diff --git a/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/UpdateSettingsViewModel.kt b/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/UpdateSettingsViewModel.kt index afc9ede..be7bddd 100644 --- a/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/UpdateSettingsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/compose/ui/viewmodel/UpdateSettingsViewModel.kt @@ -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() } diff --git a/app/src/main/java/app/revanced/manager/compose/util/PM.kt b/app/src/main/java/app/revanced/manager/compose/util/PM.kt index 40e64dc..69b7f22 100644 --- a/app/src/main/java/app/revanced/manager/compose/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/compose/util/PM.kt @@ -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() - val supportedAppList = mutableStateListOf() +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()) + private val compatibleApps = MutableStateFlow(emptyList()) - val localAppList = mutableListOf() + val appList: StateFlow> = compatibleApps.combine(installedApps) { compatibleApps, installedApps -> + if (compatibleApps.isNotEmpty()) { + (compatibleApps + installedApps) + .distinctBy { it.packageName } + .sortedWith( + compareByDescending { + 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() - @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, 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) { + 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) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 415547b..a19a7c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -62,7 +62,7 @@ No patched apps found Unsupported app Unsupported patches - Some of the patches do not support this app version (%s). The patches only support the following versions: %s. + Some of the patches do not support this app version (%1$s). The patches only support the following versions: %2$s. Installer Install