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 package app.revanced.manager
import android.app.Application import android.app.Application
import android.content.Intent
import app.revanced.manager.di.* import app.revanced.manager.di.*
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.service.ManagerRootService
import app.revanced.manager.service.RootConnection
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import coil.Coil import coil.Coil
import coil.ImageLoader import coil.ImageLoader
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.BuilderImpl import com.topjohnwu.superuser.internal.BuilderImpl
import com.topjohnwu.superuser.ipc.RootService
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.zhanghai.android.appiconloader.coil.AppIconFetcher import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer 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.android.inject
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
@ -61,9 +56,6 @@ class ManagerApplication : Application() {
val shellBuilder = BuilderImpl.create().setFlags(Shell.FLAG_MOUNT_MASTER) val shellBuilder = BuilderImpl.create().setFlags(Shell.FLAG_MOUNT_MASTER)
Shell.setDefaultBuilder(shellBuilder) Shell.setDefaultBuilder(shellBuilder)
val intent = Intent(this, ManagerRootService::class.java)
RootService.bind(intent, get<RootConnection>())
scope.launch { scope.launch {
prefs.preload() prefs.preload()
} }

View File

@ -1,11 +1,9 @@
package app.revanced.manager.di package app.revanced.manager.di
import app.revanced.manager.domain.installer.RootInstaller import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.service.RootConnection
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
val rootModule = module { val rootModule = module {
singleOf(::RootConnection)
singleOf(::RootInstaller) singleOf(::RootInstaller)
} }

View File

@ -1,49 +1,93 @@
package app.revanced.manager.domain.installer package app.revanced.manager.domain.installer
import android.app.Application 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 app.revanced.manager.util.PM
import com.topjohnwu.superuser.Shell 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.Dispatchers
import kotlinx.coroutines.time.withTimeoutOrNull
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.time.Duration
class RootInstaller( class RootInstaller(
private val app: Application, private val app: Application,
private val rootConnection: RootConnection,
private val pm: PM 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 hasRootAccess() = Shell.isAppGrantedRoot() ?: false
fun isAppInstalled(packageName: String) = suspend fun isAppInstalled(packageName: String) =
rootConnection.remoteFS?.getFile("$modulesPath/$packageName-revanced") awaitRemoteFS().getFile("$modulesPath/$packageName-revanced").exists()
?.exists() ?: throw RootServiceException()
fun isAppMounted(packageName: String): Boolean { suspend fun isAppMounted(packageName: String) = withContext(Dispatchers.IO) {
return pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let { pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let {
Shell.cmd("mount | grep \"$it\"").exec().isSuccess execute("mount | grep \"$it\"").isSuccess
} ?: false } ?: false
} }
fun mount(packageName: String) { suspend fun mount(packageName: String) {
if (isAppMounted(packageName)) return if (isAppMounted(packageName)) return
withContext(Dispatchers.IO) {
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
?: throw Exception("Failed to load application info") ?: throw Exception("Failed to load application info")
val patchedAPK = "$modulesPath/$packageName-revanced/$packageName.apk" val patchedAPK = "$modulesPath/$packageName-revanced/$packageName.apk"
Shell.cmd("mount -o bind \"$patchedAPK\" \"$stockAPK\"").exec() execute("mount -o bind \"$patchedAPK\" \"$stockAPK\"").assertSuccess("Failed to mount APK")
.also { if (!it.isSuccess) throw Exception("Failed to mount APK") } }
} }
fun unmount(packageName: String) { suspend fun unmount(packageName: String) {
if (!isAppMounted(packageName)) return if (!isAppMounted(packageName)) return
withContext(Dispatchers.IO) {
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
?: throw Exception("Failed to load application info") ?: throw Exception("Failed to load application info")
Shell.cmd("umount -l \"$stockAPK\"").exec() execute("umount -l \"$stockAPK\"").assertSuccess("Failed to unmount APK")
.also { if (!it.isSuccess) throw Exception("Failed to unmount APK") } }
} }
suspend fun install( suspend fun install(
@ -52,9 +96,8 @@ class RootInstaller(
packageName: String, packageName: String,
version: String, version: String,
label: String label: String
) { ) = withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) { val remoteFS = awaitRemoteFS()
rootConnection.remoteFS?.let { remoteFS ->
val assets = app.assets val assets = app.assets
val modulePath = "$modulesPath/$packageName-revanced" val modulePath = "$modulesPath/$packageName-revanced"
@ -63,12 +106,10 @@ class RootInstaller(
stockAPK?.let { stockApp -> stockAPK?.let { stockApp ->
pm.getPackageInfo(packageName)?.let { packageInfo -> pm.getPackageInfo(packageName)?.let { packageInfo ->
if (packageInfo.versionName <= version) if (packageInfo.versionName <= version)
Shell.cmd("pm uninstall -k --user 0 $packageName").exec() execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app")
.also { if (!it.isSuccess) throw Exception("Failed to uninstall stock app") }
} }
Shell.cmd("pm install \"${stockApp.absolutePath}\"").exec() execute("pm install \"${stockApp.absolutePath}\"").assertSuccess("Failed to install stock app")
.also { if (!it.isSuccess) throw Exception("Failed to install stock app") }
} }
remoteFS.getFile(modulePath).mkdir() remoteFS.getFile(modulePath).mkdir()
@ -101,30 +142,30 @@ class RootInstaller(
} }
} }
Shell.cmd( execute(
"chmod 644 $apkPath", "chmod 644 $apkPath",
"chown system:system $apkPath", "chown system:system $apkPath",
"chcon u:object_r:apk_data_file:s0 $apkPath", "chcon u:object_r:apk_data_file:s0 $apkPath",
"chmod +x $modulePath/service.sh" "chmod +x $modulePath/service.sh"
).exec() ).assertSuccess("Failed to set file permissions")
.let { if (!it.isSuccess) throw Exception("Failed to set file permissions") }
}
} ?: throw RootServiceException()
} }
} }
fun uninstall(packageName: String) { suspend fun uninstall(packageName: String) {
rootConnection.remoteFS?.let { remoteFS -> val remoteFS = awaitRemoteFS()
if (isAppMounted(packageName)) if (isAppMounted(packageName))
unmount(packageName) unmount(packageName)
remoteFS.getFile("$modulesPath/$packageName-revanced").deleteRecursively() remoteFS.getFile("$modulesPath/$packageName-revanced").deleteRecursively()
.also { if (!it) throw Exception("Failed to delete files") } .also { if (!it) throw Exception("Failed to delete files") }
} ?: throw RootServiceException()
} }
companion object { companion object {
const val modulesPath = "/data/adb/modules" const val modulesPath = "/data/adb/modules"
private fun Shell.Result.assertSuccess(errorMessage: String) {
if (!isSuccess) throw Exception(errorMessage)
}
} }
} }

