diff --git a/app/src/main/assets/root/module.prop b/app/src/main/assets/root/module.prop deleted file mode 100644 index 05a5a159dd..0000000000 --- a/app/src/main/assets/root/module.prop +++ /dev/null @@ -1,6 +0,0 @@ -id=__PKG_NAME__-ReVanced -name=__LABEL__ ReVanced -version=__VERSION__ -versionCode=0 -author=ReVanced -description=Mounts the patched APK on top of the original one \ No newline at end of file diff --git a/app/src/main/assets/root/service.sh b/app/src/main/assets/root/service.sh deleted file mode 100644 index dc3bcb5f45..0000000000 --- a/app/src/main/assets/root/service.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/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" diff --git a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt index c0986dfd10..e42773afc0 100644 --- a/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt +++ b/app/src/main/java/app/revanced/manager/data/room/apps/installed/InstalledApp.kt @@ -7,7 +7,8 @@ import app.revanced.manager.R enum class InstallType(val stringResource: Int) { DEFAULT(R.string.default_install), - MOUNT(R.string.mount_install) + MOUNT(R.string.mount_install), + MAGISK(R.string.magisk_install) } @Entity(tableName = "installed_app") diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index b7fbf19c7a..ac852c3af2 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -5,18 +5,22 @@ import android.content.ComponentName import android.content.Intent import android.content.ServiceConnection import android.os.IBinder +import app.revanced.library.MagiskUtils +import app.revanced.library.installation.installer.Constants +import app.revanced.library.installation.installer.Constants.invoke import app.revanced.manager.IRootSystemService +import app.revanced.manager.ui.model.RootCheckResult import app.revanced.manager.service.ManagerRootService import app.revanced.manager.util.PM import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.ipc.RootService import com.topjohnwu.superuser.nio.FileSystemManager +import java.io.File +import java.time.Duration import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.time.withTimeoutOrNull import kotlinx.coroutines.withContext -import java.io.File -import java.time.Duration class RootInstaller( private val app: Application, @@ -24,6 +28,9 @@ class RootInstaller( ) : ServiceConnection { private var remoteFS = CompletableDeferred() + // Android user (0 for primary, 10+ for secondary/work profiles) via pure public API. + private val userId = android.os.Process.myUid() / 100000 + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { val ipc = IRootSystemService.Stub.asInterface(service) val binder = ipc.fileSystemService @@ -43,12 +50,12 @@ class RootInstaller( } } - return withTimeoutOrNull(Duration.ofSeconds(20L)) { - remoteFS.await() - } ?: throw RootServiceException() + return withTimeoutOrNull(Duration.ofSeconds(20L)) { remoteFS.await() } + ?: throw RootServiceException() } private suspend fun getShell() = with(CompletableDeferred()) { + Shell.getCachedShell()?.takeIf { !it.isRoot }?.close() Shell.getShell(::complete) await() @@ -60,14 +67,24 @@ class RootInstaller( return getShell().newJob().add(*commands).to(stdout, stderr).exec() } - fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false + fun hasRootAccess() = MagiskUtils.hasRootAccess() - fun isDeviceRooted() = System.getenv("PATH")?.split(":")?.any { path -> - File(path, "su").canExecute() - } ?: false + fun isDeviceRooted() = MagiskUtils.isDeviceRooted() + fun isMagiskInstalled() = MagiskUtils.isMagiskInstalled() + + fun requestRoot() = MagiskUtils.requestRoot() + + fun checkRootStatus(): RootCheckResult = when { + !isDeviceRooted() -> RootCheckResult.UNAVAILABLE + isMagiskInstalled() -> RootCheckResult.GRANTED + else -> RootCheckResult.DENIED + } suspend fun isAppInstalled(packageName: String) = - awaitRemoteFS().getFile("$modulesPath/$packageName-revanced").exists() + MagiskUtils.isInstalled(packageName, awaitRemoteFS()) + + suspend fun isAppInstalledAsMagiskModule(packageName: String) = + MagiskUtils.isInstalledAsMagiskModule(packageName, awaitRemoteFS()) suspend fun isAppMounted(packageName: String) = withContext(Dispatchers.IO) { pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let { @@ -79,11 +96,11 @@ class RootInstaller( if (isAppMounted(packageName)) return withContext(Dispatchers.IO) { - val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir - ?: throw Exception("Failed to load application info") - val patchedAPK = "$modulesPath/$packageName-revanced/$packageName.apk" - - execute("mount -o bind \"$patchedAPK\" \"$stockAPK\"").assertSuccess("Failed to mount APK") + val sourceDir = + pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir + ?: throw Exception("Failed to load application info") + + MagiskUtils.mount(packageName, sourceDir) } } @@ -91,10 +108,10 @@ class RootInstaller( if (!isAppMounted(packageName)) return withContext(Dispatchers.IO) { - val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir + val sourceDir = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir ?: throw Exception("Failed to load application info") - execute("umount -l \"$stockAPK\"").assertSuccess("Failed to unmount APK") + MagiskUtils.unmount(sourceDir) } } @@ -102,121 +119,45 @@ class RootInstaller( patchedAPK: File, stockAPK: File?, packageName: String, - version: String, - label: String ) = withContext(Dispatchers.IO) { val remoteFS = awaitRemoteFS() - val assets = app.assets - val modulePath = "$modulesPath/$packageName-revanced" + val patchedPackageName = withContext(Dispatchers.IO) { pm.getPackageInfo(patchedAPK)?.packageName } ?: packageName unmount(packageName) + if (isAppInstalledAsMagiskModule(packageName)) { + uninstallMagiskModule(packageName, patchedPackageName) + } stockAPK?.let { stockApp -> - // TODO: get user id programmatically - execute("pm uninstall -k --user 0 $packageName") - - execute("pm install -r -d --user 0 \"${stockApp.absolutePath}\"") - .assertSuccess("Failed to install stock app") - + MagiskUtils.uninstallKeepData(packageName) + execute("pm install -r -d --user $userId \"${stockApp.absolutePath}\"") stockApp.delete() } - remoteFS.getFile(modulePath).apply { - if (!mkdirs() && !exists()) { - throw Exception("Failed to create module directory") - } - } - - 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) - } - } - - execute( - "chmod 644 $apkPath", - "chown system:system $apkPath", - "chcon u:object_r:apk_data_file:s0 $apkPath", - "chmod +x $modulePath/service.sh" - ).assertSuccess("Failed to set file permissions") - } + MagiskUtils.prepareRootFolder(remoteFS, packageName, patchedAPK) } - suspend fun uninstall(packageName: String) { - val remoteFS = awaitRemoteFS() - if (isAppMounted(packageName)) - unmount(packageName) - - remoteFS.getFile("$modulesPath/$packageName-revanced").deleteRecursively() - .also { if (!it) throw Exception("Failed to delete files") } + suspend fun installAsMagiskModule( + patchedAPK: File, + packageName: String, + patchedPackageName: String, + ) = withContext(Dispatchers.IO) { + if (isAppInstalledAsMagiskModule(packageName)) { + uninstallMagiskModule(packageName, patchedPackageName) + } else if (isAppInstalled(packageName)) { + uninstall(packageName) + } + MagiskUtils.prepareMagiskModule(awaitRemoteFS(), packageName, patchedPackageName, patchedAPK) + runCatching { execute("pm install -r -d --user $userId \"${Constants.MOUNTED_APK_PATH(packageName)}\"") } } - companion object { - const val modulesPath = "/data/adb/modules" - - private fun Shell.Result.assertSuccess(errorMessage: String) { - if (!isSuccess) { - throw ShellCommandException( - errorMessage, - code, - out, - err - ) - } - } + suspend fun uninstallMagiskModule(packageName: String, patchedPackageName: String) { + MagiskUtils.uninstallMagiskModule(packageName, patchedPackageName, awaitRemoteFS()) } -} -class ShellCommandException( - val userMessage: String, - val exitCode: Int, - val stdout: List, - val stderr: List -) : Exception(format(userMessage, exitCode, stdout, stderr)) { - companion object { - private fun format( - message: String, - exitCode: Int, - stdout: List, - stderr: List - ): String = - buildString { - appendLine(message) - appendLine("Exit code: $exitCode") - - val output = stdout.filter { it.isNotBlank() } - val errors = stderr.filter { it.isNotBlank() } - - if (output.isNotEmpty()) { - appendLine("stdout:") - output.forEach(::appendLine) - } - if (errors.isNotEmpty()) { - appendLine("stderr:") - errors.forEach(::appendLine) - } - } + suspend fun uninstall(packageName: String) { + if (isAppMounted(packageName)) unmount(packageName) + MagiskUtils.uninstall(packageName, awaitRemoteFS()) } } diff --git a/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt index 82fddad0a6..d85716f721 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt @@ -116,6 +116,13 @@ enum class DialogKind( contentStringResId = R.string.installation_timeout_description, confirmButton = installerStatusDialogButton(R.string.try_again) { it.install() }, dismissButton = installerStatusDialogButton(R.string.cancel), + ), + SUCCESS_MAGISK( + flag = 1000, + title = R.string.magisk_install_success_title, + contentStringResId = R.string.magisk_install_success_description, + confirmButton = installerStatusDialogButton(R.string.restart_now) { it.reboot() }, + dismissButton = installerStatusDialogButton(R.string.close), ); // Needed due to the @FromValue annotation. diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt index aa5c1b748c..c1033b032a 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/InstallPickerDialog.kt @@ -20,6 +20,7 @@ import app.revanced.manager.util.transparentListItemColors @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun InstallPickerDialog( + isMagiskInstalled: Boolean, onDismiss: () -> Unit, onConfirm: (InstallType) -> Unit ) { @@ -47,6 +48,8 @@ fun InstallPickerDialog( text = { Column(modifier = Modifier.verticalScroll(rememberScrollState())) { InstallType.entries.forEach { + // Dont show magisk if its not installed + if (it == InstallType.MAGISK && !isMagiskInstalled) return@forEach ListItem( modifier = Modifier.clickable { selectedInstallType = it }, leadingContent = { diff --git a/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt b/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt index 410b64c1bd..1c080c53c0 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/InstallerModel.kt @@ -3,4 +3,5 @@ package app.revanced.manager.ui.model interface InstallerModel { fun reinstall() fun install() + fun reboot() } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/model/RootStatus.kt b/app/src/main/java/app/revanced/manager/ui/model/RootStatus.kt new file mode 100644 index 0000000000..59bbda6ebb --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/model/RootStatus.kt @@ -0,0 +1,9 @@ +package app.revanced.manager.ui.model + +import app.revanced.manager.R + +enum class RootCheckResult(val displayName: Int) { + GRANTED(R.string.generic_active), + DENIED(R.string.generic_inactive), + UNAVAILABLE(R.string.generic_not_available) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt index 49257b5c47..0abdf1e90f 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt @@ -105,6 +105,13 @@ fun InstalledAppInfoScreen( style = MaterialTheme.typography.bodySmall ) } + + if (installedApp.installType == InstallType.MAGISK) { + Text( + text = stringResource(if (viewModel.appInfo != null) R.string.generic_active else R.string.generic_inactive), + style = MaterialTheme.typography.bodySmall + ) + } } Row( @@ -142,6 +149,15 @@ fun InstalledAppInfoScreen( ) } + InstallType.MAGISK -> { + SegmentedButton( + icon = Icons.Outlined.SettingsBackupRestore, + text = stringResource(R.string.unpatch), + onClick = { showUninstallDialog = true }, + enabled = viewModel.rootInstaller.hasRootAccess() + ) + } + } SegmentedButton( @@ -150,7 +166,7 @@ fun InstalledAppInfoScreen( onClick = { onPatchClick(installedApp.originalPackageName) }, - enabled = installedApp.installType != InstallType.MOUNT || viewModel.rootInstaller.hasRootAccess() + enabled = installedApp.installType == InstallType.DEFAULT || viewModel.rootInstaller.hasRootAccess() ) } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index 0d13a647b8..1c402edd07 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -41,6 +41,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -61,6 +62,8 @@ import app.revanced.manager.ui.component.patcher.InstallPickerDialog import app.revanced.manager.ui.component.patcher.Steps import app.revanced.manager.ui.model.StepCategory import app.revanced.manager.ui.viewmodel.PatcherViewModel +import app.revanced.manager.ui.model.RootCheckResult +import kotlinx.coroutines.launch import app.revanced.manager.util.APK_MIMETYPE import app.revanced.manager.util.EventEffect import app.revanced.manager.util.toast @@ -87,6 +90,8 @@ fun PatcherScreen( } val patcherSucceeded by viewModel.patcherSucceeded.observeAsState(null) + val coroutineScope = rememberCoroutineScope() + var isCheckingRoot by remember { mutableStateOf(false) } val canInstall by remember { derivedStateOf { patcherSucceeded == true && (viewModel.installedPackageName != null || !viewModel.isInstalling) } } var showInstallPicker by rememberSaveable { mutableStateOf(false) } var showDismissConfirmationDialog by rememberSaveable { mutableStateOf(false) } @@ -117,6 +122,7 @@ fun PatcherScreen( if (showInstallPicker) InstallPickerDialog( + isMagiskInstalled = viewModel.isMagiskInstalled, onDismiss = { showInstallPicker = false }, onConfirm = viewModel::install ) @@ -243,10 +249,25 @@ fun PatcherScreen( ) }, onClick = { - if (viewModel.installedPackageName == null) - if (viewModel.isDeviceRooted()) showInstallPicker = true - else viewModel.install(InstallType.DEFAULT) - else viewModel.open() + if (viewModel.installedPackageName == null) { + isCheckingRoot = true + coroutineScope.launch { + try { + when (viewModel.requestRootForInstall()) { + RootCheckResult.GRANTED -> showInstallPicker = true + RootCheckResult.DENIED, + RootCheckResult.UNAVAILABLE -> viewModel.install(InstallType.DEFAULT) + } + } catch (_: Exception) { + context.toast(R.string.root_check_failed) + viewModel.install(InstallType.DEFAULT) + } finally { + isCheckingRoot = false + } + } + } else { + viewModel.open() + } }, elevation = FloatingActionButtonDefaults.elevation( defaultElevation = 0.dp, diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt index 771aebab41..50ec82fda2 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt @@ -48,6 +48,7 @@ import app.revanced.manager.ui.component.settings.BooleanItem import app.revanced.manager.ui.component.settings.IntegerItem import app.revanced.manager.ui.component.settings.SafeguardBooleanItem import app.revanced.manager.ui.component.settings.SettingsListItem +import app.revanced.manager.ui.model.RootCheckResult import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel import app.revanced.manager.util.toast import app.revanced.manager.util.withHapticFeedback @@ -61,6 +62,7 @@ fun AdvancedSettingsScreen( ) { val context = LocalContext.current val resources = LocalResources.current + LaunchedEffect(Unit) { viewModel.refreshRootStatus() } val memoryLimit = remember(resources) { val activityManager = context.getSystemService()!! resources.getString( @@ -209,6 +211,7 @@ fun AdvancedSettingsScreen( it?.let(viewModel::exportDebugLogs) } val clipboard = remember { context.getSystemService()!! } + val rootStatusText = viewModel.rootStatus?.let { stringResource(it.displayName) } ?: stringResource(R.string.generic_checking) val deviceContent = """ Version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) Build type: ${BuildConfig.BUILD_TYPE} @@ -216,6 +219,7 @@ fun AdvancedSettingsScreen( Android version: ${Build.VERSION.RELEASE} (${Build.VERSION.SDK_INT}) Supported Archs: ${Build.SUPPORTED_ABIS.joinToString(", ")} Memory limit: $memoryLimit + Root: $rootStatusText """.trimIndent() SettingsListItem( headlineContent = stringResource(R.string.debug_logs_export), diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt index 420bc4e39b..ec971bf79f 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt @@ -3,12 +3,17 @@ package app.revanced.manager.ui.viewmodel import android.app.Application import android.net.Uri import android.util.Log +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.domain.installer.RootInstaller import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.DownloaderRepository import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.ui.model.RootCheckResult import app.revanced.manager.util.tag import app.revanced.manager.util.toast import com.github.pgreze.process.Redirect @@ -27,8 +32,20 @@ class AdvancedSettingsViewModel( val prefs: PreferencesManager, private val app: Application, private val patchBundleRepository: PatchBundleRepository, - private val downloaderRepository: DownloaderRepository + private val downloaderRepository: DownloaderRepository, + private val rootInstaller: RootInstaller ) : ViewModel() { + var rootStatus by mutableStateOf(null) + private set + + init { refreshRootStatus() } + + fun refreshRootStatus() { + viewModelScope.launch(Dispatchers.IO) { + rootStatus = rootInstaller.checkRootStatus() + } + } + val debugLogFileName: String get() { val time = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now()) diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppsViewModel.kt index 09ac84f171..9968d85977 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppsViewModel.kt @@ -130,9 +130,18 @@ class AppsViewModel( } catch (_: RootServiceException) { } } + if (app.installType == InstallType.MAGISK) { + try { + if (!rootInstaller.isAppInstalledAsMagiskModule(app.currentPackageName)) { + installedAppsRepository.delete(app) + return@withContext null + } + } catch (_: RootServiceException) { } + } + val packageInfo = pm.getPackageInfo(app.currentPackageName) - if (packageInfo == null && app.installType != InstallType.MOUNT) { + if (packageInfo == null && app.installType != InstallType.MOUNT && app.installType != InstallType.MAGISK) { installedAppsRepository.delete(app) return@withContext null } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt index 70914d5122..43fe646a6f 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt @@ -109,6 +109,8 @@ class InstalledAppInfoViewModel( } InstallType.MOUNT -> rootInstaller.uninstall(app.currentPackageName) + + InstallType.MAGISK -> rootInstaller.uninstallMagiskModule(app.originalPackageName, app.currentPackageName) } installedAppRepository.delete(app) onBackClick() diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index 32d2098270..1cdf3be2d1 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -50,6 +50,7 @@ import app.revanced.manager.patcher.patch.PatchBundleInfo import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection import app.revanced.manager.patcher.worker.PatcherWorker import app.revanced.manager.ui.model.InstallerModel +import app.revanced.manager.ui.model.RootCheckResult import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.State import app.revanced.manager.ui.model.Step @@ -133,6 +134,11 @@ class PatcherViewModel( var isInstalling by mutableStateOf(false) private set + var isMagiskInstalled by mutableStateOf(false) + private set + + private var lastInstallType = InstallType.DEFAULT + private var currentActivityRequest: Pair, String>? by mutableStateOf( null ) @@ -271,6 +277,12 @@ class PatcherViewModel( viewModelScope.launch { installedApp = installedAppRepository.get(packageName) } + + viewModelScope.launch(Dispatchers.IO) { + if (rootInstaller.isDeviceRooted()) { + isMagiskInstalled = rootInstaller.isMagiskInstalled() + } + } } @OptIn(DelicateCoroutinesApi::class) @@ -339,6 +351,20 @@ class PatcherViewModel( fun isDeviceRooted() = rootInstaller.isDeviceRooted() + suspend fun requestRootForInstall(): RootCheckResult { + if (!isDeviceRooted()) return RootCheckResult.UNAVAILABLE + + val hasRoot = withContext(Dispatchers.IO) { rootInstaller.requestRoot() } + return if (hasRoot) { + isMagiskInstalled = true + RootCheckResult.GRANTED + } else { + logger.warn(app.getString(R.string.root_access_denied_warning)) + withContext(Dispatchers.Main) { app.toast(R.string.root_access_denied_warning) } + RootCheckResult.DENIED + } + } + fun rejectInteraction() { currentActivityRequest?.first?.complete(false) } @@ -419,7 +445,7 @@ class PatcherViewModel( val statFs = StatFs(Environment.getDataDirectory().path) - val hasRoot = rootInstaller.hasRootAccess() + val rootStatus = context.getString(rootInstaller.checkRootStatus().displayName) val suggestedVersion = patchBundleRepository.suggestedVersions.first()[packageName] val allowIncompatiblePatches = prefs.disablePatchVersionCompatCheck.get() val disableSelectionWarning = prefs.disableSelectionWarning.get() @@ -494,7 +520,7 @@ class PatcherViewModel( addAll(managerConfiguration) addAll(patchingConfiguration) addAll(runtimeConfiguration) - add("Root permissions: ${if (hasRoot) "Yes" else "No"}") + add("Root permissions: $rootStatus") add("RAM: ${Formatter.formatFileSize(context, memInfo.availMem)} / ${Formatter.formatFileSize(context, memInfo.totalMem)} available") add("Storage: ${Formatter.formatFileSize(context, statFs.availableBytes)} / ${Formatter.formatFileSize(context, statFs.totalBytes)} available") add("Android version: ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})") @@ -567,6 +593,7 @@ class PatcherViewModel( fun install(installType: InstallType) = viewModelScope.launch { isInstalling = true + lastInstallType = installType var needsRootUninstall = false try { uiSafe(app, R.string.install_app_fail, "Failed to install") { @@ -605,10 +632,6 @@ class PatcherViewModel( } InstallType.MOUNT -> { - val label = with(pm) { - currentPackageInfo.label() - } - val inputVersion = input.selectedApp.version ?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName } ?: throw Exception("Failed to determine input APK version") @@ -616,7 +639,7 @@ class PatcherViewModel( needsRootUninstall = true // Install as root rootInstaller.install( - outputFile, inputFile, packageName, inputVersion, label + outputFile, inputFile, packageName, ) val bundleInfo = patchBundleRepository.bundleInfoFlow.first() @@ -636,6 +659,35 @@ class PatcherViewModel( needsRootUninstall = false downloadedAppRepository.deleteFor(packageName) } + + InstallType.MAGISK -> { + val inputVersion = input.selectedApp.version + ?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName } + ?: throw Exception("Failed to determine input APK version") + + try { + rootInstaller.installAsMagiskModule( + outputFile, packageName, currentPackageInfo.packageName, + ) + } catch (e: Exception) { + packageInstallerStatus = AndroidPackageInstaller.STATUS_FAILURE + throw e + } + + val bundleInfo = patchBundleRepository.bundleInfoFlow.first() + installedAppRepository.addOrUpdate( + currentPackageInfo.packageName, + packageName, + inputVersion, + InstallType.MAGISK, + input.selectedPatches, + bundleInfo + ) + + installedPackageName = packageName + packageInstallerStatus = 1000 // SUCCESS_MAGISK + downloadedAppRepository.deleteFor(packageName) + } } } } finally { @@ -651,9 +703,12 @@ class PatcherViewModel( } } - override fun install() { - // InstallType.MOUNT is never used here since this overload is for the package installer status dialog. - install(InstallType.DEFAULT) + override fun install() { install(lastInstallType) } + + override fun reboot() { + viewModelScope.launch { + rootInstaller.execute("reboot") + } } override fun reinstall() { diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt index 981143c301..76153af67c 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt @@ -243,6 +243,7 @@ class SelectedAppInfoViewModel( val matchesVersion = requiredVersion == null || installed.version == requiredVersion val usable = when { meta?.installType == InstallType.MOUNT && !hasRoot -> false + meta?.installType == InstallType.MAGISK && !hasRoot -> false meta?.installType == InstallType.DEFAULT -> false else -> true } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c7ae10283e..50353acb6e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -377,6 +377,7 @@ It’s only compatible with these versions: %2$s Default Mount + Magisk module Mounted Not mounted Mount @@ -610,4 +611,13 @@ Tap them for more details. %d selected + Magisk module installed + The patched app has been installed and is immediately active. We recommended restarting your device to fully apply the changes. + Restart now + Active + Inactive + Not available + Checking… + Root access denied. Installing without root. + Failed to check root access. Installing without root.