Skip to content

Commit

Permalink
feat: root installation (ReVanced#1243)
Browse files Browse the repository at this point in the history
  • Loading branch information
CnC-Robert committed Sep 9, 2023
1 parent b4dfcf1 commit bf10af2
Show file tree
Hide file tree
Showing 22 changed files with 684 additions and 129 deletions.
5 changes: 5 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ android {
}

buildFeatures.compose = true
buildFeatures.aidl = true

composeOptions.kotlinCompilerExtensionVersion = "1.5.1"
}
Expand Down Expand Up @@ -124,6 +125,10 @@ dependencies {
implementation(libs.apksign)
implementation(libs.bcpkix.jdk18on)

implementation(libs.libsu.core)
implementation(libs.libsu.service)
implementation(libs.libsu.nio)

// Koin
implementation(libs.koin.android)
implementation(libs.koin.compose)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// IRootService.aidl
package app.revanced.manager;

// Declare any non-default types here with import statements

interface IRootSystemService {
IBinder getFileSystemService();
}
6 changes: 6 additions & 0 deletions app/src/main/assets/root/module.prop
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
id=__PKG_NAME__-ReVanced
name=__LABEL__ ReVanced
version=__VERSION__
versionCode=0
author=ReVanced
description=Mounts the patched apk on top of the original apk
40 changes: 40 additions & 0 deletions app/src/main/assets/root/service.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/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"
4 changes: 2 additions & 2 deletions app/src/main/java/app/revanced/manager/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val vm: MainViewModel = getActivityViewModel()

installSplashScreen()

val vm: MainViewModel = getActivityViewModel()

setContent {
val theme by vm.prefs.theme.getAsState()
val dynamicColor by vm.prefs.dynamicColor.getAsState()
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/app/revanced/manager/ManagerApplication.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
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
Expand All @@ -37,6 +44,7 @@ class ManagerApplication : Application() {
workerModule,
viewModelModule,
databaseModule,
rootModule
)
}

Expand All @@ -50,6 +58,12 @@ class ManagerApplication : Application() {
.build()
)

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()
}
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/app/revanced/manager/di/RootModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package app.revanced.manager.domain.installer

import android.app.Application
import app.revanced.manager.service.RootConnection
import app.revanced.manager.util.PM
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File

class RootInstaller(
private val app: Application,
private val rootConnection: RootConnection,
private val pm: PM
) {
fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false

fun isAppInstalled(packageName: String) =
rootConnection.remoteFS?.getFile("$modulesPath/$packageName-revanced")
?.exists() ?: throw RootServiceException()

fun isAppMounted(packageName: String): Boolean {
return pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let {
Shell.cmd("mount | grep \"$it\"").exec().isSuccess
} ?: false
}

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"

Shell.cmd("mount -o bind \"$patchedAPK\" \"$stockAPK\"").exec()
.also { if (!it.isSuccess) throw Exception("Failed to mount APK") }
}

fun unmount(packageName: String) {
if (!isAppMounted(packageName)) return

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

suspend fun install(
patchedAPK: File,
stockAPK: File?,
packageName: String,
version: String,
label: String
) {
withContext(Dispatchers.IO) {
rootConnection.remoteFS?.let { remoteFS ->
val assets = app.assets
val modulePath = "$modulesPath/$packageName-revanced"

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

Shell.cmd("pm install \"${stockApp.absolutePath}\"").exec()
.also { if (!it.isSuccess) throw Exception("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)
}
}
}

"$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()
}
}

fun uninstall(packageName: String) {
rootConnection.remoteFS?.let { remoteFS ->
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"
}
}

class RootServiceException: Exception("Root not available")
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ import androidx.core.content.ContextCompat
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.domain.worker.Worker
import app.revanced.manager.domain.worker.WorkerRepository
Expand All @@ -29,7 +32,6 @@ import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.tag
import app.revanced.patcher.extensions.PatchExtensions.options
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.logging.Logger
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -49,6 +51,8 @@ class PatcherWorker(
private val prefs: PreferencesManager by inject()
private val downloadedAppRepository: DownloadedAppRepository by inject()
private val pm: PM by inject()
private val installedAppRepository: InstalledAppRepository by inject()
private val rootInstaller: RootInstaller by inject()

data class Args(
val input: SelectedApp,
Expand All @@ -58,7 +62,9 @@ class PatcherWorker(
val packageName: String,
val packageVersion: String,
val progress: MutableStateFlow<ImmutableList<Step>>,
val logger: ManagerLogger
val logger: ManagerLogger,
val selectedApp: SelectedApp,
val setInputFile: (File) -> Unit
)

companion object {
Expand Down Expand Up @@ -148,6 +154,15 @@ class PatcherWorker(
}

return try {

if (args.selectedApp is SelectedApp.Installed) {
installedAppRepository.get(args.packageName)?.let {
if (it.installType == InstallType.ROOT) {
rootInstaller.unmount(args.packageName)
}
}
}

// TODO: consider passing all the classes directly now that the input no longer needs to be serializable.
val selectedBundles = args.selectedPatches.keys
val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
Expand Down Expand Up @@ -190,11 +205,12 @@ class PatcherWorker(
args.input.version,
it
)
args.setInputFile(it)
updateProgress() // Downloading
}
}

is SelectedApp.Local -> selectedApp.file
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo.sourceDir)
}

Expand Down
36 changes: 36 additions & 0 deletions app/src/main/java/app/revanced/manager/service/RootService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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
import com.topjohnwu.superuser.nio.FileSystemManager

class ManagerRootService : RootService() {
class RootSystemService : IRootSystemService.Stub() {
override fun getFileSystemService() =
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
}
}
Loading

0 comments on commit bf10af2

Please sign in to comment.