mirror of
https://github.com/revanced/revanced-manager-compose
synced 2024-06-02 19:36:16 +02:00
feat: store patched apps
This commit is contained in:
parent
1630668360
commit
3d8453f693
|
@ -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": [],
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
|
@ -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>>
|
||||
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -18,4 +18,5 @@ val repositoryModule = module {
|
|||
singleOf(::PatchBundleRepository)
|
||||
singleOf(::WorkerRepository)
|
||||
singleOf(::DownloadedAppRepository)
|
||||
singleOf(::InstalledAppRepository)
|
||||
}
|
|
@ -19,4 +19,6 @@ val viewModelModule = module {
|
|||
viewModelOf(::ImportExportViewModel)
|
||||
viewModelOf(::ContributorViewModel)
|
||||
viewModelOf(::DownloadsViewModel)
|
||||
viewModelOf(::InstalledAppsViewModel)
|
||||
viewModelOf(::AppInfoViewModel)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 -> {
|
||||
|
|
|
@ -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) }
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user