feat: store patched apps (#79)

* feat: store patched apps

* fix: missing string

* feat: save patch selection

* feat: things

* fix: fix broken query

* fix: remove redundant `withContext`

* fix: fix
This commit is contained in:
Robert 2023-08-17 17:42:10 +02:00 committed by GitHub
parent 0ba1ac6880
commit fe95afca6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 842 additions and 89 deletions

View File

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "7142188e25ce489eb233aed8fb76e4cc", "identityHash": "5515d164bc8f713201506d42a02d337f",
"entities": [ "entities": [
{ {
"tableName": "patch_bundles", "tableName": "patch_bundles",
@ -190,12 +190,117 @@
}, },
"indices": [], "indices": [],
"foreignKeys": [] "foreignKeys": []
},
{
"tableName": "installed_app",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`current_package_name` TEXT NOT NULL, `original_package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `install_type` TEXT NOT NULL, PRIMARY KEY(`current_package_name`))",
"fields": [
{
"fieldPath": "currentPackageName",
"columnName": "current_package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "originalPackageName",
"columnName": "original_package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "installType",
"columnName": "install_type",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"current_package_name"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "applied_patch",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`package_name`, `bundle`, `patch_name`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "bundle",
"columnName": "bundle",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "patchName",
"columnName": "patch_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"package_name",
"bundle",
"patch_name"
]
},
"indices": [
{
"name": "index_applied_patch_bundle",
"unique": false,
"columnNames": [
"bundle"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_applied_patch_bundle` ON `${TABLE_NAME}` (`bundle`)"
}
],
"foreignKeys": [
{
"table": "installed_app",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"package_name"
],
"referencedColumns": [
"current_package_name"
]
},
{
"table": "patch_bundles",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"bundle"
],
"referencedColumns": [
"uid"
]
}
]
} }
], ],
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7142188e25ce489eb233aed8fb76e4cc')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5515d164bc8f713201506d42a02d337f')"
] ]
} }
} }

View File

