added root package manager and patcher classes

This commit is contained in:
X1nto 2021-12-12 14:56:07 +04:00
parent 22ac84e448
commit cbd48dce72
16 changed files with 513 additions and 46 deletions

View File

@ -123,6 +123,11 @@ dependencies {
implementation("com.github.skydoves:orchestra-colorpicker:1.1.0")
val libsuVersion = "3.1.2"
implementation("com.github.topjohnwu.libsu:core:$libsuVersion")
implementation("com.github.topjohnwu.libsu:io:$libsuVersion")
implementation("com.github.topjohnwu.libsu:busybox:$libsuVersion")
val koinVersion = "3.1.3"
implementation("io.insert-koin:koin-android:$koinVersion")
implementation("io.insert-koin:koin-androidx-compose:$koinVersion")

View File

@ -8,6 +8,11 @@ fun getVancedYoutubePath(
context: Context
) = context.getExternalFilesDir("vanced_youtube")!!.path + "/$version/$variant"
fun getStockYoutubePath(
version: String,
context: Context
) = context.getExternalFilesDir("stock_youtube")!!.path + "/$version"
fun getVancedMusicPath(
version: String,
variant: String,

View File

@ -4,4 +4,6 @@ abstract class AppInstaller {
abstract fun install(appVersions: List<String>?)
abstract fun installRoot(appVersions: List<String>?)
}

View File

@ -3,7 +3,7 @@ package com.vanced.manager.core.installer.impl
import android.content.Context
import com.vanced.manager.core.downloader.util.getMicrogPath
import com.vanced.manager.core.installer.base.AppInstaller
import com.vanced.manager.core.installer.util.installApp
import com.vanced.manager.core.installer.util.PM
import java.io.File
class MicrogInstaller(
@ -15,7 +15,11 @@ class MicrogInstaller(
) {
val musicApk = File(getMicrogPath(context) + "/microg.apk")
installApp(musicApk, context)
PM.installApp(musicApk, context)
}
override fun installRoot(appVersions: List<String>?) {
throw IllegalAccessException("Vanced microG does not have a root installer")
}
}

View File

@ -3,10 +3,9 @@ package com.vanced.manager.core.installer.impl
import android.content.Context
import com.vanced.manager.core.downloader.util.getVancedMusicPath
import com.vanced.manager.core.installer.base.AppInstaller
import com.vanced.manager.core.installer.util.installApp
import com.vanced.manager.core.installer.util.PM
import com.vanced.manager.core.preferences.holder.managerVariantPref
import com.vanced.manager.core.preferences.holder.musicVersionPref
import com.vanced.manager.core.preferences.holder.vancedVersionPref
import com.vanced.manager.core.util.getLatestOrProvidedAppVersion
import java.io.File
@ -21,7 +20,11 @@ class MusicInstaller(
val musicApk = File(getVancedMusicPath(absoluteVersion, managerVariantPref, context) + "/music.apk")
installApp(musicApk, context)
PM.installApp(musicApk, context)
}
override fun installRoot(appVersions: List<String>?) {
}
}

View File

@ -1,11 +1,13 @@
package com.vanced.manager.core.installer.impl
import android.content.Context
import android.content.Intent
import com.vanced.manager.core.downloader.util.getStockYoutubePath
import com.vanced.manager.core.downloader.util.getVancedYoutubePath
import com.vanced.manager.core.installer.base.AppInstaller
import com.vanced.manager.core.installer.util.installSplitApp
import com.vanced.manager.core.preferences.holder.managerVariantPref
import com.vanced.manager.core.installer.util.*
import com.vanced.manager.core.preferences.holder.vancedVersionPref
import com.vanced.manager.core.util.Message
import com.vanced.manager.core.util.getLatestOrProvidedAppVersion
import java.io.File
@ -18,12 +20,14 @@ class VancedInstaller(
) {
val absoluteVersion = getLatestOrProvidedAppVersion(vancedVersionPref, appVersions)
val apks = File(getVancedYoutubePath(absoluteVersion, managerVariantPref, context))
.listFiles { file ->
file.extension == "apk"
}
val apks = File(getVancedYoutubePath(absoluteVersion, "nonroot", context))
.listFiles()
PM.installSplitApp(apks!!, context)
}
override fun installRoot(appVersions: List<String>?) {
installSplitApp(apks!!, context)
}
}

