From 7c7fb7b343bf09fb92f5c870269bdbe41595b2a0 Mon Sep 17 00:00:00 2001 From: Ax333l Date: Fri, 5 Jul 2024 19:06:10 +0200 Subject: [PATCH] feat: dont ask for root on launch --- .../revanced/manager/ManagerApplication.kt | 8 - .../app/revanced/manager/di/RootModule.kt | 2 - .../manager/domain/installer/RootInstaller.kt | 197 +++++++++++------- .../revanced/manager/service/RootService.kt | 22 +- .../ui/screen/InstalledAppInfoScreen.kt | 2 +- .../ui/viewmodel/InstalledAppInfoViewModel.kt | 10 +- .../manager/ui/viewmodel/PatcherViewModel.kt | 22 +- 7 files changed, 144 insertions(+), 119 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/ManagerApplication.kt index 8a2811bd..66ab2483 100644 --- a/app/src/main/java/app/revanced/manager/ManagerApplication.kt +++ b/app/src/main/java/app/revanced/manager/ManagerApplication.kt @@ -1,23 +1,18 @@ 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 @@ -61,9 +56,6 @@ class ManagerApplication : Application() { val shellBuilder = BuilderImpl.create().setFlags(Shell.FLAG_MOUNT_MASTER) Shell.setDefaultBuilder(shellBuilder) - val intent = Intent(this, ManagerRootService::class.java) - RootService.bind(intent, get()) - scope.launch { prefs.preload() } diff --git a/app/src/main/java/app/revanced/manager/di/RootModule.kt b/app/src/main/java/app/revanced/manager/di/RootModule.kt index acfad58d..1e27555b 100644 --- a/app/src/main/java/app/revanced/manager/di/RootModule.kt +++ b/app/src/main/java/app/revanced/manager/di/RootModule.kt @@ -1,11 +1,9 @@ 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) } \ No newline at end of file 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 0b2ee413..9ca6cd9b 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 @@ -1,49 +1,93 @@ package app.revanced.manager.domain.installer import android.app.Application -import app.revanced.manager.service.RootConnection +import android.content.ComponentName +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import app.revanced.manager.IRootSystemService +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 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, - private val rootConnection: RootConnection, private val pm: PM -) { +) : ServiceConnection { + private var remoteFS = CompletableDeferred() + + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + val ipc = IRootSystemService.Stub.asInterface(service) + val binder = ipc.fileSystemService + + remoteFS.complete(FileSystemManager.getRemote(binder)) + } + + override fun onServiceDisconnected(name: ComponentName?) { + remoteFS = CompletableDeferred() + } + + private suspend fun awaitRemoteFS(): FileSystemManager { + if (remoteFS.isActive) { + withContext(Dispatchers.Main) { + val intent = Intent(app, ManagerRootService::class.java) + RootService.bind(intent, this@RootInstaller) + } + } + + return withTimeoutOrNull(Duration.ofSeconds(120L)) { + remoteFS.await() + } ?: throw RootServiceException() + } + + private suspend fun getShell() = with(CompletableDeferred()) { + Shell.getShell(::complete) + + await() + } + + suspend fun execute(vararg commands: String) = getShell().newJob().add(*commands).exec() + fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false - fun isAppInstalled(packageName: String) = - rootConnection.remoteFS?.getFile("$modulesPath/$packageName-revanced") - ?.exists() ?: throw RootServiceException() + suspend fun isAppInstalled(packageName: String) = + awaitRemoteFS().getFile("$modulesPath/$packageName-revanced").exists() - fun isAppMounted(packageName: String): Boolean { - return pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let { - Shell.cmd("mount | grep \"$it\"").exec().isSuccess + suspend fun isAppMounted(packageName: String) = withContext(Dispatchers.IO) { + pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let { + execute("mount | grep \"$it\"").isSuccess } ?: false } - fun mount(packageName: String) { + suspend 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" + withContext(Dispatchers.IO) { + 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") } + execute("mount -o bind \"$patchedAPK\" \"$stockAPK\"").assertSuccess("Failed to mount APK") + } } - fun unmount(packageName: String) { + suspend fun unmount(packageName: String) { if (!isAppMounted(packageName)) return - val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir - ?: throw Exception("Failed to load application info") + withContext(Dispatchers.IO) { + 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") } + execute("umount -l \"$stockAPK\"").assertSuccess("Failed to unmount APK") + } } suspend fun install( @@ -52,80 +96,77 @@ class RootInstaller( packageName: String, version: String, label: String - ) { - withContext(Dispatchers.IO) { - rootConnection.remoteFS?.let { remoteFS -> - val assets = app.assets - val modulePath = "$modulesPath/$packageName-revanced" + ) = withContext(Dispatchers.IO) { + val remoteFS = awaitRemoteFS() + val assets = app.assets + val modulePath = "$modulesPath/$packageName-revanced" - unmount(packageName) + 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") } + stockAPK?.let { stockApp -> + pm.getPackageInfo(packageName)?.let { packageInfo -> + if (packageInfo.versionName <= version) + execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app") + } + + execute("pm install \"${stockApp.absolutePath}\"").assertSuccess("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) } + } + } - Shell.cmd("pm install \"${stockApp.absolutePath}\"").exec() - .also { if (!it.isSuccess) throw Exception("Failed to install stock app") } - } + "$modulePath/$packageName.apk".let { apkPath -> - 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) - } + 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) } } - "$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() + 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") } } - fun uninstall(packageName: String) { - rootConnection.remoteFS?.let { remoteFS -> - if (isAppMounted(packageName)) - unmount(packageName) + 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") } - } ?: throw RootServiceException() + remoteFS.getFile("$modulesPath/$packageName-revanced").deleteRecursively() + .also { if (!it) throw Exception("Failed to delete files") } } companion object { const val modulesPath = "/data/adb/modules" + + private fun Shell.Result.assertSuccess(errorMessage: String) { + if (!isSuccess) throw Exception(errorMessage) + } } } -class RootServiceException: Exception("Root not available") \ No newline at end of file +class RootServiceException : Exception("Root not available") \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/service/RootService.kt b/app/src/main/java/app/revanced/manager/service/RootService.kt index 6fa68b6c..ed475e50 100644 --- a/app/src/main/java/app/revanced/manager/service/RootService.kt +++ b/app/src/main/java/app/revanced/manager/service/RootService.kt @@ -1,8 +1,6 @@ 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 @@ -14,23 +12,5 @@ class ManagerRootService : RootService() { 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 - } + override fun onBind(intent: Intent): IBinder = RootSystemService() } \ 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 cd2752ca..2727e290 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 @@ -83,7 +83,7 @@ fun InstalledAppInfoScreen( if (viewModel.installedApp.installType == InstallType.ROOT) { Text( - text = if (viewModel.rootInstaller.isAppMounted(viewModel.installedApp.currentPackageName)) { + text = if (viewModel.isMounted) { stringResource(R.string.mounted) } else { stringResource(R.string.not_mounted) 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 aa0a8203..7e610f55 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 @@ -44,12 +44,18 @@ class InstalledAppInfoViewModel( var appInfo: PackageInfo? by mutableStateOf(null) private set var appliedPatches: PatchSelection? by mutableStateOf(null) - var isMounted by mutableStateOf(rootInstaller.isAppMounted(installedApp.currentPackageName)) + var isMounted by mutableStateOf(false) private set + init { + viewModelScope.launch { + isMounted = rootInstaller.isAppMounted(installedApp.currentPackageName) + } + } + fun launch() = pm.launch(installedApp.currentPackageName) - fun mountOrUnmount() { + fun mountOrUnmount() = viewModelScope.launch { try { if (isMounted) rootInstaller.unmount(installedApp.currentPackageName) 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 15a51a20..27049127 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 @@ -39,15 +39,21 @@ 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 app.revanced.manager.util.uiSafe +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.time.withTimeout import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout import org.koin.core.component.KoinComponent import org.koin.core.component.inject import java.io.File import java.nio.file.Files +import java.time.Duration import java.util.UUID @Stable @@ -177,6 +183,7 @@ class PatcherViewModel( } } + @OptIn(DelicateCoroutinesApi::class) override fun onCleared() { super.onCleared() app.unregisterReceiver(installBroadcastReceiver) @@ -188,15 +195,16 @@ class PatcherViewModel( } is SelectedApp.Installed -> { - try { - installedApp?.let { - if (it.installType == InstallType.ROOT) { - rootInstaller.mount(packageName) + GlobalScope.launch(Dispatchers.Main) { + uiSafe(app, R.string.failed_to_mount, "Failed to mount") { + installedApp?.let { + if (it.installType == InstallType.ROOT) { + withTimeout(Duration.ofMinutes(1L)) { + rootInstaller.mount(packageName) + } + } } } - } catch (e: Exception) { - Log.e(tag, "Failed to mount", e) - app.toast(app.getString(R.string.failed_to_mount, e.simpleMessage())) } }