@ -9,6 +9,7 @@ import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import app.revanced.manager.ui.component.AutoUpdatesDialog import app.revanced.manager.ui.component.AutoUpdatesDialog
import app.revanced.manager.ui.destination.Destination import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.ui.screen.AppInfoScreen
import app.revanced.manager.ui.screen.VersionSelectorScreen import app.revanced.manager.ui.screen.VersionSelectorScreen
import app.revanced.manager.ui.screen.AppSelectorScreen import app.revanced.manager.ui.screen.AppSelectorScreen
import app.revanced.manager.ui.screen.DashboardScreen import app.revanced.manager.ui.screen.DashboardScreen
@ -18,19 +19,14 @@ import app.revanced.manager.ui.screen.SettingsScreen
import app.revanced.manager.ui.theme.ReVancedManagerTheme import app.revanced.manager.ui.theme.ReVancedManagerTheme
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.MainViewModel import app.revanced.manager.ui.viewmodel.MainViewModel
import coil.Coil
import coil.ImageLoader
import dev.olshevski.navigation.reimagined.AnimatedNavHost import dev.olshevski.navigation.reimagined.AnimatedNavHost
import dev.olshevski.navigation.reimagined.NavBackHandler import dev.olshevski.navigation.reimagined.NavBackHandler
import dev.olshevski.navigation.reimagined.navigate import dev.olshevski.navigation.reimagined.navigate
import dev.olshevski.navigation.reimagined.pop import dev.olshevski.navigation.reimagined.pop
import dev.olshevski.navigation.reimagined.popUpTo import dev.olshevski.navigation.reimagined.popUpTo
import dev.olshevski.navigation.reimagined.rememberNavController import dev.olshevski.navigation.reimagined.rememberNavController
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.getViewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import kotlin.math.roundToInt
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -42,17 +38,6 @@ class MainActivity : ComponentActivity() {
installSplashScreen() installSplashScreen()
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 { setContent {
val theme by vm.prefs.theme.getAsState() val theme by vm.prefs.theme.getAsState()
val dynamicColor by vm.prefs.dynamicColor.getAsState() val dynamicColor by vm.prefs.dynamicColor.getAsState()
@ -77,7 +62,16 @@ class MainActivity : ComponentActivity() {
when (destination) { when (destination) {
is Destination.Dashboard -> DashboardScreen( is Destination.Dashboard -> DashboardScreen(
onSettingsClick = { navController.navigate(Destination.Settings) }, onSettingsClick = { navController.navigate(Destination.Settings) },
onAppSelectorClick = { navController.navigate(Destination.AppSelector) } onAppSelectorClick = { navController.navigate(Destination.AppSelector) },
onAppClick = { installedApp -> navController.navigate(Destination.ApplicationInfo(installedApp)) }
)
is Destination.ApplicationInfo -> AppInfoScreen(
onPatchClick = { packageName, patchesSelection ->
navController.navigate(Destination.VersionSelector(packageName, patchesSelection))
},
onBackClick = { navController.pop() },
viewModel = getViewModel { parametersOf(destination.installedApp) }
) )
is Destination.Settings -> SettingsScreen( is Destination.Settings -> SettingsScreen(
@ -92,8 +86,15 @@ class MainActivity : ComponentActivity() {
is Destination.VersionSelector -> VersionSelectorScreen( is Destination.VersionSelector -> VersionSelectorScreen(
onBackClick = { navController.pop() }, onBackClick = { navController.pop() },
onAppClick = { navController.navigate(Destination.PatchesSelector(it)) }, onAppClick = { selectedApp ->
viewModel = getViewModel { parametersOf(destination.packageName) } navController.navigate(
Destination.PatchesSelector(
selectedApp,
destination.patchesSelection
)
)
},
viewModel = getViewModel { parametersOf(destination.packageName, destination.patchesSelection) }
) )
is Destination.PatchesSelector -> PatchesSelectorScreen( is Destination.PatchesSelector -> PatchesSelectorScreen(
@ -107,7 +108,7 @@ class MainActivity : ComponentActivity() {
) )
) )
}, },
vm = getViewModel { parametersOf(destination.selectedApp) } vm = getViewModel { parametersOf(destination) }
) )
is Destination.Installer -> InstallerScreen( is Destination.Installer -> InstallerScreen(

View File

@ -5,8 +5,12 @@ import app.revanced.manager.di.*
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import coil.Coil
import coil.ImageLoader
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
@ -36,6 +40,16 @@ class ManagerApplication : Application() {
) )
} }
val pixels = 512
Coil.setImageLoader(
ImageLoader.Builder(this)
.components {
add(AppIconKeyer())
add(AppIconFetcher.Factory(pixels, true, this@ManagerApplication))
}
.build()
)
scope.launch { scope.launch {
prefs.preload() prefs.preload()
} }

View File

@ -3,8 +3,11 @@ package app.revanced.manager.data.room
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import app.revanced.manager.data.room.apps.AppDao import app.revanced.manager.data.room.apps.downloaded.DownloadedAppDao
import app.revanced.manager.data.room.apps.DownloadedApp import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
import app.revanced.manager.data.room.apps.installed.AppliedPatch
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.data.room.apps.installed.InstalledAppDao
import app.revanced.manager.data.room.selection.PatchSelection import app.revanced.manager.data.room.selection.PatchSelection
import app.revanced.manager.data.room.selection.SelectedPatch import app.revanced.manager.data.room.selection.SelectedPatch
import app.revanced.manager.data.room.selection.SelectionDao import app.revanced.manager.data.room.selection.SelectionDao
@ -12,12 +15,13 @@ import app.revanced.manager.data.room.bundles.PatchBundleDao
import app.revanced.manager.data.room.bundles.PatchBundleEntity import app.revanced.manager.data.room.bundles.PatchBundleEntity
import kotlin.random.Random import kotlin.random.Random
@Database(entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class], version = 1) @Database(entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class], version = 1)
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun patchBundleDao(): PatchBundleDao abstract fun patchBundleDao(): PatchBundleDao
abstract fun selectionDao(): SelectionDao abstract fun selectionDao(): SelectionDao
abstract fun appDao(): AppDao abstract fun downloadedAppDao(): DownloadedAppDao
abstract fun installedAppDao(): InstalledAppDao
companion object { companion object {
fun generateUid() = Random.Default.nextInt() fun generateUid() = Random.Default.nextInt()

View File

@ -1,4 +1,4 @@
package app.revanced.manager.data.room.apps package app.revanced.manager.data.room.apps.downloaded
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity

View File

@ -1,4 +1,4 @@
package app.revanced.manager.data.room.apps package app.revanced.manager.data.room.apps.downloaded
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Delete import androidx.room.Delete
@ -7,7 +7,7 @@ import androidx.room.Query
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface AppDao { interface DownloadedAppDao {
@Query("SELECT * FROM downloaded_app") @Query("SELECT * FROM downloaded_app")
fun getAllApps(): Flow<List<DownloadedApp>> fun getAllApps(): Flow<List<DownloadedApp>>

View File

@ -0,0 +1,34 @@
package app.revanced.manager.data.room.apps.installed
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import app.revanced.manager.data.room.bundles.PatchBundleEntity
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(
tableName = "applied_patch",
primaryKeys = ["package_name", "bundle", "patch_name"],
foreignKeys = [
ForeignKey(
InstalledApp::class,
parentColumns = ["current_package_name"],
childColumns = ["package_name"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
PatchBundleEntity::class,
parentColumns = ["uid"],
childColumns = ["bundle"]
)
],
indices = [Index(value = ["bundle"], unique = false)]
)
data class AppliedPatch(
@ColumnInfo(name = "package_name") val packageName: String,
@ColumnInfo(name = "bundle") val bundle: Int,
@ColumnInfo(name = "patch_name") val patchName: String
) : Parcelable

View File

@ -0,0 +1,23 @@
package app.revanced.manager.data.room.apps.installed
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import app.revanced.manager.R
import kotlinx.parcelize.Parcelize
enum class InstallType(val stringResource: Int) {
DEFAULT(R.string.default_install),
ROOT(R.string.root_install)
}
@Parcelize
@Entity(tableName = "installed_app")
data class InstalledApp(
@PrimaryKey
@ColumnInfo(name = "current_package_name") val currentPackageName: String,
@ColumnInfo(name = "original_package_name") val originalPackageName: String,
@ColumnInfo(name = "version") val version: String,
@ColumnInfo(name = "install_type") val installType: InstallType
) : Parcelable

View File

@ -0,0 +1,40 @@
package app.revanced.manager.data.room.apps.installed
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.MapInfo
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow
@Dao
interface InstalledAppDao {
@Query("SELECT * FROM installed_app")
fun getAll(): Flow<List<InstalledApp>>
@Query("SELECT * FROM installed_app WHERE current_package_name = :packageName")
suspend fun get(packageName: String): InstalledApp?
@MapInfo(keyColumn = "bundle", valueColumn = "patch_name")
@Query(
"SELECT bundle, patch_name FROM applied_patch" +
" WHERE package_name = :packageName"
)
suspend fun getPatchesSelection(packageName: String): Map<Int, List<String>>
@Transaction
suspend fun insertApp(installedApp: InstalledApp, appliedPatches: List<AppliedPatch>) {
insertApp(installedApp)
insertAppliedPatches(appliedPatches)
}
@Insert
suspend fun insertApp(installedApp: InstalledApp)
@Insert
suspend fun insertAppliedPatches(appliedPatches: List<AppliedPatch>)
@Delete
suspend fun delete(installedApp: InstalledApp)
}

View File

@ -18,4 +18,5 @@ val repositoryModule = module {
singleOf(::PatchBundleRepository) singleOf(::PatchBundleRepository)
singleOf(::WorkerRepository) singleOf(::WorkerRepository)
singleOf(::DownloadedAppRepository) singleOf(::DownloadedAppRepository)
singleOf(::InstalledAppRepository)
} }

View File

@ -19,4 +19,6 @@ val viewModelModule = module {
viewModelOf(::ImportExportViewModel) viewModelOf(::ImportExportViewModel)
viewModelOf(::ContributorViewModel) viewModelOf(::ContributorViewModel)
viewModelOf(::DownloadsViewModel) viewModelOf(::DownloadsViewModel)
viewModelOf(::InstalledAppsViewModel)
viewModelOf(::AppInfoViewModel)
} }

View File

@ -1,14 +1,14 @@
package app.revanced.manager.domain.repository package app.revanced.manager.domain.repository
import app.revanced.manager.data.room.AppDatabase import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.apps.DownloadedApp import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import java.io.File import java.io.File
class DownloadedAppRepository( class DownloadedAppRepository(
db: AppDatabase db: AppDatabase
) { ) {
private val dao = db.appDao() private val dao = db.downloadedAppDao()
fun getAll() = dao.getAllApps().distinctUntilChanged() fun getAll() = dao.getAllApps().distinctUntilChanged()

View File

@ -0,0 +1,51 @@
package app.revanced.manager.domain.repository
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.apps.installed.AppliedPatch
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.util.PatchesSelection
import kotlinx.coroutines.flow.distinctUntilChanged
class InstalledAppRepository(
db: AppDatabase
) {
private val dao = db.installedAppDao()
fun getAll() = dao.getAll().distinctUntilChanged()
suspend fun get(packageName: String) = dao.get(packageName)
suspend fun getAppliedPatches(packageName: String): PatchesSelection =
dao.getPatchesSelection(packageName).mapValues { (_, patches) -> patches.toSet() }
suspend fun add(
currentPackageName: String,
originalPackageName: String,
version: String,
installType: InstallType,
patchesSelection: PatchesSelection
) {
dao.insertApp(
InstalledApp(
currentPackageName = currentPackageName,
originalPackageName = originalPackageName,
version = version,
installType = installType
),
patchesSelection.flatMap { (uid, patches) ->
patches.map { patch ->
AppliedPatch(
packageName = currentPackageName,
bundle = uid,
patchName = patch
)
}
}
)
}
suspend fun delete(installedApp: InstalledApp) {
dao.delete(installedApp)
}
}

View File

@ -14,7 +14,10 @@ class UninstallService : Service() {
flags: Int, flags: Int,
startId: Int startId: Int
): Int { ): Int {
when (intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) { val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
when (extraStatus) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> { PackageInstaller.STATUS_PENDING_USER_ACTION -> {
startActivity(if (Build.VERSION.SDK_INT >= 33) { startActivity(if (Build.VERSION.SDK_INT >= 33) {
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java) intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
@ -28,6 +31,9 @@ class UninstallService : Service() {
else -> { else -> {
sendBroadcast(Intent().apply { sendBroadcast(Intent().apply {
action = APP_UNINSTALL_ACTION action = APP_UNINSTALL_ACTION
putExtra(EXTRA_UNINSTALL_STATUS, extraStatus)
putExtra(EXTRA_UNINSTALL_STATUS_MESSAGE, extraStatusMessage)
}) })
} }
} }
@ -39,6 +45,9 @@ class UninstallService : Service() {
companion object { companion object {
const val APP_UNINSTALL_ACTION = "APP_UNINSTALL_ACTION" const val APP_UNINSTALL_ACTION = "APP_UNINSTALL_ACTION"
const val EXTRA_UNINSTALL_STATUS = "EXTRA_UNINSTALL_STATUS"
const val EXTRA_UNINSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE"
} }
} }

View File

@ -25,7 +25,7 @@ fun AppLabel(
packageInfo: PackageInfo?, packageInfo: PackageInfo?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
style: TextStyle = LocalTextStyle.current, style: TextStyle = LocalTextStyle.current,
defaultText: String = stringResource(R.string.not_installed) defaultText: String? = stringResource(R.string.not_installed)
) { ) {
val context = LocalContext.current val context = LocalContext.current

View File

@ -12,18 +12,26 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Composable @Composable
fun LoadingIndicator(progress: Float? = null, text: String? = null) { fun LoadingIndicator(
modifier: Modifier = Modifier,
progress: Float? = null,
text: String? = null
) {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
if (text != null) text?.let { Text(text) }
Text(text)
if (progress == null) { progress?.let {
CircularProgressIndicator(modifier = Modifier.padding(vertical = 16.dp)) CircularProgressIndicator(
} else { progress = progress,
CircularProgressIndicator(progress = progress, modifier = Modifier.padding(vertical = 16.dp)) modifier = Modifier.padding(vertical = 16.dp).then(modifier)
} )
} ?:
CircularProgressIndicator(
modifier = Modifier.padding(vertical = 16.dp).then(modifier)
)
} }
} }

View File

@ -0,0 +1,68 @@
package app.revanced.manager.ui.component
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
/**
* Credits to [Vendetta](https://github.com/vendetta-mod)
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RowScope.SegmentedButton(
icon: Any,
iconDescription: String? = null,
text: String,
onClick: () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
modifier = Modifier
.clickable(onClick = onClick)
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp))
.weight(1f)
.padding(vertical = 20.dp)
) {
when (icon) {
is ImageVector -> {
Icon(
imageVector = icon,
contentDescription = iconDescription,
tint = MaterialTheme.colorScheme.primary
)
}
is Painter -> {
Icon(
painter = icon,
contentDescription = iconDescription,
tint = MaterialTheme.colorScheme.primary
)
}
}
Text(
text = text,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
maxLines = 1,
modifier = Modifier.basicMarquee()
)
}
}

View File

@ -1,6 +1,7 @@
package app.revanced.manager.ui.destination package app.revanced.manager.ui.destination
import android.os.Parcelable import android.os.Parcelable
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection import app.revanced.manager.util.PatchesSelection
@ -12,6 +13,9 @@ sealed interface Destination : Parcelable {
@Parcelize @Parcelize
object Dashboard : Destination object Dashboard : Destination
@Parcelize
data class ApplicationInfo(val installedApp: InstalledApp) : Destination
@Parcelize @Parcelize
object AppSelector : Destination object AppSelector : Destination
@ -19,11 +23,12 @@ sealed interface Destination : Parcelable {
object Settings : Destination object Settings : Destination
@Parcelize @Parcelize
data class VersionSelector(val packageName: String) : Destination data class VersionSelector(val packageName: String, val patchesSelection: PatchesSelection? = null) : Destination
@Parcelize @Parcelize
data class PatchesSelector(val selectedApp: SelectedApp) : Destination data class PatchesSelector(val selectedApp: SelectedApp, val patchesSelection: PatchesSelection? = null) : Destination
@Parcelize @Parcelize
data class Installer(val selectedApp: SelectedApp, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination data class Installer(val selectedApp: SelectedApp, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination
} }

View File

@ -0,0 +1,158 @@
package app.revanced.manager.ui.screen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowRight
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.OpenInNew
import androidx.compose.material.icons.outlined.Update
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppIcon
import app.revanced.manager.ui.component.AppLabel
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.SegmentedButton
import app.revanced.manager.ui.viewmodel.AppInfoViewModel
import app.revanced.manager.util.PatchesSelection
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppInfoScreen(
onPatchClick: (packageName: String, patchesSelection: PatchesSelection) -> Unit,
onBackClick: () -> Unit,
viewModel: AppInfoViewModel
) {
SideEffect {
viewModel.onBackClick = onBackClick
}
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.app_info),
onBackClick = onBackClick
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
AppIcon(
viewModel.appInfo,
contentDescription = null,
modifier = Modifier
.size(100.dp)
.padding(bottom = 5.dp)
)
AppLabel(
viewModel.appInfo,
style = MaterialTheme.typography.titleLarge,
defaultText = null
)
Text(viewModel.installedApp.version, style = MaterialTheme.typography.bodySmall)
}
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(24.dp))
) {
SegmentedButton(
icon = Icons.Outlined.OpenInNew,
text = stringResource(R.string.open_app),
onClick = viewModel::launch
)
SegmentedButton(
icon = Icons.Outlined.Delete,
text = stringResource(R.string.uninstall),
onClick = viewModel::uninstall
)
SegmentedButton(
icon = Icons.Outlined.Update,
text = stringResource(R.string.repatch),
onClick = {
viewModel.appliedPatches?.let {
onPatchClick(viewModel.installedApp.originalPackageName, it)
}
}
)
}
Column(
modifier = Modifier.padding(vertical = 16.dp)
) {
ListItem(
modifier = Modifier.clickable { },
headlineContent = { Text(stringResource(R.string.applied_patches)) },
supportingContent = {
Text(
(viewModel.appliedPatches?.values?.sumOf { it.size } ?: 0).let {
pluralStringResource(
id = R.plurals.applied_patches,
it,
it
)
}
)
},
trailingContent = { Icon(Icons.Filled.ArrowRight, contentDescription = stringResource(R.string.view_applied_patches)) }
)
ListItem(
headlineContent = { Text(stringResource(R.string.package_name)) },
supportingContent = { Text(viewModel.installedApp.currentPackageName) }
)
if (viewModel.installedApp.originalPackageName != viewModel.installedApp.currentPackageName) {
ListItem(
headlineContent = { Text(stringResource(R.string.original_package_name)) },
supportingContent = { Text(viewModel.installedApp.originalPackageName) }
)
}
ListItem(
headlineContent = { Text(stringResource(R.string.install_type)) },
supportingContent = { Text(stringResource(viewModel.installedApp.installType.stringResource)) }
)
}
}
}
}

View File

@ -12,16 +12,7 @@ import androidx.compose.material.icons.outlined.Apps
import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Source import androidx.compose.material.icons.outlined.Source
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.*
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -35,6 +26,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.bundle.ImportBundleDialog import app.revanced.manager.ui.component.bundle.ImportBundleDialog
import app.revanced.manager.ui.viewmodel.DashboardViewModel import app.revanced.manager.ui.viewmodel.DashboardViewModel
@ -56,6 +48,7 @@ fun DashboardScreen(
vm: DashboardViewModel = getViewModel(), vm: DashboardViewModel = getViewModel(),
onAppSelectorClick: () -> Unit, onAppSelectorClick: () -> Unit,
onSettingsClick: () -> Unit, onSettingsClick: () -> Unit,
onAppClick: (InstalledApp) -> Unit
) { ) {
var showImportBundleDialog by rememberSaveable { mutableStateOf(false) } var showImportBundleDialog by rememberSaveable { mutableStateOf(false) }
val pages: Array<DashboardPage> = DashboardPage.values() val pages: Array<DashboardPage> = DashboardPage.values()
@ -150,7 +143,9 @@ fun DashboardScreen(
pageContent = { index -> pageContent = { index ->
when (pages[index]) { when (pages[index]) {
DashboardPage.DASHBOARD -> { DashboardPage.DASHBOARD -> {
InstalledAppsScreen() InstalledAppsScreen(
onAppClick = onAppClick
)
} }
DashboardPage.BUNDLES -> { DashboardPage.BUNDLES -> {

View File

@ -1,21 +1,73 @@
package app.revanced.manager.ui.screen package app.revanced.manager.ui.screen
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
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.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.ui.component.AppIcon
import app.revanced.manager.ui.component.AppLabel
import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.viewmodel.InstalledAppsViewModel
import org.koin.androidx.compose.getViewModel
@Composable @Composable
fun InstalledAppsScreen() { fun InstalledAppsScreen(
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { onAppClick: (InstalledApp) -> Unit,
Text( viewModel: InstalledAppsViewModel = getViewModel()
text = stringResource(R.string.no_patched_apps_found), ) {
style = MaterialTheme.typography.titleLarge val installedApps by viewModel.apps.collectAsStateWithLifecycle(initialValue = null)
)
LazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = installedApps?.let { if (it.isEmpty()) Arrangement.Center else Arrangement.Top } ?: Arrangement.Center
) {
installedApps?.let { installedApps ->
if (installedApps.isNotEmpty()) {
items(
installedApps,
key = { it.currentPackageName }
) { installedApp ->
viewModel.packageInfoMap[installedApp.currentPackageName].let { packageInfo ->
ListItem(
modifier = Modifier.clickable { onAppClick(installedApp) },
leadingContent = {
AppIcon(
packageInfo,
contentDescription = null,
Modifier.size(36.dp)
)
},
headlineContent = { AppLabel(packageInfo, defaultText = null) },
supportingContent = { Text(installedApp.currentPackageName) }
)
}
}
} else {
item {
Text(
text = stringResource(R.string.no_patched_apps_found),
style = MaterialTheme.typography.titleLarge
)
}
}
} ?: item { LoadingIndicator() }
} }
} }

View File

@ -70,7 +70,7 @@ fun PatchesSelectorScreen(
if (vm.compatibleVersions.isNotEmpty()) if (vm.compatibleVersions.isNotEmpty())
UnsupportedDialog( UnsupportedDialog(
appVersion = vm.selectedApp.version, appVersion = vm.input.selectedApp.version,
supportedVersions = vm.compatibleVersions, supportedVersions = vm.compatibleVersions,
onDismissRequest = vm::dismissDialogs onDismissRequest = vm::dismissDialogs
) )

View File

@ -27,6 +27,7 @@ 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.draw.alpha
import androidx.compose.ui.res.pluralStringResource 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
@ -89,7 +90,7 @@ fun VersionSelectorScreen(
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
viewModel.installedApp?.let { packageInfo -> viewModel.installedApp?.let { (packageInfo, alreadyPatched) ->
SelectedApp.Installed( SelectedApp.Installed(
packageName = viewModel.packageName, packageName = viewModel.packageName,
version = packageInfo.versionName version = packageInfo.versionName
@ -98,7 +99,8 @@ fun VersionSelectorScreen(
selectedApp = it, selectedApp = it,
selected = selectedVersion == it, selected = selectedVersion == it,
onClick = { selectedVersion = it }, onClick = { selectedVersion = it },
patchCount = supportedVersions[it.version] patchCount = supportedVersions[it.version],
alreadyPatched = alreadyPatched
) )
} }
} }
@ -132,14 +134,13 @@ fun VersionSelectorScreen(
} }
} }
const val alreadyPatched = false
@Composable @Composable
fun SelectedAppItem( fun SelectedAppItem(
selectedApp: SelectedApp, selectedApp: SelectedApp,
selected: Boolean, selected: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
patchCount: Int? patchCount: Int?,
alreadyPatched: Boolean = false
) { ) {
ListItem( ListItem(
leadingContent = { RadioButton(selected, null) }, leadingContent = { RadioButton(selected, null) },
@ -161,6 +162,11 @@ fun SelectedAppItem(
trailingContent = patchCount?.let { { trailingContent = patchCount?.let { {
Text(pluralStringResource(R.plurals.patches_count, it, it)) Text(pluralStringResource(R.plurals.patches_count, it, it))
} }, } },
modifier = Modifier.clickable(onClick = onClick) modifier = Modifier
.clickable(enabled = !alreadyPatched, onClick = onClick)
.run {
if (alreadyPatched) alpha(0.5f)
else this
}
) )
} }

View File

@ -0,0 +1,96 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
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 androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.service.UninstallService
import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class AppInfoViewModel(
val installedApp: InstalledApp
) : ViewModel(), KoinComponent {
private val app: Application by inject()
private val pm: PM by inject()
private val installedAppRepository: InstalledAppRepository by inject()
lateinit var onBackClick: () -> Unit
var appInfo: PackageInfo? by mutableStateOf(null)
private set
var appliedPatches: PatchesSelection? by mutableStateOf(null)
fun launch() = pm.launch(installedApp.currentPackageName)
fun uninstall() {
when (installedApp.installType) {
InstallType.DEFAULT -> pm.uninstallPackage(installedApp.currentPackageName)
InstallType.ROOT -> TODO()
}
}
private val uninstallBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
UninstallService.APP_UNINSTALL_ACTION -> {
val extraStatus = intent.getIntExtra(UninstallService.EXTRA_UNINSTALL_STATUS, -999)
val extraStatusMessage = intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
viewModelScope.launch {
installedAppRepository.delete(installedApp)
withContext(Dispatchers.Main) { onBackClick() }
}
} else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) {
app.toast(app.getString(R.string.uninstall_app_fail, extraStatusMessage))
}
}
}
}
}
init {
viewModelScope.launch {
appInfo = withContext(Dispatchers.IO) {
pm.getPackageInfo(installedApp.currentPackageName)
}
}
viewModelScope.launch {
appliedPatches = withContext(Dispatchers.IO) {
installedAppRepository.getAppliedPatches(installedApp.currentPackageName)
}
}
app.registerReceiver(
uninstallBroadcastReceiver,
IntentFilter(UninstallService.APP_UNINSTALL_ACTION)
)
}
override fun onCleared() {
super.onCleared()
app.unregisterReceiver(uninstallBroadcastReceiver)
}
}

View File

@ -2,7 +2,7 @@ package app.revanced.manager.ui.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.data.room.apps.DownloadedApp import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloadedAppRepository import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.util.mutableStateSetOf import app.revanced.manager.util.mutableStateSetOf

View File

@ -0,0 +1,33 @@
package app.revanced.manager.ui.viewmodel
import android.content.pm.PackageInfo
import androidx.compose.runtime.mutableStateMapOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.util.PM
import app.revanced.manager.util.collectEach
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class InstalledAppsViewModel(
private val installedAppsRepository: InstalledAppRepository,
private val pm: PM
) : ViewModel() {
val apps = installedAppsRepository.getAll().flowOn(Dispatchers.IO)
val packageInfoMap = mutableStateMapOf<String, PackageInfo?>()
init {
viewModelScope.launch {
apps.collectEach { installedApp ->
packageInfoMap[installedApp.currentPackageName] = withContext(Dispatchers.IO) {
pm.getPackageInfo(installedApp.currentPackageName)
.also { if (it == null) installedAppsRepository.delete(installedApp) }
}
}
}
}
}

View File

@ -18,8 +18,10 @@ import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.worker.WorkerRepository import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.patcher.worker.PatcherProgressManager import app.revanced.manager.patcher.worker.PatcherProgressManager
import app.revanced.manager.patcher.worker.PatcherWorker import app.revanced.manager.patcher.worker.PatcherWorker
@ -50,6 +52,7 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
private val app: Application by inject() private val app: Application by inject()
private val pm: PM by inject() private val pm: PM by inject()
private val workerRepository: WorkerRepository by inject() private val workerRepository: WorkerRepository by inject()
private val installedAppReceiver: InstalledAppRepository by inject()
val packageName: String = input.selectedApp.packageName val packageName: String = input.selectedApp.packageName
private val outputFile = File(app.cacheDir, "output.apk") private val outputFile = File(app.cacheDir, "output.apk")
@ -113,6 +116,15 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
app.toast(app.getString(R.string.install_app_success)) app.toast(app.getString(R.string.install_app_success))
installedPackageName = installedPackageName =
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME) intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
viewModelScope.launch {
installedAppReceiver.add(
installedPackageName!!,
packageName,
input.selectedApp.version,
InstallType.DEFAULT,
input.selectedPatches
)
}
} else { } else {
app.toast(app.getString(R.string.install_app_fail, extra)) app.toast(app.getString(R.string.install_app_fail, extra))
} }

View File

@ -17,7 +17,7 @@ import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.SnapshotStateSet import app.revanced.manager.util.SnapshotStateSet
@ -37,7 +37,7 @@ import org.koin.core.component.get
@Stable @Stable
@OptIn(SavedStateHandleSaveableApi::class) @OptIn(SavedStateHandleSaveableApi::class)
class PatchesSelectorViewModel( class PatchesSelectorViewModel(
val selectedApp: SelectedApp val input: Destination.PatchesSelector
) : ViewModel(), KoinComponent { ) : ViewModel(), KoinComponent {
private val selectionRepository: PatchSelectionRepository = get() private val selectionRepository: PatchSelectionRepository = get()
private val savedStateHandle: SavedStateHandle = get() private val savedStateHandle: SavedStateHandle = get()
@ -54,10 +54,10 @@ class PatchesSelectorViewModel(
val unsupported = mutableListOf<PatchInfo>() val unsupported = mutableListOf<PatchInfo>()
val universal = mutableListOf<PatchInfo>() val universal = mutableListOf<PatchInfo>()
bundle.patches.filter { it.compatibleWith(selectedApp.packageName) }.forEach { bundle.patches.filter { it.compatibleWith(input.selectedApp.packageName) }.forEach {
val targetList = when { val targetList = when {
it.compatiblePackages == null -> universal it.compatiblePackages == null -> universal
it.supportsVersion(selectedApp.version) -> supported it.supportsVersion(input.selectedApp.version) -> supported
else -> unsupported else -> unsupported
} }
@ -75,7 +75,8 @@ class PatchesSelectorViewModel(
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
val bundles = bundlesFlow.first() val bundles = bundlesFlow.first()
val filteredSelection = val filteredSelection =
selectionRepository.getSelection(selectedApp.packageName) (input.patchesSelection
?: selectionRepository.getSelection(input.selectedApp.packageName))
.mapValues { (uid, patches) -> .mapValues { (uid, patches) ->
// Filter out patches that don't exist. // Filter out patches that don't exist.
val filteredPatches = bundles.singleOrNull { it.uid == uid } val filteredPatches = bundles.singleOrNull { it.uid == uid }
@ -125,7 +126,7 @@ class PatchesSelectorViewModel(
suspend fun getAndSaveSelection(): PatchesSelection = suspend fun getAndSaveSelection(): PatchesSelection =
selectedPatches.also { selectedPatches.also {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
selectionRepository.updateSelection(selectedApp.packageName, it) selectionRepository.updateSelection(input.selectedApp.packageName, it)
} }
}.mapValues { it.value.toMutableSet() }.apply { }.mapValues { it.value.toMutableSet() }.apply {
if (allowExperimental.get()) { if (allowExperimental.get()) {
@ -158,7 +159,7 @@ class PatchesSelectorViewModel(
val set = HashSet<String>() val set = HashSet<String>()
unsupportedVersions.forEach { patch -> unsupportedVersions.forEach { patch ->
patch.compatiblePackages?.find { it.packageName == selectedApp.packageName } patch.compatiblePackages?.find { it.packageName == input.selectedApp.packageName }
?.let { compatiblePackage -> ?.let { compatiblePackage ->
set.addAll(compatiblePackage.versions) set.addAll(compatiblePackage.versions)
} }

View File

@ -7,7 +7,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.domain.repository.DownloadedAppRepository import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.network.downloader.APKMirror import app.revanced.manager.network.downloader.APKMirror
import app.revanced.manager.network.downloader.AppDownloader import app.revanced.manager.network.downloader.AppDownloader
@ -17,6 +19,7 @@ import app.revanced.manager.util.mutableStateSetOf
import app.revanced.manager.util.simpleMessage import app.revanced.manager.util.simpleMessage
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -28,11 +31,12 @@ class VersionSelectorViewModel(
val packageName: String val packageName: String
) : ViewModel(), KoinComponent { ) : ViewModel(), KoinComponent {
private val downloadedAppRepository: DownloadedAppRepository by inject() private val downloadedAppRepository: DownloadedAppRepository by inject()
private val installedAppRepository: InstalledAppRepository by inject()
private val patchBundleRepository: PatchBundleRepository by inject() private val patchBundleRepository: PatchBundleRepository by inject()
private val pm: PM by inject() private val pm: PM by inject()
private val appDownloader: AppDownloader = APKMirror() private val appDownloader: AppDownloader = APKMirror()
var installedApp: PackageInfo? by mutableStateOf(null) var installedApp: Pair<PackageInfo, Boolean>? by mutableStateOf(null)
private set private set
var isLoading by mutableStateOf(true) var isLoading by mutableStateOf(true)
private set private set
@ -67,7 +71,17 @@ class VersionSelectorViewModel(
init { init {
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Main) {
installedApp = withContext(Dispatchers.IO) { pm.getPackageInfo(packageName) } val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
val alreadyPatched = async(Dispatchers.IO) {
installedAppRepository.get(packageName)
?.let { it.installType == InstallType.DEFAULT }
?: false
}
installedApp =
packageInfo.await()?.let {
it to alreadyPatched.await()
}
} }
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {

View File

@ -53,21 +53,18 @@ class PM(
.eachCount() .eachCount()
compatiblePackages.keys.map { pkg -> compatiblePackages.keys.map { pkg ->
try { getPackageInfo(pkg)?.let { packageInfo ->
val packageInfo = app.packageManager.getPackageInfo(pkg, 0)
AppInfo( AppInfo(
pkg, pkg,
compatiblePackages[pkg], compatiblePackages[pkg],
packageInfo, packageInfo,
File(packageInfo.applicationInfo.sourceDir) File(packageInfo.applicationInfo.sourceDir)
) )
} catch (e: NameNotFoundException) { } ?: AppInfo(
AppInfo( pkg,
pkg, compatiblePackages[pkg],
compatiblePackages[pkg], null
null )
)
}
} }
} }

View File

@ -89,4 +89,12 @@ val Color.hexCode: String
val g: Int = (green * 255).toInt() val g: Int = (green * 255).toInt()
val b: Int = (blue * 255).toInt() val b: Int = (blue * 255).toInt()
return java.lang.String.format(Locale.getDefault(), "%02X%02X%02X%02X", r, g, b, a) return java.lang.String.format(Locale.getDefault(), "%02X%02X%02X%02X", r, g, b, a)
} }
suspend fun <T> Flow<Iterable<T>>.collectEach(block: suspend (T) -> Unit) {
this.collect { iterable ->
iterable.forEach {
block(it)
}
}
}

View File

@ -4,4 +4,8 @@
<item quantity="one">%d Patch</item> <item quantity="one">%d Patch</item>
<item quantity="other">%d Patches</item> <item quantity="other">%d Patches</item>
</plurals> </plurals>
<plurals name="applied_patches">
<item quantity="one">%d applied patch</item>
<item quantity="other">%d applied patches</item>
</plurals>
</resources> </resources>

View File

@ -154,6 +154,17 @@
<string name="loading">Loading…</string> <string name="loading">Loading…</string>
<string name="not_installed">Not installed</string> <string name="not_installed">Not installed</string>
<string name="app_info">App info</string>
<string name="uninstall">Uninstall</string>
<string name="repatch">Repatch</string>
<string name="install_type">Installation type</string>
<string name="package_name">Package name</string>
<string name="original_package_name">Original package name</string>
<string name="applied_patches">Applied patches</string>
<string name="view_applied_patches">View applied patches</string>
<string name="default_install">Default</string>
<string name="root_install">Root</string>
<string name="error_occurred">An error occurred</string> <string name="error_occurred">An error occurred</string>
<string name="already_downloaded">Already downloaded</string> <string name="already_downloaded">Already downloaded</string>
@ -173,6 +184,7 @@
<string name="install_app">Install</string> <string name="install_app">Install</string>
<string name="install_app_success">App installed</string> <string name="install_app_success">App installed</string>
<string name="install_app_fail">Failed to install app: %s</string> <string name="install_app_fail">Failed to install app: %s</string>
<string name="uninstall_app_fail">Failed to uninstall app: %s</string>
<string name="open_app">Open</string> <string name="open_app">Open</string>
<string name="export_app">Export</string> <string name="export_app">Export</string>
<string name="export_app_success">Apk exported</string> <string name="export_app_success">Apk exported</string>