mirror of
https://github.com/revanced/revanced-manager-compose
synced 2024-11-16 16:29:24 +01:00
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:
parent
a6b86691db
commit
cd0144b563
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
@ -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)
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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() }
|
||||||
}
|
}
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
@ -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) ->
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
@ -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) }
|
|
||||||
)
|
|
||||||
}*/
|
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user