mirror of
https://github.com/revanced/revanced-manager
synced 2024-05-14 13:56:57 +02:00
feat: root installation (#1243)
This commit is contained in:
parent
b4dfcf1bb4
commit
bf10af2ae2
@ -66,6 +66,7 @@ android {
|
||||
}
|
||||
|
||||
buildFeatures.compose = true
|
||||
buildFeatures.aidl = true
|
||||
|
||||
composeOptions.kotlinCompilerExtensionVersion = "1.5.1"
|
||||
}
|
||||
@ -124,6 +125,10 @@ dependencies {
|
||||
implementation(libs.apksign)
|
||||
implementation(libs.bcpkix.jdk18on)
|
||||
|
||||
implementation(libs.libsu.core)
|
||||
implementation(libs.libsu.service)
|
||||
implementation(libs.libsu.nio)
|
||||
|
||||
// Koin
|
||||
implementation(libs.koin.android)
|
||||
implementation(libs.koin.compose)
|
||||
|
@ -0,0 +1,8 @@
|
||||
// IRootService.aidl
|
||||
package app.revanced.manager;
|
||||
|
||||
// Declare any non-default types here with import statements
|
||||
|
||||
interface IRootSystemService {
|
||||
IBinder getFileSystemService();
|
||||
}
|
6
app/src/main/assets/root/module.prop
Normal file
6
app/src/main/assets/root/module.prop
Normal file
@ -0,0 +1,6 @@
|
||||
id=__PKG_NAME__-ReVanced
|
||||
name=__LABEL__ ReVanced
|
||||
version=__VERSION__
|
||||
versionCode=0
|
||||
author=ReVanced
|
||||
description=Mounts the patched apk on top of the original apk
|
40
app/src/main/assets/root/service.sh
Normal file
40
app/src/main/assets/root/service.sh
Normal file
@ -0,0 +1,40 @@
|
||||
#!/system/bin/sh
|
||||
DIR=${0%/*}
|
||||
|
||||
package_name="__PKG_NAME__"
|
||||
version="__VERSION__"
|
||||
|
||||
rm "$DIR/log"
|
||||
|
||||
{
|
||||
|
||||
until [ "$(getprop sys.boot_completed)" = 1 ]; do sleep 5; done
|
||||
sleep 5
|
||||
|
||||
base_path="$DIR/$package_name.apk"
|
||||
stock_path="$(pm path "$package_name" | grep base | sed 's/package://g')"
|
||||
stock_version="$(dumpsys package "$package_name" | grep versionName | cut -d "=" -f2)"
|
||||
|
||||
echo "base_path: $base_path"
|
||||
echo "stock_path: $stock_path"
|
||||
echo "base_version: $version"
|
||||
echo "stock_version: $stock_version"
|
||||
|
||||
if mount | grep -q "$stock_path" ; then
|
||||
echo "Not mounting as stock path is already mounted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$version" != "$stock_version" ]; then
|
||||
echo "Not mounting as versions don't match"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$stock_path" ]; then
|
||||
echo "Not mounting as app info could not be loaded"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mount -o bind "$base_path" "$stock_path"
|
||||
|
||||
} >> "$DIR/log"
|
@ -34,10 +34,10 @@ class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val vm: MainViewModel = getActivityViewModel()
|
||||
|
||||
installSplashScreen()
|
||||
|
||||
val vm: MainViewModel = getActivityViewModel()
|
||||
|
||||
setContent {
|
||||
val theme by vm.prefs.theme.getAsState()
|
||||
val dynamicColor by vm.prefs.dynamicColor.getAsState()
|
||||
|
@ -1,16 +1,23 @@
|
||||
package app.revanced.manager
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import app.revanced.manager.di.*
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.service.ManagerRootService
|
||||
import app.revanced.manager.service.RootConnection
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.BuilderImpl
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
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.get
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
@ -37,6 +44,7 @@ class ManagerApplication : Application() {
|
||||
workerModule,
|
||||
viewModelModule,
|
||||
databaseModule,
|
||||
rootModule
|
||||
)
|
||||
}
|
||||
|
||||
@ -50,6 +58,12 @@ class ManagerApplication : Application() {
|
||||
.build()
|
||||
)
|
||||
|
||||
val shellBuilder = BuilderImpl.create().setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||
Shell.setDefaultBuilder(shellBuilder)
|
||||
|
||||
val intent = Intent(this, ManagerRootService::class.java)
|
||||
RootService.bind(intent, get<RootConnection>())
|
||||
|
||||
scope.launch {
|
||||
prefs.preload()
|
||||
}
|
||||
|
11
app/src/main/java/app/revanced/manager/di/RootModule.kt
Normal file
11
app/src/main/java/app/revanced/manager/di/RootModule.kt
Normal file
@ -0,0 +1,11 @@
|
||||
package app.revanced.manager.di
|
||||
|
||||
import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.service.RootConnection
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val rootModule = module {
|
||||
singleOf(::RootConnection)
|
||||
singleOf(::RootInstaller)
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
package app.revanced.manager.domain.installer
|
||||
|
||||
import android.app.Application
|
||||
import app.revanced.manager.service.RootConnection
|
||||
import app.revanced.manager.util.PM
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class RootInstaller(
|
||||
private val app: Application,
|
||||
private val rootConnection: RootConnection,
|
||||
private val pm: PM
|
||||
) {
|
||||
fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false
|
||||
|
||||
fun isAppInstalled(packageName: String) =
|
||||
rootConnection.remoteFS?.getFile("$modulesPath/$packageName-revanced")
|
||||
?.exists() ?: throw RootServiceException()
|
||||
|
||||
fun isAppMounted(packageName: String): Boolean {
|
||||
return pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let {
|
||||
Shell.cmd("mount | grep \"$it\"").exec().isSuccess
|
||||
} ?: false
|
||||
}
|
||||
|
||||
fun mount(packageName: String) {
|
||||
if (isAppMounted(packageName)) return
|
||||
|
||||
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
|
||||
?: throw Exception("Failed to load application info")
|
||||
val patchedAPK = "$modulesPath/$packageName-revanced/$packageName.apk"
|
||||
|
||||
Shell.cmd("mount -o bind \"$patchedAPK\" \"$stockAPK\"").exec()
|
||||
.also { if (!it.isSuccess) throw Exception("Failed to mount APK") }
|
||||
}
|
||||
|
||||
fun unmount(packageName: String) {
|
||||
if (!isAppMounted(packageName)) return
|
||||
|
||||
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
|
||||
?: throw Exception("Failed to load application info")
|
||||
|
||||
Shell.cmd("umount -l \"$stockAPK\"").exec()
|
||||
.also { if (!it.isSuccess) throw Exception("Failed to unmount APK") }
|
||||
}
|
||||
|
||||
suspend fun install(
|
||||
patchedAPK: File,
|
||||
stockAPK: File?,
|
||||
packageName: String,
|
||||
version: String,
|
||||
label: String
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
rootConnection.remoteFS?.let { remoteFS ->
|
||||
val assets = app.assets
|
||||
val modulePath = "$modulesPath/$packageName-revanced"
|
||||
|
||||
unmount(packageName)
|
||||
|
||||
stockAPK?.let { stockApp ->
|
||||
pm.getPackageInfo(packageName)?.let { packageInfo ->
|
||||
if (packageInfo.versionName <= version)
|
||||
Shell.cmd("pm uninstall -k --user 0 $packageName").exec()
|
||||
.also { if (!it.isSuccess) throw Exception("Failed to uninstall stock app") }
|
||||
}
|
||||
|
||||
Shell.cmd("pm install \"${stockApp.absolutePath}\"").exec()
|
||||
.also { if (!it.isSuccess) throw Exception("Failed to install stock app") }
|
||||
}
|
||||
|
||||
remoteFS.getFile(modulePath).mkdir()
|
||||
|
||||
listOf(
|
||||
"service.sh",
|
||||
"module.prop",
|
||||
).forEach { file ->
|
||||
assets.open("root/$file").use { inputStream ->
|
||||
remoteFS.getFile("$modulePath/$file").newOutputStream()
|
||||
.use { outputStream ->
|
||||
val content = String(inputStream.readBytes())
|
||||
.replace("__PKG_NAME__", packageName)
|
||||
.replace("__VERSION__", version)
|
||||
.replace("__LABEL__", label)
|
||||
.toByteArray()
|
||||
|
||||
outputStream.write(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"$modulePath/$packageName.apk".let { apkPath ->
|
||||
|
||||
remoteFS.getFile(patchedAPK.absolutePath)
|
||||
.also { if (!it.exists()) throw Exception("File doesn't exist") }
|
||||
.newInputStream().use { inputStream ->
|
||||
remoteFS.getFile(apkPath).newOutputStream().use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
|
||||
Shell.cmd(
|
||||
"chmod 644 $apkPath",
|
||||
"chown system:system $apkPath",
|
||||
"chcon u:object_r:apk_data_file:s0 $apkPath",
|
||||
"chmod +x $modulePath/service.sh"
|
||||
).exec()
|
||||
.let { if (!it.isSuccess) throw Exception("Failed to set file permissions") }
|
||||
}
|
||||
} ?: throw RootServiceException()
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstall(packageName: String) {
|
||||
rootConnection.remoteFS?.let { remoteFS ->
|
||||
if (isAppMounted(packageName))
|
||||
unmount(packageName)
|
||||
|
||||
remoteFS.getFile("$modulesPath/$packageName-revanced").deleteRecursively()
|
||||
.also { if (!it) throw Exception("Failed to delete files") }
|
||||
} ?: throw RootServiceException()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val modulesPath = "/data/adb/modules"
|
||||
}
|
||||
}
|
||||
|
||||
class RootServiceException: Exception("Root not available")
|
@ -14,8 +14,11 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
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.worker.Worker
|
||||
import app.revanced.manager.domain.worker.WorkerRepository
|
||||
@ -29,7 +32,6 @@ import app.revanced.manager.util.PatchesSelection
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.patcher.extensions.PatchExtensions.options
|
||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||
import app.revanced.patcher.logging.Logger
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@ -49,6 +51,8 @@ class PatcherWorker(
|
||||
private val prefs: PreferencesManager by inject()
|
||||
private val downloadedAppRepository: DownloadedAppRepository by inject()
|
||||
private val pm: PM by inject()
|
||||
private val installedAppRepository: InstalledAppRepository by inject()
|
||||
private val rootInstaller: RootInstaller by inject()
|
||||
|
||||
data class Args(
|
||||
val input: SelectedApp,
|
||||
@ -58,7 +62,9 @@ class PatcherWorker(
|
||||
val packageName: String,
|
||||
val packageVersion: String,
|
||||
val progress: MutableStateFlow<ImmutableList<Step>>,
|
||||
val logger: ManagerLogger
|
||||
val logger: ManagerLogger,
|
||||
val selectedApp: SelectedApp,
|
||||
val setInputFile: (File) -> Unit
|
||||
)
|
||||
|
||||
companion object {
|
||||
@ -148,6 +154,15 @@ class PatcherWorker(
|
||||
}
|
||||
|
||||
return try {
|
||||
|
||||
if (args.selectedApp is SelectedApp.Installed) {
|
||||
installedAppRepository.get(args.packageName)?.let {
|
||||
if (it.installType == InstallType.ROOT) {
|
||||
rootInstaller.unmount(args.packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: consider passing all the classes directly now that the input no longer needs to be serializable.
|
||||
val selectedBundles = args.selectedPatches.keys
|
||||
val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
|
||||
@ -190,11 +205,12 @@ class PatcherWorker(
|
||||
args.input.version,
|
||||
it
|
||||
)
|
||||
args.setInputFile(it)
|
||||
updateProgress() // Downloading
|
||||
}
|
||||
}
|
||||
|
||||
is SelectedApp.Local -> selectedApp.file
|
||||
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
|
||||
is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo.sourceDir)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,36 @@
|
||||
package app.revanced.manager.service
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import app.revanced.manager.IRootSystemService
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||
|
||||
class ManagerRootService : RootService() {
|
||||
class RootSystemService : IRootSystemService.Stub() {
|
||||
override fun getFileSystemService() =
|
||||
FileSystemManager.getService()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return RootSystemService()
|
||||
}
|
||||
}
|
||||
|
||||
class RootConnection : ServiceConnection {
|
||||
var remoteFS: FileSystemManager? = null
|
||||
private set
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
val ipc = IRootSystemService.Stub.asInterface(service)
|
||||
val binder = ipc.fileSystemService
|
||||
|
||||
remoteFS = FileSystemManager.getRemote(binder)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
remoteFS = null
|
||||
}
|
||||
}
|
@ -9,10 +9,12 @@ 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.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
@ -26,43 +28,53 @@ import androidx.compose.ui.unit.dp
|
||||
@Composable
|
||||
fun RowScope.SegmentedButton(
|
||||
icon: Any,
|
||||
iconDescription: String? = null,
|
||||
text: String,
|
||||
onClick: () -> Unit
|
||||
onClick: () -> Unit,
|
||||
iconDescription: String? = null,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
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
|
||||
val contentColor = if (enabled)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurface.copy(0.38f)
|
||||
|
||||
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
|
||||
modifier = Modifier
|
||||
.clickable(enabled = enabled, onClick = onClick)
|
||||
.background(
|
||||
if (enabled)
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurface.copy(0.12f)
|
||||
)
|
||||
.weight(1f)
|
||||
.padding(vertical = 20.dp)
|
||||
) {
|
||||
when (icon) {
|
||||
is ImageVector -> {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = iconDescription
|
||||
)
|
||||
}
|
||||
|
||||
is Painter -> {
|
||||
Icon(
|
||||
painter = icon,
|
||||
contentDescription = iconDescription
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is Painter -> {
|
||||
Icon(
|
||||
painter = icon,
|
||||
contentDescription = iconDescription,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.basicMarquee()
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.basicMarquee()
|
||||
)
|
||||
}
|
||||
}
|
@ -13,17 +13,25 @@ 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.Circle
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.OpenInNew
|
||||
import androidx.compose.material.icons.outlined.SettingsBackupRestore
|
||||
import androidx.compose.material.icons.outlined.Update
|
||||
import androidx.compose.material3.AlertDialog
|
||||
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.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
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.draw.clip
|
||||
@ -31,6 +39,7 @@ 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.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.ui.component.AppIcon
|
||||
import app.revanced.manager.ui.component.AppLabel
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
@ -49,6 +58,14 @@ fun AppInfoScreen(
|
||||
viewModel.onBackClick = onBackClick
|
||||
}
|
||||
|
||||
var showUninstallDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (showUninstallDialog)
|
||||
UninstallDialog(
|
||||
onDismiss = { showUninstallDialog = false },
|
||||
onConfirm = { viewModel.uninstall() }
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
@ -84,6 +101,17 @@ fun AppInfoScreen(
|
||||
)
|
||||
|
||||
Text(viewModel.installedApp.version, style = MaterialTheme.typography.bodySmall)
|
||||
|
||||
if (viewModel.installedApp.installType == InstallType.ROOT) {
|
||||
Text(
|
||||
text = if (viewModel.rootInstaller.isAppMounted(viewModel.installedApp.currentPackageName)) {
|
||||
stringResource(R.string.mounted)
|
||||
} else {
|
||||
stringResource(R.string.not_mounted)
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
@ -98,11 +126,30 @@ fun AppInfoScreen(
|
||||
onClick = viewModel::launch
|
||||
)
|
||||
|
||||
SegmentedButton(
|
||||
icon = Icons.Outlined.Delete,
|
||||
text = stringResource(R.string.uninstall),
|
||||
onClick = viewModel::uninstall
|
||||
)
|
||||
when (viewModel.installedApp.installType) {
|
||||
InstallType.DEFAULT -> SegmentedButton(
|
||||
icon = Icons.Outlined.Delete,
|
||||
text = stringResource(R.string.uninstall),
|
||||
onClick = viewModel::uninstall
|
||||
)
|
||||
|
||||
InstallType.ROOT -> {
|
||||
SegmentedButton(
|
||||
icon = Icons.Outlined.SettingsBackupRestore,
|
||||
text = stringResource(R.string.unpatch),
|
||||
onClick = { showUninstallDialog = true },
|
||||
enabled = viewModel.rootInstaller.hasRootAccess()
|
||||
)
|
||||
|
||||
SegmentedButton(
|
||||
icon = Icons.Outlined.Circle,
|
||||
text = if (viewModel.isMounted) stringResource(R.string.unmount) else stringResource(R.string.mount),
|
||||
onClick = viewModel::mountOrUnmount,
|
||||
enabled = viewModel.rootInstaller.hasRootAccess()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SegmentedButton(
|
||||
icon = Icons.Outlined.Update,
|
||||
@ -111,7 +158,8 @@ fun AppInfoScreen(
|
||||
viewModel.appliedPatches?.let {
|
||||
onPatchClick(viewModel.installedApp.originalPackageName, it)
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = viewModel.installedApp.installType != InstallType.ROOT || viewModel.rootInstaller.hasRootAccess()
|
||||
)
|
||||
}
|
||||
|
||||
@ -156,3 +204,30 @@ fun AppInfoScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UninstallDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) = AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.unpatch_app)) },
|
||||
text = { Text(stringResource(R.string.unpatch_description)) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onConfirm()
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = onDismiss
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
@ -5,6 +5,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@ -35,6 +36,7 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.patcher.worker.Step
|
||||
import app.revanced.manager.patcher.worker.State
|
||||
import app.revanced.manager.ui.component.AppScaffold
|
||||
@ -59,6 +61,13 @@ fun InstallerScreen(
|
||||
val steps by vm.progress.collectAsStateWithLifecycle()
|
||||
val canInstall by remember { derivedStateOf { patcherState == true && (vm.installedPackageName != null || !vm.isInstalling) } }
|
||||
var dropdownActive by rememberSaveable { mutableStateOf(false) }
|
||||
var showInstallPicker by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (showInstallPicker)
|
||||
InstallPicker(
|
||||
onDismiss = { showInstallPicker = false },
|
||||
onConfirm = { vm.install(it) }
|
||||
)
|
||||
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
@ -111,7 +120,12 @@ fun InstallerScreen(
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = vm::installOrOpen,
|
||||
onClick = {
|
||||
if (vm.installedPackageName == null)
|
||||
showInstallPicker = true
|
||||
else
|
||||
vm.open()
|
||||
},
|
||||
enabled = canInstall
|
||||
) {
|
||||
Text(stringResource(vm.appButtonText))
|
||||
@ -121,6 +135,51 @@ fun InstallerScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InstallPicker(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (InstallType) -> Unit
|
||||
) {
|
||||
var selectedInstallType by rememberSaveable { mutableStateOf(InstallType.DEFAULT) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
dismissButton = {
|
||||
Button(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
onConfirm(selectedInstallType)
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.install_app))
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(R.string.select_install_type)) },
|
||||
text = {
|
||||
Column {
|
||||
InstallType.values().forEach {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { selectedInstallType = it },
|
||||
leadingContent = {
|
||||
RadioButton(
|
||||
selected = selectedInstallType == it,
|
||||
onClick = null
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(stringResource(it.stringResource)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Credits: https://github.com/Aliucord/AliucordManager/blob/main/app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallGroup.kt
|
||||
|
||||
@Composable
|
||||
|
@ -33,6 +33,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.InstallType
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.GroupHeader
|
||||
import app.revanced.manager.ui.component.LoadingIndicator
|
||||
@ -78,7 +79,7 @@ fun VersionSelectorScreen(
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text("Select version") },
|
||||
text = { Text(stringResource(R.string.select_version)) },
|
||||
icon = { Icon(Icons.Default.Check, null) },
|
||||
onClick = { selectedVersion?.let(onAppClick) }
|
||||
)
|
||||
@ -90,7 +91,7 @@ fun VersionSelectorScreen(
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
viewModel.installedApp?.let { (packageInfo, alreadyPatched) ->
|
||||
viewModel.installedApp?.let { (packageInfo, installedApp) ->
|
||||
SelectedApp.Installed(
|
||||
packageName = viewModel.packageName,
|
||||
version = packageInfo.versionName
|
||||
@ -100,12 +101,14 @@ fun VersionSelectorScreen(
|
||||
selected = selectedVersion == it,
|
||||
onClick = { selectedVersion = it },
|
||||
patchCount = supportedVersions[it.version],
|
||||
alreadyPatched = alreadyPatched
|
||||
enabled =
|
||||
!(installedApp?.installType == InstallType.ROOT && !viewModel.rootInstaller.hasRootAccess()),
|
||||
alreadyPatched = installedApp != null && installedApp.installType != InstallType.ROOT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
GroupHeader("Downloadable versions")
|
||||
GroupHeader(stringResource(R.string.downloadable_versions))
|
||||
|
||||
list.forEach {
|
||||
SelectedAppItem(
|
||||
@ -140,6 +143,7 @@ fun SelectedAppItem(
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
patchCount: Int?,
|
||||
enabled: Boolean = true,
|
||||
alreadyPatched: Boolean = false
|
||||
) {
|
||||
ListItem(
|
||||
@ -148,9 +152,9 @@ fun SelectedAppItem(
|
||||
supportingContent = when (selectedApp) {
|
||||
is SelectedApp.Installed ->
|
||||
if (alreadyPatched) {
|
||||
{ Text("Already patched") }
|
||||
{ Text(stringResource(R.string.already_patched)) }
|
||||
} else {
|
||||
{ Text("Installed") }
|
||||
{ Text(stringResource(R.string.installed)) }
|
||||
}
|
||||
|
||||
is SelectedApp.Local -> {
|
||||
@ -163,9 +167,9 @@ fun SelectedAppItem(
|
||||
Text(pluralStringResource(R.plurals.patches_count, it, it))
|
||||
} },
|
||||
modifier = Modifier
|
||||
.clickable(enabled = !alreadyPatched, onClick = onClick)
|
||||
.clickable(enabled = !alreadyPatched && enabled, onClick = onClick)
|
||||
.run {
|
||||
if (alreadyPatched) alpha(0.5f)
|
||||
if (!enabled || alreadyPatched) alpha(0.5f)
|
||||
else this
|
||||
}
|
||||
)
|
||||
|
@ -7,6 +7,7 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
@ -15,10 +16,13 @@ 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.installer.RootInstaller
|
||||
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.simpleMessage
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.manager.util.toast
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -32,20 +36,46 @@ class AppInfoViewModel(
|
||||
private val app: Application by inject()
|
||||
private val pm: PM by inject()
|
||||
private val installedAppRepository: InstalledAppRepository by inject()
|
||||
val rootInstaller: RootInstaller by inject()
|
||||
|
||||
lateinit var onBackClick: () -> Unit
|
||||
|
||||
var appInfo: PackageInfo? by mutableStateOf(null)
|
||||
private set
|
||||
var appliedPatches: PatchesSelection? by mutableStateOf(null)
|
||||
var isMounted by mutableStateOf(rootInstaller.isAppMounted(installedApp.currentPackageName))
|
||||
private set
|
||||
|
||||
fun launch() = pm.launch(installedApp.currentPackageName)
|
||||
|
||||
fun mountOrUnmount() {
|
||||
try {
|
||||
if (isMounted)
|
||||
rootInstaller.unmount(installedApp.currentPackageName)
|
||||
else
|
||||
rootInstaller.mount(installedApp.currentPackageName)
|
||||
} catch (e: Exception) {
|
||||
if (isMounted) {
|
||||
app.toast(app.getString(R.string.failed_to_unmount, e.simpleMessage()))
|
||||
Log.e(tag, "Failed to unmount", e)
|
||||
} else {
|
||||
app.toast(app.getString(R.string.failed_to_mount, e.simpleMessage()))
|
||||
Log.e(tag, "Failed to mount", e)
|
||||
}
|
||||
} finally {
|
||||
isMounted = rootInstaller.isAppMounted(installedApp.currentPackageName)
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstall() {
|
||||
when (installedApp.installType) {
|
||||
InstallType.DEFAULT -> pm.uninstallPackage(installedApp.currentPackageName)
|
||||
|
||||
InstallType.ROOT -> TODO()
|
||||
InstallType.ROOT -> viewModelScope.launch {
|
||||
rootInstaller.uninstall(installedApp.currentPackageName)
|
||||
installedAppRepository.delete(installedApp)
|
||||
onBackClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,7 +89,7 @@ class AppInfoViewModel(
|
||||
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||
viewModelScope.launch {
|
||||
installedAppRepository.delete(installedApp)
|
||||
withContext(Dispatchers.Main) { onBackClick() }
|
||||
onBackClick()
|
||||
}
|
||||
} else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) {
|
||||
app.toast(app.getString(R.string.uninstall_app_fail, extraStatusMessage))
|
||||
|
@ -11,22 +11,19 @@ import java.nio.file.Files
|
||||
|
||||
class AppSelectorViewModel(
|
||||
private val app: Application,
|
||||
pm: PM
|
||||
private val pm: PM
|
||||
) : ViewModel() {
|
||||
private val packageManager = app.packageManager
|
||||
|
||||
val appList = pm.appList
|
||||
|
||||
fun loadLabel(app: PackageInfo?) = (app?.applicationInfo?.loadLabel(packageManager) ?: "Not installed").toString()
|
||||
fun loadLabel(app: PackageInfo?) = with(pm) { app?.label() ?: "Not installed" }
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun loadSelectedFile(uri: Uri) =
|
||||
app.contentResolver.openInputStream(uri)?.use { stream ->
|
||||
File(app.cacheDir, "input.apk").also {
|
||||
it.delete()
|
||||
Files.copy(stream, it.toPath())
|
||||
}.let { file ->
|
||||
packageManager.getPackageArchiveInfo(file.absolutePath, 0)
|
||||
pm.getPackageInfo(file)
|
||||
?.let { packageInfo ->
|
||||
SelectedApp.Local(packageName = packageInfo.packageName, version = packageInfo.versionName, file = file)
|
||||
}
|
||||
|
@ -4,6 +4,9 @@ import android.content.pm.PackageInfo
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.domain.installer.RootServiceException
|
||||
import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.collectEach
|
||||
@ -14,7 +17,8 @@ import kotlinx.coroutines.withContext
|
||||
|
||||
class InstalledAppsViewModel(
|
||||
private val installedAppsRepository: InstalledAppRepository,
|
||||
private val pm: PM
|
||||
private val pm: PM,
|
||||
private val rootInstaller: RootInstaller
|
||||
) : ViewModel() {
|
||||
val apps = installedAppsRepository.getAll().flowOn(Dispatchers.IO)
|
||||
|
||||
@ -24,8 +28,23 @@ class InstalledAppsViewModel(
|
||||
viewModelScope.launch {
|
||||
apps.collectEach { installedApp ->
|
||||
packageInfoMap[installedApp.currentPackageName] = withContext(Dispatchers.IO) {
|
||||
pm.getPackageInfo(installedApp.currentPackageName)
|
||||
.also { if (it == null) installedAppsRepository.delete(installedApp) }
|
||||
try {
|
||||
if (
|
||||
installedApp.installType == InstallType.ROOT && !rootInstaller.isAppInstalled(installedApp.currentPackageName)
|
||||
) {
|
||||
installedAppsRepository.delete(installedApp)
|
||||
return@withContext null
|
||||
}
|
||||
} catch (_: RootServiceException) { }
|
||||
|
||||
val packageInfo = pm.getPackageInfo(installedApp.currentPackageName)
|
||||
|
||||
if (packageInfo == null && installedApp.installType != InstallType.ROOT) {
|
||||
installedAppsRepository.delete(installedApp)
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
packageInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
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.installer.RootInstaller
|
||||
import app.revanced.manager.domain.manager.KeystoreManager
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.domain.worker.WorkerRepository
|
||||
@ -29,7 +31,9 @@ import app.revanced.manager.patcher.worker.Step
|
||||
import app.revanced.manager.service.InstallService
|
||||
import app.revanced.manager.service.UninstallService
|
||||
import app.revanced.manager.ui.destination.Destination
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.simpleMessage
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.manager.util.toast
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@ -48,18 +52,23 @@ import java.util.logging.Level
|
||||
import java.util.logging.LogRecord
|
||||
|
||||
@Stable
|
||||
class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinComponent {
|
||||
class InstallerViewModel(
|
||||
private val input: Destination.Installer
|
||||
) : ViewModel(), KoinComponent {
|
||||
private val keystoreManager: KeystoreManager by inject()
|
||||
private val app: Application by inject()
|
||||
private val pm: PM by inject()
|
||||
private val workerRepository: WorkerRepository by inject()
|
||||
private val installedAppReceiver: InstalledAppRepository by inject()
|
||||
private val installedAppRepository: InstalledAppRepository by inject()
|
||||
private val rootInstaller: RootInstaller by inject()
|
||||
|
||||
val packageName: String = input.selectedApp.packageName
|
||||
private val outputFile = File(app.cacheDir, "output.apk")
|
||||
private val signedFile = File(app.cacheDir, "signed.apk").also { if (it.exists()) it.delete() }
|
||||
private var hasSigned = false
|
||||
var inputFile: File? = null
|
||||
|
||||
private var installedApp: InstalledApp? = null
|
||||
var isInstalling by mutableStateOf(false)
|
||||
private set
|
||||
var installedPackageName by mutableStateOf<String?>(null)
|
||||
@ -73,13 +82,18 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
|
||||
private val logger = ManagerLogger()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
installedApp = installedAppRepository.get(packageName)
|
||||
}
|
||||
|
||||
val (selectedApp, patches, options) = input
|
||||
|
||||
_progress = MutableStateFlow(PatcherProgressManager.generateSteps(
|
||||
app,
|
||||
patches.flatMap { (_, selected) -> selected },
|
||||
input.selectedApp
|
||||
selectedApp
|
||||
).toImmutableList())
|
||||
|
||||
patcherWorkerId =
|
||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||
"patching", PatcherWorker.Args(
|
||||
@ -90,7 +104,9 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
|
||||
packageName,
|
||||
selectedApp.version,
|
||||
_progress,
|
||||
logger
|
||||
logger,
|
||||
selectedApp,
|
||||
setInputFile = { inputFile = it }
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -118,7 +134,7 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
|
||||
installedPackageName =
|
||||
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
||||
viewModelScope.launch {
|
||||
installedAppReceiver.add(
|
||||
installedAppRepository.add(
|
||||
installedPackageName!!,
|
||||
packageName,
|
||||
input.selectedApp.version,
|
||||
@ -162,6 +178,19 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
|
||||
|
||||
outputFile.delete()
|
||||
signedFile.delete()
|
||||
|
||||
try {
|
||||
if (input.selectedApp is SelectedApp.Installed) {
|
||||
installedApp?.let {
|
||||
if (it.installType == InstallType.ROOT) {
|
||||
rootInstaller.mount(packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Failed to mount", e)
|
||||
app.toast(app.getString(R.string.failed_to_mount, e.simpleMessage()))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun signApk(): Boolean {
|
||||
@ -192,20 +221,62 @@ class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinCompon
|
||||
}
|
||||
}
|
||||
|
||||
fun installOrOpen() = viewModelScope.launch {
|
||||
installedPackageName?.let {
|
||||
pm.launch(it)
|
||||
return@launch
|
||||
}
|
||||
|
||||
fun install(installType: InstallType) = viewModelScope.launch {
|
||||
isInstalling = true
|
||||
try {
|
||||
if (!signApk()) return@launch
|
||||
pm.installApp(listOf(signedFile))
|
||||
|
||||
when (installType) {
|
||||
InstallType.DEFAULT -> { pm.installApp(listOf(signedFile)) }
|
||||
|
||||
InstallType.ROOT -> { installAsRoot() }
|
||||
}
|
||||
|
||||
} finally {
|
||||
isInstalling = false
|
||||
}
|
||||
}
|
||||
|
||||
fun open() = installedPackageName?.let { pm.launch(it) }
|
||||
|
||||
private suspend fun installAsRoot() {
|
||||
try {
|
||||
val label = with(pm) {
|
||||
getPackageInfo(signedFile)?.label()
|
||||
?: throw Exception("Failed to load application info")
|
||||
}
|
||||
|
||||
rootInstaller.install(
|
||||
outputFile,
|
||||
inputFile,
|
||||
packageName,
|
||||
input.selectedApp.version,
|
||||
label
|
||||
)
|
||||
|
||||
rootInstaller.mount(packageName)
|
||||
|
||||
installedApp?.let { installedAppRepository.delete(it) }
|
||||
|
||||
installedAppRepository.add(
|
||||
packageName,
|
||||
packageName,
|
||||
input.selectedApp.version,
|
||||
InstallType.ROOT,
|
||||
input.selectedPatches
|
||||
)
|
||||
|
||||
installedPackageName = packageName
|
||||
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Failed to install as root", e)
|
||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
||||
try {
|
||||
rootInstaller.uninstall(packageName)
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move this to a better place
|
||||
|
@ -7,7 +7,8 @@ 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.data.room.apps.installed.InstalledApp
|
||||
import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
@ -35,8 +36,9 @@ class VersionSelectorViewModel(
|
||||
private val patchBundleRepository: PatchBundleRepository by inject()
|
||||
private val pm: PM by inject()
|
||||
private val appDownloader: AppDownloader = APKMirror()
|
||||
val rootInstaller: RootInstaller by inject()
|
||||
|
||||
var installedApp: Pair<PackageInfo, Boolean>? by mutableStateOf(null)
|
||||
var installedApp: Pair<PackageInfo, InstalledApp?>? by mutableStateOf(null)
|
||||
private set
|
||||
var isLoading by mutableStateOf(true)
|
||||
private set
|
||||
@ -72,15 +74,11 @@ class VersionSelectorViewModel(
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
|
||||
val alreadyPatched = async(Dispatchers.IO) {
|
||||
installedAppRepository.get(packageName)
|
||||
?.let { it.installType == InstallType.DEFAULT }
|
||||
?: false
|
||||
}
|
||||
val installedAppDeferred = async(Dispatchers.IO) { installedAppRepository.get(packageName) }
|
||||
|
||||
installedApp =
|
||||
packageInfo.await()?.let {
|
||||
it to alreadyPatched.await()
|
||||
it to installedAppDeferred.await()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,8 +32,7 @@ private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readab
|
||||
data class AppInfo(
|
||||
val packageName: String,
|
||||
val patches: Int?,
|
||||
val packageInfo: PackageInfo?,
|
||||
val path: File? = null
|
||||
val packageInfo: PackageInfo?
|
||||
) : Parcelable
|
||||
|
||||
@SuppressLint("QueryPermissionsNeeded")
|
||||
@ -57,8 +56,7 @@ class PM(
|
||||
AppInfo(
|
||||
pkg,
|
||||
compatiblePackages[pkg],
|
||||
packageInfo,
|
||||
File(packageInfo.applicationInfo.sourceDir)
|
||||
packageInfo
|
||||
)
|
||||
} ?: AppInfo(
|
||||
pkg,
|
||||
@ -73,8 +71,7 @@ class PM(
|
||||
AppInfo(
|
||||
packageInfo.packageName,
|
||||
0,
|
||||
packageInfo,
|
||||
File(packageInfo.applicationInfo.sourceDir)
|
||||
packageInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -85,9 +82,13 @@ class PM(
|
||||
.sortedWith(
|
||||
compareByDescending<AppInfo> {
|
||||
it.patches
|
||||
}.thenBy { it.packageInfo?.applicationInfo?.loadLabel(app.packageManager).toString() }.thenBy { it.packageName }
|
||||
}.thenBy {
|
||||
it.packageInfo?.label()
|
||||
}.thenBy { it.packageName }
|
||||
)
|
||||
} else { emptyList() }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
fun getPackageInfo(packageName: String): PackageInfo? =
|
||||
@ -97,6 +98,10 @@ class PM(
|
||||
null
|
||||
}
|
||||
|
||||
fun getPackageInfo(file: File): PackageInfo? = app.packageManager.getPackageArchiveInfo(file.absolutePath, 0)
|
||||
|
||||
fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString()
|
||||
|
||||
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
|
||||
val packageInstaller = app.packageManager.packageInstaller
|
||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
|
||||
@ -114,42 +119,42 @@ class PM(
|
||||
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
app.startActivity(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun PackageInstaller.Session.writeApk(apk: File) {
|
||||
apk.inputStream().use { inputStream ->
|
||||
openWrite(apk.name, 0, apk.length()).use { outputStream ->
|
||||
inputStream.copyTo(outputStream, byteArraySize)
|
||||
fsync(outputStream)
|
||||
private fun PackageInstaller.Session.writeApk(apk: File) {
|
||||
apk.inputStream().use { inputStream ->
|
||||
openWrite(apk.name, 0, apk.length()).use { outputStream ->
|
||||
inputStream.copyTo(outputStream, byteArraySize)
|
||||
fsync(outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val intentFlags
|
||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
PendingIntent.FLAG_MUTABLE
|
||||
else
|
||||
0
|
||||
|
||||
private val sessionParams
|
||||
get() = PackageInstaller.SessionParams(
|
||||
PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
||||
).apply {
|
||||
setInstallReason(PackageManager.INSTALL_REASON_USER)
|
||||
}
|
||||
|
||||
private val Context.installIntentSender
|
||||
get() = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, InstallService::class.java),
|
||||
intentFlags
|
||||
).intentSender
|
||||
|
||||
private val Context.uninstallIntentSender
|
||||
get() = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, UninstallService::class.java),
|
||||
intentFlags
|
||||
).intentSender
|
||||
}
|
||||
|
||||
private val intentFlags
|
||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
PendingIntent.FLAG_MUTABLE
|
||||
else
|
||||
0
|
||||
|
||||
private val sessionParams
|
||||
get() = PackageInstaller.SessionParams(
|
||||
PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
||||
).apply {
|
||||
setInstallReason(PackageManager.INSTALL_REASON_USER)
|
||||
}
|
||||
|
||||
private val Context.installIntentSender
|
||||
get() = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, InstallService::class.java),
|
||||
intentFlags
|
||||
).intentSender
|
||||
|
||||
private val Context.uninstallIntentSender
|
||||
get() = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, UninstallService::class.java),
|
||||
intentFlags
|
||||
).intentSender
|
@ -24,8 +24,6 @@
|
||||
<string name="bundle_missing">Missing</string>
|
||||
<string name="bundle_error">Error</string>
|
||||
|
||||
<string name="select_version">Select version</string>
|
||||
|
||||
<string name="auto_updates_dialog_title">Select updates to receive</string>
|
||||
<string name="auto_updates_dialog_description">Periodically connect to update providers to check for updates.</string>
|
||||
<string name="auto_updates_dialog_manager">Manager updates</string>
|
||||
@ -156,9 +154,11 @@
|
||||
<string name="failed_to_load_apk">Failed to load apk</string>
|
||||
<string name="loading">Loading…</string>
|
||||
<string name="not_installed">Not installed</string>
|
||||
<string name="installed">Installed</string>
|
||||
|
||||
<string name="app_info">App info</string>
|
||||
<string name="uninstall">Uninstall</string>
|
||||
<string name="unpatch">Unpatch</string>
|
||||
<string name="repatch">Repatch</string>
|
||||
<string name="install_type">Installation type</string>
|
||||
<string name="package_name">Package name</string>
|
||||
@ -167,9 +167,20 @@
|
||||
<string name="view_applied_patches">View applied patches</string>
|
||||
<string name="default_install">Default</string>
|
||||
<string name="root_install">Root</string>
|
||||
<string name="mounted">Mounted</string>
|
||||
<string name="not_mounted">Not mounted</string>
|
||||
<string name="mount">Mount</string>
|
||||
<string name="unmount">Unmount</string>
|
||||
<string name="failed_to_mount">Failed to mount: %s</string>
|
||||
<string name="failed_to_unmount">Failed to unmount: %s</string>
|
||||
<string name="unpatch_app">Unpatch app?</string>
|
||||
<string name="unpatch_description">Are you sure you want to unpatch this app?</string>
|
||||
|
||||
<string name="error_occurred">An error occurred</string>
|
||||
<string name="already_downloaded">Already downloaded</string>
|
||||
<string name="select_version">Select version</string>
|
||||
<string name="downloadable_versions">Downloadable versions</string>
|
||||
<string name="already_patched">Already patched</string>
|
||||
|
||||
<string name="string_option_icon_description">Edit</string>
|
||||
<string name="string_option_menu_description">More options</string>
|
||||
@ -193,6 +204,7 @@
|
||||
<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>
|
||||
<string name="select_install_type">Select installation type</string>
|
||||
|
||||
<string name="patcher_step_group_prepare">Preparation</string>
|
||||
<string name="patcher_step_load_patches">Load patches</string>
|
||||
|
@ -26,6 +26,7 @@ aboutLibrariesGradlePlugin = "10.8.2"
|
||||
coil = "2.4.0"
|
||||
app-icon-loader-coil = "1.5.0"
|
||||
skrapeit = "1.2.1"
|
||||
libsu = "5.2.0"
|
||||
|
||||
[libraries]
|
||||
# AndroidX Core
|
||||
@ -98,6 +99,11 @@ skrapeit-parser = { group = "it.skrape", name = "skrapeit-html-parser", version.
|
||||
# Markdown
|
||||
markdown = { group = "org.jetbrains", name = "markdown", version.ref = "markdown" }
|
||||
|
||||
# LibSU
|
||||
libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" }
|
||||
libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" }
|
||||
libsu-nio = { group = "com.github.topjohnwu.libsu", name = "nio", version.ref = "libsu" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinGradlePlugin" }
|
||||
|
Loading…
Reference in New Issue
Block a user