Skip to content

Commit

Permalink
Merge branch 'compose-dev' into feat/view-bundle-patches
Browse files Browse the repository at this point in the history
  • Loading branch information
Ushie authored Jul 29, 2024
2 parents b9323be + c18901c commit 70a1453
Show file tree
Hide file tree
Showing 53 changed files with 715 additions and 610 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/pr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ jobs:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v3

- name: Build with Gradle
env:
Expand All @@ -38,7 +38,7 @@ jobs:
run: mv app/build/outputs/apk/release/app-release.apk revanced-manager-${{ env.COMMIT_HASH }}.apk

- name: Upload build
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: revanced-manager
path: revanced-manager-${{ env.COMMIT_HASH }}.apk
6 changes: 2 additions & 4 deletions .github/workflows/release-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@ jobs:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/gradle-build-action@v2
with:
cache-disabled: true
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v3

- name: Build with Gradle
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update-documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
name: Dispatch event to documentation repository
if: github.ref == 'refs/heads/main'
steps:
- uses: peter-evans/repository-dispatch@v2
- uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
repository: revanced/revanced-documentation
Expand Down
9 changes: 6 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ android {
targetSdk = 34
versionCode = 1
versionName = "0.0.1"
resourceConfigurations.addAll(listOf(
"en",
))
vectorDrawables.useSupportLibrary = true
}

Expand Down Expand Up @@ -88,6 +85,12 @@ android {
buildFeatures.aidl = true
buildFeatures.buildConfig=true

android {
androidResources {
generateLocaleConfig = true
}
}

composeOptions.kotlinCompilerExtensionVersion = "1.5.10"
externalNativeBuild {
cmake {
Expand Down
8 changes: 0 additions & 8 deletions app/src/main/java/app/revanced/manager/ManagerApplication.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
}
Expand Down
2 changes: 0 additions & 2 deletions app/src/main/java/app/revanced/manager/di/RootModule.kt
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ val viewModelModule = module {
viewModelOf(::ChangelogsViewModel)
viewModelOf(::ImportExportViewModel)
viewModelOf(::AboutViewModel)
viewModelOf(::DeveloperOptionsViewModel)
viewModelOf(::ContributorViewModel)
viewModelOf(::DownloadsViewModel)
viewModelOf(::InstalledAppsViewModel)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import app.revanced.manager.R
import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
import app.revanced.manager.patcher.patch.PatchBundle
import app.revanced.manager.util.tag
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
Expand Down Expand Up @@ -80,8 +82,8 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
* Create a flow that emits the [app.revanced.manager.data.room.bundles.BundleProperties] of this [PatchBundleSource].
* The flow will emit null if the associated [PatchBundleSource] is deleted.
*/
fun propsFlow() = configRepository.getProps(uid)
suspend fun getProps() = configRepository.getProps(uid).first()!!
fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default)
suspend fun getProps() = propsFlow().first()!!

suspend fun currentVersion() = getProps().versionInfo
protected suspend fun saveVersion(patches: String?, integrations: String?) =
Expand Down
195 changes: 118 additions & 77 deletions app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt
Original file line number Diff line number Diff line change
@@ -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

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

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

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(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 ->
"$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)
}
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")
class RootServiceException : Exception("Root not available")
Loading

0 comments on commit 70a1453

Please sign in to comment.