View File

@ -1,8 +1,6 @@
package app.revanced.manager.service package app.revanced.manager.service
import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder import android.os.IBinder
import app.revanced.manager.IRootSystemService import app.revanced.manager.IRootSystemService
import com.topjohnwu.superuser.ipc.RootService import com.topjohnwu.superuser.ipc.RootService
@ -14,23 +12,5 @@ class ManagerRootService : RootService() {
FileSystemManager.getService() FileSystemManager.getService()
} }
override fun onBind(intent: Intent): IBinder { override fun onBind(intent: Intent): IBinder = RootSystemService()
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
}
} }

View File

@ -83,7 +83,7 @@ fun InstalledAppInfoScreen(
if (viewModel.installedApp.installType == InstallType.ROOT) { if (viewModel.installedApp.installType == InstallType.ROOT) {
Text( Text(
text = if (viewModel.rootInstaller.isAppMounted(viewModel.installedApp.currentPackageName)) { text = if (viewModel.isMounted) {
stringResource(R.string.mounted) stringResource(R.string.mounted)
} else { } else {
stringResource(R.string.not_mounted) stringResource(R.string.not_mounted)

View File

@ -44,12 +44,18 @@ class InstalledAppInfoViewModel(
var appInfo: PackageInfo? by mutableStateOf(null) var appInfo: PackageInfo? by mutableStateOf(null)
private set private set
var appliedPatches: PatchSelection? by mutableStateOf(null) var appliedPatches: PatchSelection? by mutableStateOf(null)
var isMounted by mutableStateOf(rootInstaller.isAppMounted(installedApp.currentPackageName)) var isMounted by mutableStateOf(false)
private set private set
init {
viewModelScope.launch {
isMounted = rootInstaller.isAppMounted(installedApp.currentPackageName)
}
}
fun launch() = pm.launch(installedApp.currentPackageName) fun launch() = pm.launch(installedApp.currentPackageName)
fun mountOrUnmount() { fun mountOrUnmount() = viewModelScope.launch {
try { try {
if (isMounted) if (isMounted)
rootInstaller.unmount(installedApp.currentPackageName) 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.simpleMessage
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.time.withTimeout
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.File import java.io.File
import java.nio.file.Files import java.nio.file.Files
import java.time.Duration
import java.util.UUID import java.util.UUID
@Stable @Stable
@ -177,6 +183,7 @@ class PatcherViewModel(
} }
} }
@OptIn(DelicateCoroutinesApi::class)
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
app.unregisterReceiver(installBroadcastReceiver) app.unregisterReceiver(installBroadcastReceiver)
@ -188,15 +195,16 @@ class PatcherViewModel(
} }
is SelectedApp.Installed -> { is SelectedApp.Installed -> {
try { GlobalScope.launch(Dispatchers.Main) {
uiSafe(app, R.string.failed_to_mount, "Failed to mount") {
installedApp?.let { installedApp?.let {
if (it.installType == InstallType.ROOT) { if (it.installType == InstallType.ROOT) {
withTimeout(Duration.ofMinutes(1L)) {
rootInstaller.mount(packageName) rootInstaller.mount(packageName)
} }
} }
} catch (e: Exception) { }
Log.e(tag, "Failed to mount", e) }
app.toast(app.getString(R.string.failed_to_mount, e.simpleMessage()))
} }
} }