mirror of
https://github.com/YTVanced/VancedManager
synced 2024-12-03 00:07:25 +00:00
added root package manager and patcher classes
This commit is contained in:
parent
22ac84e448
commit
cbd48dce72
16 changed files with 513 additions and 46 deletions
|
@ -123,6 +123,11 @@ dependencies {
|
||||||
|
|
||||||
implementation("com.github.skydoves:orchestra-colorpicker:1.1.0")
|
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"
|
val koinVersion = "3.1.3"
|
||||||
implementation("io.insert-koin:koin-android:$koinVersion")
|
implementation("io.insert-koin:koin-android:$koinVersion")
|
||||||
implementation("io.insert-koin:koin-androidx-compose:$koinVersion")
|
implementation("io.insert-koin:koin-androidx-compose:$koinVersion")
|
||||||
|
|
|
@ -8,6 +8,11 @@ fun getVancedYoutubePath(
|
||||||
context: Context
|
context: Context
|
||||||
) = context.getExternalFilesDir("vanced_youtube")!!.path + "/$version/$variant"
|
) = context.getExternalFilesDir("vanced_youtube")!!.path + "/$version/$variant"
|
||||||
|
|
||||||
|
fun getStockYoutubePath(
|
||||||
|
version: String,
|
||||||
|
context: Context
|
||||||
|
) = context.getExternalFilesDir("stock_youtube")!!.path + "/$version"
|
||||||
|
|
||||||
fun getVancedMusicPath(
|
fun getVancedMusicPath(
|
||||||
version: String,
|
version: String,
|
||||||
variant: String,
|
variant: String,
|
||||||
|
|
|
@ -4,4 +4,6 @@ abstract class AppInstaller {
|
||||||
|
|
||||||
abstract fun install(appVersions: List<String>?)
|
abstract fun install(appVersions: List<String>?)
|
||||||
|
|
||||||
|
abstract fun installRoot(appVersions: List<String>?)
|
||||||
|
|
||||||
}
|
}
|
|
@ -3,7 +3,7 @@ package com.vanced.manager.core.installer.impl
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.vanced.manager.core.downloader.util.getMicrogPath
|
import com.vanced.manager.core.downloader.util.getMicrogPath
|
||||||
import com.vanced.manager.core.installer.base.AppInstaller
|
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
|
import java.io.File
|
||||||
|
|
||||||
class MicrogInstaller(
|
class MicrogInstaller(
|
||||||
|
@ -15,7 +15,11 @@ class MicrogInstaller(
|
||||||
) {
|
) {
|
||||||
val musicApk = File(getMicrogPath(context) + "/microg.apk")
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -3,10 +3,9 @@ package com.vanced.manager.core.installer.impl
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.vanced.manager.core.downloader.util.getVancedMusicPath
|
import com.vanced.manager.core.downloader.util.getVancedMusicPath
|
||||||
import com.vanced.manager.core.installer.base.AppInstaller
|
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.managerVariantPref
|
||||||
import com.vanced.manager.core.preferences.holder.musicVersionPref
|
import com.vanced.manager.core.preferences.holder.musicVersionPref
|
||||||
import com.vanced.manager.core.preferences.holder.vancedVersionPref
|
|
||||||
import com.vanced.manager.core.util.getLatestOrProvidedAppVersion
|
import com.vanced.manager.core.util.getLatestOrProvidedAppVersion
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
@ -21,7 +20,11 @@ class MusicInstaller(
|
||||||
|
|
||||||
val musicApk = File(getVancedMusicPath(absoluteVersion, managerVariantPref, context) + "/music.apk")
|
val musicApk = File(getVancedMusicPath(absoluteVersion, managerVariantPref, context) + "/music.apk")
|
||||||
|
|
||||||
installApp(musicApk, context)
|
PM.installApp(musicApk, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun installRoot(appVersions: List<String>?) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,11 +1,13 @@
|
||||||
package com.vanced.manager.core.installer.impl
|
package com.vanced.manager.core.installer.impl
|
||||||
|
|
||||||
import android.content.Context
|
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.downloader.util.getVancedYoutubePath
|
||||||
import com.vanced.manager.core.installer.base.AppInstaller
|
import com.vanced.manager.core.installer.base.AppInstaller
|
||||||
import com.vanced.manager.core.installer.util.installSplitApp
|
import com.vanced.manager.core.installer.util.*
|
||||||
import com.vanced.manager.core.preferences.holder.managerVariantPref
|
|
||||||
import com.vanced.manager.core.preferences.holder.vancedVersionPref
|
import com.vanced.manager.core.preferences.holder.vancedVersionPref
|
||||||
|
import com.vanced.manager.core.util.Message
|
||||||
import com.vanced.manager.core.util.getLatestOrProvidedAppVersion
|
import com.vanced.manager.core.util.getLatestOrProvidedAppVersion
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
@ -18,12 +20,14 @@ class VancedInstaller(
|
||||||
) {
|
) {
|
||||||
val absoluteVersion = getLatestOrProvidedAppVersion(vancedVersionPref, appVersions)
|
val absoluteVersion = getLatestOrProvidedAppVersion(vancedVersionPref, appVersions)
|
||||||
|
|
||||||
val apks = File(getVancedYoutubePath(absoluteVersion, managerVariantPref, context))
|
val apks = File(getVancedYoutubePath(absoluteVersion, "nonroot", context))
|
||||||
.listFiles { file ->
|
.listFiles()
|
||||||
file.extension == "apk"
|
|
||||||
}
|
PM.installSplitApp(apks!!, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun installRoot(appVersions: List<String>?) {
|
||||||
|
|
||||||
installSplitApp(apks!!, context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -26,7 +26,7 @@ class AppInstallService : Service() {
|
||||||
sendBroadcast(Intent().apply {
|
sendBroadcast(Intent().apply {
|
||||||
action = APP_INSTALL_ACTION
|
action = APP_INSTALL_ACTION
|
||||||
putExtra(EXTRA_INSTALL_STATUS, extraStatus)
|
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 APP_INSTALL_ACTION = "APP_INSTALL_ACTION"
|
||||||
|
|
||||||
const val EXTRA_INSTALL_STATUS = "EXTRA_INSTALL_STATUS"
|
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -13,29 +13,32 @@ import java.io.FileInputStream
|
||||||
|
|
||||||
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
|
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
|
||||||
|
|
||||||
fun installApp(apk: File, context: Context) {
|
object PM {
|
||||||
val packageInstaller = context.packageManager.packageInstaller
|
|
||||||
val session =
|
|
||||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
|
|
||||||
writeApkToSession(apk, session)
|
|
||||||
session.commit(context.installIntentSender)
|
|
||||||
session.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun installSplitApp(apks: Array<File>, context: Context) {
|
fun installApp(apk: File, context: Context) {
|
||||||
val packageInstaller = context.packageManager.packageInstaller
|
val packageInstaller = context.packageManager.packageInstaller
|
||||||
val session =
|
val session =
|
||||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
|
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
|
||||||
for (apk in apks) {
|
|
||||||
writeApkToSession(apk, session)
|
writeApkToSession(apk, session)
|
||||||
|
session.commit(context.installIntentSender)
|
||||||
|
session.close()
|
||||||
}
|
}
|
||||||
session.commit(context.installIntentSender)
|
|
||||||
session.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun uninstallPackage(pkg: String, context: Context) {
|
fun installSplitApp(apks: Array<File>, context: Context) {
|
||||||
val packageInstaller = context.packageManager.packageInstaller
|
val packageInstaller = context.packageManager.packageInstaller
|
||||||
packageInstaller.uninstall(pkg, context.uninstallIntentSender)
|
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(
|
private fun writeApkToSession(
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 @@")
|
||||||
|
|
||||||
|
}
|
13
app/src/main/java/com/vanced/manager/core/util/SuShell.kt
Normal file
13
app/src/main/java/com/vanced/manager/core/util/SuShell.kt
Normal 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
|
||||||
|
|
|
@ -41,7 +41,7 @@ class MainActivity : ComponentActivity() {
|
||||||
AppInstallService.APP_INSTALL_ACTION -> {
|
AppInstallService.APP_INSTALL_ACTION -> {
|
||||||
installViewModel.postInstallStatus(
|
installViewModel.postInstallStatus(
|
||||||
pmStatus = intent.getIntExtra(AppInstallService.EXTRA_INSTALL_STATUS, -999),
|
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()
|
mainViewModel.fetch()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,16 +3,30 @@ package com.vanced.manager.ui
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import com.topjohnwu.superuser.BusyBoxInstaller
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import com.vanced.manager.BuildConfig
|
||||||
|
|
||||||
class SplashScreenActivity : ComponentActivity() {
|
class SplashScreenActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
Shell.enableVerboseLogging = BuildConfig.DEBUG;
|
||||||
|
Shell.setDefaultBuilder(
|
||||||
|
Shell.Builder
|
||||||
|
.create()
|
||||||
|
.setInitializers(BusyBoxInstaller::class.java)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
startActivity(
|
Shell.getShell {
|
||||||
Intent(this, MainActivity::class.java)
|
startActivity(
|
||||||
)
|
Intent(this, MainActivity::class.java)
|
||||||
finish()
|
)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -63,14 +63,22 @@ class InstallViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun postInstallStatus(pmStatus: Int, extra: String) {
|
fun postInstallStatus(pmStatus: Int, extra: String) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||||
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
|
status = Status.Installed
|
||||||
status = Status.Installed
|
log(Log.Success("Successfully installed"))
|
||||||
log(Log.Success("Successfully installed"))
|
} else {
|
||||||
} else {
|
status = Status.Failure
|
||||||
status = Status.Failure
|
log(Log.Error("Failed to install app", extra))
|
||||||
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue