mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-05-02 23:04:25 +02:00
feat: dont ask for root on launch
This commit is contained in:
parent
f99cdfe926
commit
7c7fb7b343
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
@ -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
|
||||||
|
|
||||||
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
|
withContext(Dispatchers.IO) {
|
||||||
?: throw Exception("Failed to load application info")
|
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
|
||||||
val patchedAPK = "$modulesPath/$packageName-revanced/$packageName.apk"
|
?: throw Exception("Failed to load application info")
|
||||||
|
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
|
||||||
|
|
||||||
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
|
withContext(Dispatchers.IO) {
|
||||||
?: throw Exception("Failed to load application info")
|
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
|
||||||
|
?: 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,80 +96,77 @@ 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"
|
|
||||||
|
|
||||||
unmount(packageName)
|
unmount(packageName)
|
||||||
|
|
||||||
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") }
|
}
|
||||||
|
|
||||||
|
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()
|
"$modulePath/$packageName.apk".let { apkPath ->
|
||||||
.also { if (!it.isSuccess) throw Exception("Failed to install stock app") }
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteFS.getFile(modulePath).mkdir()
|
remoteFS.getFile(patchedAPK.absolutePath)
|
||||||
|
.also { if (!it.exists()) throw Exception("File doesn't exist") }
|
||||||
listOf(
|
.newInputStream().use { inputStream ->
|
||||||
"service.sh",
|
remoteFS.getFile(apkPath).newOutputStream().use { outputStream ->
|
||||||
"module.prop",
|
inputStream.copyTo(outputStream)
|
||||||
).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 ->
|
execute(
|
||||||
|
"chmod 644 $apkPath",
|
||||||
remoteFS.getFile(patchedAPK.absolutePath)
|
"chown system:system $apkPath",
|
||||||
.also { if (!it.exists()) throw Exception("File doesn't exist") }
|
"chcon u:object_r:apk_data_file:s0 $apkPath",
|
||||||
.newInputStream().use { inputStream ->
|
"chmod +x $modulePath/service.sh"
|
||||||
remoteFS.getFile(apkPath).newOutputStream().use { outputStream ->
|
).assertSuccess("Failed to set file permissions")
|
||||||
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) {
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RootServiceException: Exception("Root not available")
|
class RootServiceException : Exception("Root not available")
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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) {
|
||||||
installedApp?.let {
|
uiSafe(app, R.string.failed_to_mount, "Failed to mount") {
|
||||||
if (it.installType == InstallType.ROOT) {
|
installedApp?.let {
|
||||||
rootInstaller.mount(packageName)
|
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()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user