feat: store patched apps

This commit is contained in:
CnC-Robert 2023-08-09 11:40:43 +02:00
parent 1630668360
commit 3d8453f693
No known key found for this signature in database
GPG Key ID: C58ED617AEA8CB68
28 changed files with 649 additions and 65 deletions

View File

@ -190,6 +190,44 @@
},
"indices": [],
"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": []
}
],
"views": [],

View File

@ -9,6 +9,7 @@ import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import app.revanced.manager.ui.component.AutoUpdatesDialog
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.AppSelectorScreen
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.Theme
import app.revanced.manager.ui.viewmodel.MainViewModel
import coil.Coil
import coil.ImageLoader
import dev.olshevski.navigation.reimagined.AnimatedNavHost
import dev.olshevski.navigation.reimagined.NavBackHandler
import dev.olshevski.navigation.reimagined.navigate
import dev.olshevski.navigation.reimagined.pop
import dev.olshevski.navigation.reimagined.popUpTo
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.core.parameter.parametersOf
import kotlin.math.roundToInt
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
class MainActivity : ComponentActivity() {
@ -42,17 +38,6 @@ class MainActivity : ComponentActivity() {
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 {
val theme by vm.prefs.theme.getAsState()
val dynamicColor by vm.prefs.dynamicColor.getAsState()
@ -77,7 +62,14 @@ class MainActivity : ComponentActivity() {
when (destination) {
is Destination.Dashboard -> DashboardScreen(
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 -> navController.navigate(Destination.VersionSelector(packageName)) },
onBackClick = { navController.pop() },
viewModel = getViewModel { parametersOf(destination.installedApp) }
)
is Destination.Settings -> SettingsScreen(

View File

@ -5,8 +5,12 @@ import app.revanced.manager.di.*
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository
import kotlinx.coroutines.Dispatchers
import coil.Coil
import coil.ImageLoader
import kotlinx.coroutines.MainScope
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.koin.androidContext
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 {
prefs.preload()
}

View File

@ -3,8 +3,10 @@ package app.revanced.manager.data.room
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import app.revanced.manager.data.room.apps.AppDao
import app.revanced.manager.data.room.apps.DownloadedApp
import app.revanced.manager.data.room.apps.downloaded.DownloadedAppDao
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
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.SelectedPatch
import app.revanced.manager.data.room.selection.SelectionDao
@ -12,12 +14,13 @@ import app.revanced.manager.data.room.bundles.PatchBundleDao
import app.revanced.manager.data.room.bundles.PatchBundleEntity
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], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun patchBundleDao(): PatchBundleDao
abstract fun selectionDao(): SelectionDao
abstract fun appDao(): AppDao
abstract fun downloadedAppDao(): DownloadedAppDao
abstract fun installedAppDao(): InstalledAppDao
companion object {
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.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.Delete
@ -7,7 +7,7 @@ import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface AppDao {
interface DownloadedAppDao {
@Query("SELECT * FROM downloaded_app")
fun getAllApps(): Flow<List<DownloadedApp>>

View File

@ -0,0 +1,22 @@
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 kotlinx.parcelize.Parcelize
enum class InstallType(val displayName: String) {
DEFAULT("Default"),
ROOT("Root")
}
@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,22 @@
package app.revanced.manager.data.room.apps.installed
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface InstalledAppDao {
@Query("SELECT * FROM installed_app")
fun getAllApps(): Flow<List<InstalledApp>>
@Query("SELECT * FROM installed_app WHERE current_package_name = :packageName")
suspend fun get(packageName: String): InstalledApp?
@Insert
suspend fun insert(installedApp: InstalledApp)
@Delete
suspend fun delete(installedApp: InstalledApp)
}

View File

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

View File

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

View File

@ -1,14 +1,14 @@
package app.revanced.manager.domain.repository
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 java.io.File
class DownloadedAppRepository(
db: AppDatabase
) {
private val dao = db.appDao()
private val dao = db.downloadedAppDao()
fun getAll() = dao.getAllApps().distinctUntilChanged()

View File

@ -0,0 +1,34 @@
package app.revanced.manager.domain.repository
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import kotlinx.coroutines.flow.distinctUntilChanged
class InstalledAppRepository(
db: AppDatabase
) {
private val dao = db.installedAppDao()
fun getAll() = dao.getAllApps().distinctUntilChanged()
suspend fun get(packageName: String) = dao.get(packageName)
suspend fun add(
currentPackageName: String,
originalPackageName: String,
version: String,
installType: InstallType
) = dao.insert(
InstalledApp(
currentPackageName = currentPackageName,
originalPackageName = originalPackageName,
version = version,
installType = installType
)
)
suspend fun delete(installedApp: InstalledApp) {
dao.delete(installedApp)
}
}

View File

@ -14,7 +14,10 @@ class UninstallService : Service() {
flags: Int,
startId: 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 -> {
startActivity(if (Build.VERSION.SDK_INT >= 33) {
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
@ -28,6 +31,9 @@ class UninstallService : Service() {
else -> {
sendBroadcast(Intent().apply {
action = APP_UNINSTALL_ACTION
putExtra(EXTRA_UNINSTALL_STATUS, extraStatus)
putExtra(EXTRA_UNINSTALL_STATUS_MESSAGE, extraStatusMessage)
})
}
}
@ -39,6 +45,9 @@ class UninstallService : Service() {
companion object {
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?,
modifier: Modifier = Modifier,
style: TextStyle = LocalTextStyle.current,
defaultText: String = stringResource(R.string.not_installed)
defaultText: String? = stringResource(R.string.not_installed)
) {
val context = LocalContext.current

View File

@ -12,18 +12,26 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun LoadingIndicator(progress: Float? = null, text: String? = null) {
fun LoadingIndicator(
modifier: Modifier = Modifier,
progress: Float? = null,
text: String? = null
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
if (text != null)
Text(text)
if (progress == null) {
CircularProgressIndicator(modifier = Modifier.padding(vertical = 16.dp))
} else {
CircularProgressIndicator(progress = progress, modifier = Modifier.padding(vertical = 16.dp))
}
text?.let { Text(text) }
progress?.let {
CircularProgressIndicator(
progress = progress,
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
import android.os.Parcelable
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection
@ -12,6 +13,9 @@ sealed interface Destination : Parcelable {
@Parcelize
object Dashboard : Destination
@Parcelize
data class ApplicationInfo(val installedApp: InstalledApp) : Destination
@Parcelize
object AppSelector : Destination
@ -26,4 +30,5 @@ sealed interface Destination : Parcelable {
@Parcelize
data class Installer(val selectedApp: SelectedApp, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination
}

View File

@ -0,0 +1,138 @@
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
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppInfoScreen(
onPatchClick: (packageName: String) -> 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 = { onPatchClick(viewModel.installedApp.originalPackageName) }
)
}
Column(
modifier = Modifier.padding(vertical = 16.dp)
) {
ListItem(
modifier = Modifier.clickable { },
headlineContent = { Text(stringResource(R.string.applied_patches)) },
supportingContent = { Text(pluralStringResource(
id = R.plurals.applied_patches, 420, 420
)) },
trailingContent = { Icon(Icons.Filled.ArrowRight, contentDescription = null) }
)
ListItem(
headlineContent = { Text(stringResource(R.string.package_name)) },
supportingContent = { Text(viewModel.installedApp.currentPackageName) }
)
ListItem(
headlineContent = { Text(stringResource(R.string.install_type)) },
supportingContent = { Text(viewModel.installedApp.installType.displayName) }
)
}
}
}
}

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.Settings
import androidx.compose.material.icons.outlined.Source
import androidx.compose.material3.ExperimentalMaterial3Api
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.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -35,6 +26,7 @@ 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.data.room.apps.installed.InstalledApp
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.bundle.ImportBundleDialog
import app.revanced.manager.ui.viewmodel.DashboardViewModel
@ -56,6 +48,7 @@ fun DashboardScreen(
vm: DashboardViewModel = getViewModel(),
onAppSelectorClick: () -> Unit,
onSettingsClick: () -> Unit,
onAppClick: (InstalledApp) -> Unit
) {
var showImportBundleDialog by rememberSaveable { mutableStateOf(false) }
val pages: Array<DashboardPage> = DashboardPage.values()
@ -150,7 +143,9 @@ fun DashboardScreen(
pageContent = { index ->
when (pages[index]) {
DashboardPage.DASHBOARD -> {
InstalledAppsScreen()
InstalledAppsScreen(
onAppClick = onAppClick
)
}
DashboardPage.BUNDLES -> {

View File

@ -1,21 +1,105 @@
package app.revanced.manager.ui.screen
import androidx.compose.foundation.layout.Box
import android.content.pm.PackageInfo
import android.content.pm.PackageManager.NameNotFoundException
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
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.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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.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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.androidx.compose.getViewModel
@Composable
fun InstalledAppsScreen() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
text = stringResource(R.string.no_patched_apps_found),
style = MaterialTheme.typography.titleLarge
)
fun InstalledAppsScreen(
onAppClick: (InstalledApp) -> Unit,
viewModel: InstalledAppsViewModel = getViewModel()
) {
val installedApps by viewModel.apps.collectAsStateWithLifecycle(initialValue = null)
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
installedApps?.let { installedApps ->
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(
installedApps,
key = { it.currentPackageName }
) { installedApp ->
InstalledAppItem(
installedApp = installedApp,
onClick = { onAppClick(installedApp) },
deleteApp = { viewModel.delete(installedApp) }
)
}
}
installedApps.ifEmpty {
Text(
text = stringResource(R.string.no_patched_apps_found),
style = MaterialTheme.typography.titleLarge
)
}
} ?: LoadingIndicator()
}
}
@Suppress("DEPRECATION")
@Composable
fun InstalledAppItem(
installedApp: InstalledApp,
onClick: () -> Unit,
deleteApp: () -> Unit,
modifier: Modifier = Modifier
) {
val packageManager = LocalContext.current.packageManager
var appInfo: PackageInfo? by rememberSaveable { mutableStateOf(null) }
LaunchedEffect(Unit) {
try {
appInfo = withContext(Dispatchers.IO) {
packageManager.getPackageInfo(installedApp.currentPackageName, 0)
}
} catch (e: NameNotFoundException) {
deleteApp()
}
}
ListItem(
modifier = Modifier
.clickable(onClick = onClick)
.then(modifier),
leadingContent = { AppIcon(packageInfo = appInfo, contentDescription = null, Modifier.size(36.dp)) },
headlineContent = { AppLabel(packageInfo = appInfo, defaultText = null) },
supportingContent = { Text(installedApp.currentPackageName) }
)
}

View File

@ -27,6 +27,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@ -89,7 +90,7 @@ fun VersionSelectorScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
viewModel.installedApp?.let { packageInfo ->
viewModel.installedApp?.let { (packageInfo, alreadyPatched) ->
SelectedApp.Installed(
packageName = viewModel.packageName,
version = packageInfo.versionName
@ -98,7 +99,8 @@ fun VersionSelectorScreen(
selectedApp = it,
selected = 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
fun SelectedAppItem(
selectedApp: SelectedApp,
selected: Boolean,
onClick: () -> Unit,
patchCount: Int?
patchCount: Int?,
alreadyPatched: Boolean = false
) {
ListItem(
leadingContent = { RadioButton(selected, null) },
@ -161,6 +162,11 @@ fun SelectedAppItem(
trailingContent = patchCount?.let { {
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,84 @@
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.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
@Suppress("DEPRECATION")
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
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(Dispatchers.Main) {
appInfo = withContext(Dispatchers.IO) {
app.packageManager.getPackageInfo(installedApp.currentPackageName, 0)
}
}
app.registerReceiver(
uninstallBroadcastReceiver,
IntentFilter(UninstallService.APP_UNINSTALL_ACTION)
)
}
}

View File

@ -2,7 +2,7 @@ package app.revanced.manager.ui.viewmodel
import androidx.lifecycle.ViewModel
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.repository.DownloadedAppRepository
import app.revanced.manager.util.mutableStateSetOf

View File

@ -0,0 +1,21 @@
package app.revanced.manager.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.repository.InstalledAppRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
class InstalledAppsViewModel(
private val installedAppsRepository: InstalledAppRepository
) : ViewModel() {
val apps = installedAppsRepository.getAll().flowOn(Dispatchers.IO)
fun delete(installedApp: InstalledApp) {
viewModelScope.launch {
installedAppsRepository.delete(installedApp)
}
}
}

View File

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

View File

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

View File

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

View File

@ -153,6 +153,13 @@
<string name="loading">Loading…</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="applied_patches">Applied patches</string>
<string name="error_occurred">An error occurred</string>
<string name="already_downloaded">Already downloaded</string>
@ -165,8 +172,8 @@
<string name="install_app">Install</string>
<string name="install_app_success">App installed</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="export_app">Export</string>
<string name="export_app_success">Apk exported</string>
<string name="sign_fail">Failed to sign Apk: %s</string>
<string name="save_logs">Save logs</string>