feat: dont ask for root on launch

This commit is contained in:
Ax333l 2024-07-05 19:06:10 +02:00
parent f99cdfe926
commit 7c7fb7b343
No known key found for this signature in database
GPG Key ID: D2B4D85271127D23
7 changed files with 144 additions and 119 deletions

View File

@ -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<RootConnection>())
scope.launch {
prefs.preload()
}

View File

@ -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)
}

View File

@ -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<FileSystemManager>()
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>()) {
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
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
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,9 +96,8 @@ class RootInstaller(
packageName: String,
version: String,
label: String
) {
withContext(Dispatchers.IO) {
rootConnection.remoteFS?.let { remoteFS ->
) = withContext(Dispatchers.IO) {
val remoteFS = awaitRemoteFS()
val assets = app.assets
val modulePath = "$modulesPath/$packageName-revanced"
@ -63,12 +106,10 @@ class RootInstaller(
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") }
execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app")
}
Shell.cmd("pm install \"${stockApp.absolutePath}\"").exec()
.also { if (!it.isSuccess) throw Exception("Failed to install stock app") }
execute("pm install \"${stockApp.absolutePath}\"").assertSuccess("Failed to install stock app")
}
remoteFS.getFile(modulePath).mkdir()
@ -101,31 +142,31 @@ class RootInstaller(
}
}
Shell.cmd(
execute(
"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()
).assertSuccess("Failed to set file permissions")
}
}
fun uninstall(packageName: String) {
rootConnection.remoteFS?.let { remoteFS ->
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()
}
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")
class RootServiceException : Exception("Root not available")

View File

@ -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()
}

View File

@ -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)

View File

@ -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)

View File

@ -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 {
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()))
}
}
}
}