View File

@ -26,7 +26,7 @@ class AppInstallService : Service() {
sendBroadcast(Intent().apply {
action = APP_INSTALL_ACTION
putExtra(EXTRA_INSTALL_STATUS, extraStatus)
putExtra(EXTRA_INSTALL_EXTRA, extraStatusMessage)
putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage)
})
}
}
@ -40,7 +40,7 @@ class AppInstallService : Service() {
const val APP_INSTALL_ACTION = "APP_INSTALL_ACTION"
const val EXTRA_INSTALL_STATUS = "EXTRA_INSTALL_STATUS"
const val EXTRA_INSTALL_EXTRA = "EXTRA_INSTALL_EXTRA"
const val EXTRA_INSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE"
}
}

View File

@ -13,29 +13,32 @@ import java.io.FileInputStream
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
fun installApp(apk: File, context: Context) {
val packageInstaller = context.packageManager.packageInstaller
val session =
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
writeApkToSession(apk, session)
session.commit(context.installIntentSender)
session.close()
}
object PM {
fun installSplitApp(apks: Array<File>, context: Context) {
val packageInstaller = context.packageManager.packageInstaller
val session =
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
for (apk in apks) {
fun installApp(apk: File, context: Context) {
val packageInstaller = context.packageManager.packageInstaller
val session =
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
writeApkToSession(apk, session)
session.commit(context.installIntentSender)
session.close()
}
session.commit(context.installIntentSender)
session.close()
}
fun uninstallPackage(pkg: String, context: Context) {
val packageInstaller = context.packageManager.packageInstaller
packageInstaller.uninstall(pkg, context.uninstallIntentSender)
fun installSplitApp(apks: Array<File>, context: Context) {
val packageInstaller = context.packageManager.packageInstaller
val session =
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
for (apk in apks) {
writeApkToSession(apk, session)
}
session.commit(context.installIntentSender)
session.close()
}
fun uninstallPackage(pkg: String, context: Context) {
val packageInstaller = context.packageManager.packageInstaller
packageInstaller.uninstall(pkg, context.uninstallIntentSender)
}
}
private fun writeApkToSession(

View File

@ -0,0 +1,139 @@
package com.vanced.manager.core.installer.util
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile
import com.topjohnwu.superuser.io.SuFileOutputStream
import com.vanced.manager.core.util.errString
import com.vanced.manager.core.util.outString
import java.io.File
import java.io.IOException
object PMRoot {
fun installApp(apkPath: String): PMRootStatus<Nothing> {
val apk = File(apkPath)
val tmpApk = copyApkToTemp(apk) { error ->
return PMRootStatus.Error(PMRootStatusType.SESSION_FAILED_COPY, error)
}
val install = Shell.su("pm", "install", "-r", tmpApk.absolutePath).exec()
tmpApk.delete()
if (!install.isSuccess) {
val errString = install.errString
return PMRootStatus.Error(getEnumForInstallFailed(errString), errString)
}
return PMRootStatus.Success()
}
fun installSplitApp(apkPaths: List<String>): PMRootStatus<Nothing> {
val installCreate = Shell.su("pm", "install-create", "-r").exec()
if (!installCreate.isSuccess)
return PMRootStatus.Error(PMRootStatusType.SESSION_FAILED_CREATE, installCreate.errString)
val sessionId = installCreate.outString
if (sessionId.toIntOrNull() == null)
return PMRootStatus.Error(PMRootStatusType.SESSION_INVALID_ID, installCreate.errString)
for (apkPath in apkPaths) {
val apk = File(apkPath)
val tmpApk = copyApkToTemp(apk) { error ->
return PMRootStatus.Error(PMRootStatusType.SESSION_FAILED_COPY, error)
}
val installWrite =
Shell.su("pm", "install-write", sessionId, tmpApk.name, tmpApk.absolutePath)
.exec()
tmpApk.delete()
if (!installWrite.isSuccess)
return PMRootStatus.Error(PMRootStatusType.SESSION_FAILED_WRITE, installWrite.errString)
}
val installCommit = Shell.su("pm", "install-commit", sessionId).exec()
if (!installCommit.isSuccess) {
val errString = installCommit.errString
return PMRootStatus.Error(getEnumForInstallFailed(errString), errString)
}
return PMRootStatus.Success()
}
fun uninstallApp(pkg: String): PMRootStatus<Nothing> {
val uninstall = Shell.su("pm", "uninstall", pkg).exec()
if (!uninstall.isSuccess)
return PMRootStatus.Error(PMRootStatusType.UNINSTALL_FAILED, uninstall.errString)
return PMRootStatus.Success()
}
fun setInstallerPackage(targetPkg: String, installerPkg: String): PMRootStatus<Nothing> {
val setInstaller = Shell.su("pm", "set-installer", targetPkg, installerPkg)
.exec()
if (!setInstaller.isSuccess)
return PMRootStatus.Error(PMRootStatusType.ACTION_FAILED_SET_INSTALLER, setInstaller.errString)
return PMRootStatus.Success()
}
fun forceStopApp(pkg: String): PMRootStatus<Nothing> {
val stopApp = Shell.su("am", "force-stop", pkg).exec()
if (!stopApp.isSuccess)
return PMRootStatus.Error(PMRootStatusType.ACTION_FAILED_FORCE_STOP_APP, stopApp.errString)
return PMRootStatus.Success()
}
fun getPackageDir(pkg: String): PMRootStatus<String> {
val delimeter = "path: "
val dumpsys = Shell.su("dumpsys", "package", pkg, "|", "grep", delimeter).exec()
if (!dumpsys.isSuccess)
return PMRootStatus.Error(PMRootStatusType.ACTION_FAILED_GET_PACKAGE_DIR, dumpsys.errString)
return PMRootStatus.Success(dumpsys.outString.removePrefix(delimeter))
}
}
private inline fun copyApkToTemp(
apk: File,
onError: (String) -> Unit
): SuFile {
val tmpPath = "/data/local/tmp/${apk.name}"
val tmpApk = SuFile(tmpPath).apply {
createNewFile()
}
try {
SuFileOutputStream.open(tmpApk).use {
it.write(apk.readBytes())
it.flush()
}
} catch (e: IOException) {
onError(e.stackTraceToString())
}
return tmpApk
}
private fun getEnumForInstallFailed(outString: String) =
when {
outString.contains("INSTALL_FAILED_ABORTED") -> PMRootStatusType.INSTALL_FAILED_ABORTED
outString.contains("INSTALL_FAILED_ALREADY_EXISTS") -> PMRootStatusType.INSTALL_FAILED_ALREADY_EXISTS
outString.contains("INSTALL_FAILED_CPU_ABI_INCOMPATIBLE") -> PMRootStatusType.INSTALL_FAILED_CPU_ABI_INCOMPATIBLE
outString.contains("INSTALL_FAILED_INSUFFICIENT_STORAGE") -> PMRootStatusType.INSTALL_FAILED_INSUFFICIENT_STORAGE
outString.contains("INSTALL_FAILED_INVALID_APK") -> PMRootStatusType.INSTALL_FAILED_INVALID_APK
outString.contains("INSTALL_FAILED_VERSION_DOWNGRADE") -> PMRootStatusType.INSTALL_FAILED_VERSION_DOWNGRADE
outString.contains("INSTALL_PARSE_FAILED_NO_CERTIFICATES") -> PMRootStatusType.INSTALL_FAILED_PARSE_NO_CERTIFICATES
else -> PMRootStatusType.INSTALL_FAILED_UNKNOWN
}

View File

@ -0,0 +1,72 @@
package com.vanced.manager.core.installer.util
enum class PMRootStatusType {
ACTION_FAILED_SET_INSTALLER,
ACTION_FAILED_GET_PACKAGE_DIR,
ACTION_FAILED_FORCE_STOP_APP,
INSTALL_SUCCESSFUL,
INSTALL_FAILED_ABORTED,
INSTALL_FAILED_ALREADY_EXISTS,
INSTALL_FAILED_CPU_ABI_INCOMPATIBLE,
INSTALL_FAILED_INSUFFICIENT_STORAGE,
INSTALL_FAILED_INVALID_APK,
INSTALL_FAILED_VERSION_DOWNGRADE,
INSTALL_FAILED_PARSE_NO_CERTIFICATES,
INSTALL_FAILED_UNKNOWN,
LINK_FAILED_UNMOUNT,
LINK_FAILED_MOUNT,
PATCH_FAILED_COPY,
PATCH_FAILED_CHMOD,
PATCH_FAILED_CHOWN,
PATCH_FAILED_CHCON,
PATCH_FAILED_DESTROY,
SESSION_FAILED_CREATE,
SESSION_FAILED_WRITE,
SESSION_FAILED_COPY,
SESSION_INVALID_ID,
SCRIPT_FAILED_SETUP_POST_FS,
SCRIPT_FAILED_SETUP_SERVICE_D,
SCRIPT_FAILED_DESTROY_POST_FS,
SCRIPT_FAILED_DESTROY_SERVICE_D,
UNINSTALL_SUCCESSFUL,
UNINSTALL_FAILED,
}
sealed class PMRootStatus<out V> {
data class Success<out V>(val value: V? = null) : PMRootStatus<V>()
data class Error(val error: PMRootStatusType, val message: String) : PMRootStatus<Nothing>()
}
inline fun <V, S : PMRootStatus<V>> S.exec(
onError: (PMRootStatusType, String) -> Unit,
onSuccess: () -> Unit
) {
when (this) {
is PMRootStatus.Success<*> -> {
onSuccess()
}
is PMRootStatus.Error -> {
onError(error, message)
}
}
}
inline fun <reified V, S : PMRootStatus<V>> S.execWithValue(
onError: (PMRootStatusType, String) -> Unit,
onSuccess: (value: V?) -> Unit
) {
when (this) {
is PMRootStatus.Success<*> -> {
onSuccess(value as V?)
}
is PMRootStatus.Error -> {
onError(error, message)
}
}
}

View File

@ -0,0 +1,163 @@
package com.vanced.manager.core.installer.util
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile
import com.topjohnwu.superuser.io.SuFileOutputStream
import com.vanced.manager.core.util.errString
import java.io.File
import java.io.IOException
object Patcher {
fun setupScript(
app: String,
pkg: String,
stockPath: String,
): PMRootStatus<Nothing> {
val postFsDataScriptPath = getAppPostFsScriptPath(app)
val serviceDScriptPath = getAppServiceDScriptPath(app)
val postFsDataScript = getPostFsDataScript(pkg)
val serviceDScript = getServiceDScript(getAppPatchPath(app), stockPath)
copyScriptToDestination(postFsDataScriptPath, postFsDataScript) { error ->
return PMRootStatus.Error(PMRootStatusType.SCRIPT_FAILED_SETUP_POST_FS, error)
}
copyScriptToDestination(serviceDScriptPath, serviceDScript) { error ->
return PMRootStatus.Error(PMRootStatusType.SCRIPT_FAILED_SETUP_SERVICE_D, error)
}
return PMRootStatus.Success()
}
fun movePatchToDataAdb(patchPath: String, app: String): PMRootStatus<Nothing> {
val newPatchPath = getAppPatchPath(app)
val patchApk = File(patchPath)
val newPatchApk = SuFile(newPatchPath).apply {
if (exists())
delete()
createNewFile()
}
try {
patchApk.copyTo(newPatchApk)
} catch (e: IOException) {
return PMRootStatus.Error(PMRootStatusType.PATCH_FAILED_COPY, e.stackTraceToString())
}
val chmod = Shell.su("chmod", "644", newPatchPath).exec()
if (!chmod.isSuccess)
return PMRootStatus.Error(PMRootStatusType.PATCH_FAILED_CHMOD, chmod.errString)
val chown = Shell.su("chown", "system:system", newPatchPath).exec()
if (!chmod.isSuccess)
return PMRootStatus.Error(PMRootStatusType.PATCH_FAILED_CHOWN, chown.errString)
return PMRootStatus.Success()
}
fun chconPatch(app: String): PMRootStatus<Nothing> {
val chcon = Shell.su("chcon u:object_r:apk_data_file:s0 ${getAppPatchPath(app)}").exec()
if (!chcon.isSuccess)
return PMRootStatus.Error(PMRootStatusType.PATCH_FAILED_CHCON, chcon.errString)
return PMRootStatus.Success()
}
fun linkPatch(app: String, pkg: String, stockPath: String): PMRootStatus<Nothing> {
val umount = Shell.su("""for i in ${'$'}(ls /data/app/ | grep $pkg | tr " "); do umount -l "/data/app/${"$"}i/base.apk"; done """).exec()
if (!umount.isSuccess)
return PMRootStatus.Error(PMRootStatusType.LINK_FAILED_UNMOUNT, umount.errString)
val mount = Shell.su("su", "-mm", "-c", """"mount -o bind ${getAppPatchPath(app)} $stockPath"""").exec()
if (!mount.isSuccess)
return PMRootStatus.Error(PMRootStatusType.LINK_FAILED_MOUNT, mount.errString)
return PMRootStatus.Success()
}
fun destroyPatch(app: String) =
cleanPatchFiles(
postFsPath = getAppPostFsScriptPath(app),
serviceDPath = getAppServiceDScriptPath(app),
patchPath = getAppPatchPath(app)
)
//TODO
fun destroyOldPatch(app: String) =
cleanPatchFiles(
postFsPath = "",
serviceDPath = "",
patchPath = ""
)
//TODO return proper error if destroying was unsuccessful
private fun cleanPatchFiles(
postFsPath: String,
serviceDPath: String,
patchPath: String,
): PMRootStatus<Nothing> {
val postFs = SuFile(postFsPath)
if (postFs.exists() && !postFs.delete())
return PMRootStatus.Error(PMRootStatusType.SCRIPT_FAILED_DESTROY_POST_FS, "")
val serviceD = SuFile(serviceDPath)
if (serviceD.exists() && !serviceD.delete())
return PMRootStatus.Error(PMRootStatusType.SCRIPT_FAILED_DESTROY_SERVICE_D, "")
val patch = SuFile(patchPath)
if (patch.exists() && !patch.delete())
return PMRootStatus.Error(PMRootStatusType.PATCH_FAILED_DESTROY, "")
return PMRootStatus.Success()
}
}
private fun getAppPatchPath(app: String) = "${getAppPatchFolderPath(app)}/base.apk"
private fun getAppPatchFolderPath(app: String) = "/data/adb/vanced_manager/$app"
private fun getAppPostFsScriptPath(app: String) = "/data/adb/post-fs-data.d/$app.sh"
private fun getAppServiceDScriptPath(app: String) = "/data/adb/service.d/$app.sh"
//TODO support dynamic sleep timer
private fun getServiceDScript(patchPath: String, stockPath: String) =
"""
#!/system/bin/sh
while [ "${'$'}(getprop sys.boot_completed | tr -d '\r')" != "1" ]; do sleep 1; done
sleep 1
chcon u:object_r:apk_data_file:s0 $patchPath
mount -o bind $patchPath $stockPath
""".trimIndent()
private fun getPostFsDataScript(pkg: String) =
"""
#!/system/bin/sh
while read line; do echo \${'$'}{line} | grep $pkg | awk '{print \${'$'}2}' | xargs umount -l; done< /proc/mounts
""".trimIndent()
private inline fun copyScriptToDestination(
scriptDestination: String,
script: String,
onError: (String) -> Unit
) {
val scriptFile = SuFile(scriptDestination)
.apply {
if (!exists())
createNewFile()
}
try {
SuFileOutputStream.open(scriptFile).use {
it.write(script.toByteArray())
it.flush()
}
val chmod = Shell.su("chmod", "744", scriptFile.absolutePath).exec()
if (!chmod.isSuccess) {
onError(chmod.errString)
}
} catch (e: IOException) {
onError(e.stackTraceToString())
}
}

View File

@ -0,0 +1,32 @@
package com.vanced.manager.core.io
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile
import com.vanced.manager.core.util.errString
import com.vanced.manager.core.util.outString
import java.io.File
class ManagerSuFile : SuFile {
sealed class SuFileResult {
data class Success<out V>(val result: V? = null) : SuFileResult()
data class Error(val error: String) : SuFileResult()
}
constructor(pathName: String) : super(pathName)
constructor(parent: String, child: String) : super(parent, child)
constructor(parent: File, child: String) : super(parent, child)
private fun cmd(input: String): SuFileResult {
val cmd = Shell.su(input.replace("@@", escapedPath)).exec()
if (!cmd.isSuccess)
return SuFileResult.Error(cmd.errString)
return SuFileResult.Success(cmd.outString)
}
fun deleteResult() = cmd("rm -f @@ || rmdir -f @@")
}

View File

@ -0,0 +1,13 @@
package com.vanced.manager.core.util
import com.topjohnwu.superuser.Shell
val Shell.Result.outString
get() = out.joinToString("\n")
val Shell.Result.errString
get() = err.joinToString("\n")
val isMagiskInstalled
get() = Shell.su("magisk", "-c").exec().isSuccess

View File

@ -41,7 +41,7 @@ class MainActivity : ComponentActivity() {
AppInstallService.APP_INSTALL_ACTION -> {
installViewModel.postInstallStatus(
pmStatus = intent.getIntExtra(AppInstallService.EXTRA_INSTALL_STATUS, -999),
extra = intent.getStringExtra(AppInstallService.EXTRA_INSTALL_EXTRA)!!,
extra = intent.getStringExtra(AppInstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!,
)
mainViewModel.fetch()
}

View File

@ -3,16 +3,30 @@ package com.vanced.manager.ui
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import com.topjohnwu.superuser.BusyBoxInstaller
import com.topjohnwu.superuser.Shell
import com.vanced.manager.BuildConfig
class SplashScreenActivity : ComponentActivity() {
init {
Shell.enableVerboseLogging = BuildConfig.DEBUG;
Shell.setDefaultBuilder(
Shell.Builder
.create()
.setInitializers(BusyBoxInstaller::class.java)
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(
Intent(this, MainActivity::class.java)
)
finish()
Shell.getShell {
startActivity(
Intent(this, MainActivity::class.java)
)
finish()
}
}
}

View File

@ -63,14 +63,22 @@ class InstallViewModel(
}
fun postInstallStatus(pmStatus: Int, extra: String) {
viewModelScope.launch(Dispatchers.IO) {
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
status = Status.Installed
log(Log.Success("Successfully installed"))
} else {
status = Status.Failure
log(Log.Error("Failed to install app", extra))
}
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
status = Status.Installed
log(Log.Success("Successfully installed"))
} else {
status = Status.Failure
log(Log.Error("Failed to install app", extra))
}
}
fun postInstallStatusRoot(pmStatus: Int, extra: String) {
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
status = Status.Installed
log(Log.Success("Successfully installed"))
} else {
status = Status.Failure
log(Log.Error("Failed to install app", extra))
}
}