587 lines
22 KiB
Kotlin
587 lines
22 KiB
Kotlin
package com.vanced.manager.utils
|
|
|
|
import android.app.PendingIntent
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.content.pm.PackageInfo
|
|
import android.content.pm.PackageInstaller
|
|
import android.content.pm.PackageManager
|
|
import android.os.Build
|
|
import com.topjohnwu.superuser.Shell
|
|
import com.topjohnwu.superuser.io.SuFile
|
|
import com.vanced.manager.core.installer.AppInstallerService
|
|
import com.vanced.manager.core.installer.AppUninstallerService
|
|
import com.vanced.manager.utils.AppUtils.log
|
|
import com.vanced.manager.utils.AppUtils.musicRootPkg
|
|
import com.vanced.manager.utils.AppUtils.playStorePkg
|
|
import com.vanced.manager.utils.AppUtils.sendCloseDialog
|
|
import com.vanced.manager.utils.AppUtils.sendFailure
|
|
import com.vanced.manager.utils.AppUtils.sendRefresh
|
|
import com.vanced.manager.utils.AppUtils.vancedRootPkg
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.launch
|
|
import java.io.File
|
|
import java.io.FileInputStream
|
|
import java.io.IOException
|
|
import java.io.InputStream
|
|
import java.util.*
|
|
|
|
object PackageHelper {
|
|
|
|
const val apkInstallPath = "/data/adb"
|
|
private const val INSTALLER_TAG = "VMInstall"
|
|
private val vancedThemes =
|
|
vanced.value?.array<String>("themes")?.value ?: listOf("black", "dark", "pink", "blue")
|
|
|
|
private fun getAppNameRoot(pkg: String): String {
|
|
return when (pkg) {
|
|
vancedRootPkg -> "vanced"
|
|
musicRootPkg -> "music"
|
|
else -> ""
|
|
}
|
|
}
|
|
|
|
fun scriptExists(scriptName: String): Boolean {
|
|
val serviceDScript = SuFile.open("$apkInstallPath/service.d/$scriptName.sh")
|
|
val postFsDataScript = SuFile.open("$apkInstallPath/post-fs-data.d/$scriptName.sh")
|
|
if (serviceDScript.exists() && postFsDataScript.exists()) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun getPkgNameRoot(app: String): String {
|
|
return when (app) {
|
|
"vanced" -> vancedRootPkg
|
|
"music" -> musicRootPkg
|
|
else -> ""
|
|
}
|
|
}
|
|
|
|
fun isPackageInstalled(packageName: String, packageManager: PackageManager): Boolean {
|
|
return try {
|
|
packageManager.getPackageInfo(packageName, 0)
|
|
true
|
|
} catch (e: PackageManager.NameNotFoundException) {
|
|
false
|
|
}
|
|
}
|
|
|
|
fun getPackageVersionName(packageName: String, packageManager: PackageManager): String {
|
|
return if (isPackageInstalled(packageName, packageManager))
|
|
packageManager.getPackageInfo(packageName, 0).versionName
|
|
else
|
|
""
|
|
}
|
|
|
|
@Suppress("DEPRECATION")
|
|
fun getPkgVerCode(pkg: String, pm: PackageManager): Int? {
|
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
|
pm.getPackageInfo(pkg, 0)?.longVersionCode?.and(0xFFFFFFFF)?.toInt()
|
|
else
|
|
pm.getPackageInfo(pkg, 0)?.versionCode
|
|
|
|
}
|
|
|
|
fun downloadStockCheck(pkg: String, versionCode: Int, context: Context): Boolean {
|
|
return try {
|
|
getPkgVerCode(pkg, context.packageManager) != versionCode
|
|
} catch (e: Exception) {
|
|
true
|
|
}
|
|
}
|
|
|
|
fun apkExist(context: Context, apk: String): Boolean {
|
|
val apkPath = File(context.getExternalFilesDir(apk.substring(0, apk.length - 4))?.path, apk)
|
|
if (apkPath.exists())
|
|
return true
|
|
|
|
return false
|
|
}
|
|
|
|
fun musicApkExists(context: Context): Boolean {
|
|
val apkPath = File(context.getExternalFilesDir("music/nonroot")?.path, "nonroot.apk")
|
|
if (apkPath.exists()) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
fun vancedInstallFilesExist(context: Context): Boolean {
|
|
val apksPath = File(context.getExternalFilesDir("vanced/nonroot")?.path.toString())
|
|
val splitFiles = mutableListOf<String>()
|
|
if (apksPath.exists()) {
|
|
val files = apksPath.listFiles()
|
|
if (files?.isNotEmpty() == true) {
|
|
for (file in files) {
|
|
when {
|
|
vancedThemes.any { file.name == "$it.apk" } && !splitFiles.contains("base") -> splitFiles.add(
|
|
"base"
|
|
)
|
|
file.name.matches(Regex("split_config\\.(..)\\.apk")) && !splitFiles.contains(
|
|
"lang"
|
|
) -> splitFiles.add("lang")
|
|
(file.name.startsWith("split_config.arm") || file.name.startsWith("split_config.x86")) && !splitFiles.contains(
|
|
"arch"
|
|
) -> splitFiles.add("arch")
|
|
}
|
|
|
|
if (splitFiles.size == 3) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun uninstallRootApk(pkg: String): Boolean {
|
|
val app = getAppNameRoot(pkg)
|
|
Shell.su("rm -rf $apkInstallPath/${app.capitalize(Locale.ROOT)}").exec()
|
|
Shell.su("rm $apkInstallPath/post-fs-data.d/$app.sh").exec()
|
|
Shell.su("rm $apkInstallPath/service.d/$app.sh").exec()
|
|
return Shell.su("pm uninstall $pkg").exec().isSuccess
|
|
}
|
|
|
|
fun uninstallApk(pkg: String, context: Context) {
|
|
val callbackIntent = Intent(context, AppUninstallerService::class.java)
|
|
callbackIntent.putExtra("pkg", pkg)
|
|
val pendingIntent = PendingIntent.getService(context, 0, callbackIntent, intentFlags)
|
|
try {
|
|
context.packageManager.packageInstaller.uninstall(pkg, pendingIntent.intentSender)
|
|
} catch (e: Exception) {
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
|
|
fun install(path: String, context: Context) {
|
|
val callbackIntent = Intent(context, AppInstallerService::class.java)
|
|
val pendingIntent = PendingIntent.getService(context, 0, callbackIntent, intentFlags)
|
|
val packageInstaller = context.packageManager.packageInstaller
|
|
val params =
|
|
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
|
val sessionId: Int
|
|
var session: PackageInstaller.Session? = null
|
|
try {
|
|
sessionId = packageInstaller.createSession(params)
|
|
session = packageInstaller.openSession(sessionId)
|
|
val inputStream: InputStream = FileInputStream(path)
|
|
val outputStream = session.openWrite("install", 0, -1)
|
|
val buffer = ByteArray(65536)
|
|
var length: Int
|
|
while (inputStream.read(buffer).also { length = it } > 0) {
|
|
outputStream.write(buffer, 0, length)
|
|
}
|
|
session.fsync(outputStream)
|
|
inputStream.close()
|
|
outputStream.close()
|
|
session.commit(pendingIntent.intentSender)
|
|
} catch (e: Exception) {
|
|
log(INSTALLER_TAG, e.stackTraceToString())
|
|
sendFailure(e.stackTraceToString(), context)
|
|
sendCloseDialog(context)
|
|
} finally {
|
|
session?.close()
|
|
}
|
|
}
|
|
|
|
private fun installRootMusic(files: List<File>, context: Context): Boolean {
|
|
files.forEach { apk ->
|
|
if (apk.name != "root.apk") {
|
|
val newPath = "/data/local/tmp/${apk.name}"
|
|
|
|
//moving apk to tmp folder in order to avoid permission denials
|
|
Shell.su("mv ${apk.path} $newPath").exec()
|
|
val command = Shell.su("pm install -r $newPath").exec()
|
|
Shell.su("rm $newPath").exec()
|
|
if (command.isSuccess) {
|
|
return true
|
|
} else {
|
|
sendFailure(command.out, context)
|
|
sendCloseDialog(context)
|
|
}
|
|
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
private fun installRootApp(
|
|
context: Context,
|
|
app: String,
|
|
appVerCode: Int?,
|
|
pkg: String,
|
|
modApkBool: (fileName: String) -> Boolean
|
|
) = CoroutineScope(Dispatchers.IO).launch {
|
|
Shell.getShell {
|
|
val apkFilesPath = context.getExternalFilesDir("$app/root")?.path
|
|
val files = File(apkFilesPath.toString()).listFiles()?.toList()
|
|
if (files != null) {
|
|
val modApk: File? = files.lastOrNull { modApkBool(it.name) }
|
|
if (modApk != null) {
|
|
if (appVerCode != null) {
|
|
if (overwriteBase(modApk, files, appVerCode, pkg, app, context)) {
|
|
setInstallerPackage(context, pkg, playStorePkg)
|
|
log(INSTALLER_TAG, "Finished installation")
|
|
sendRefresh(context)
|
|
sendCloseDialog(context)
|
|
}
|
|
} else {
|
|
sendFailure(listOf("appVerCode is null").toMutableList(), context)
|
|
sendCloseDialog(context)
|
|
}
|
|
} else {
|
|
sendFailure(listOf("ModApk_Missing").toMutableList(), context)
|
|
sendCloseDialog(context)
|
|
}
|
|
} else {
|
|
sendFailure(listOf("Files_Missing_VA").toMutableList(), context)
|
|
sendCloseDialog(context)
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fun installMusicRoot(context: Context) {
|
|
installRootApp(
|
|
context,
|
|
"music",
|
|
music.value?.int("versionCode"),
|
|
musicRootPkg
|
|
) {
|
|
it == "root.apk"
|
|
}
|
|
}
|
|
|
|
fun installVancedRoot(context: Context) {
|
|
installRootApp(
|
|
context,
|
|
"vanced",
|
|
vanced.value?.int("versionCode"),
|
|
vancedRootPkg
|
|
) { fileName ->
|
|
vancedThemes.any { fileName == "$it.apk" }
|
|
}
|
|
}
|
|
|
|
fun installSplitApkFiles(
|
|
context: Context,
|
|
appName: String
|
|
) {
|
|
val packageInstaller = context.packageManager.packageInstaller
|
|
val folder = File(context.getExternalFilesDir("$appName/nonroot")?.path.toString())
|
|
var session: PackageInstaller.Session? = null
|
|
val sessionId: Int
|
|
val sessionParams =
|
|
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
|
val callbackIntent = Intent(context, AppInstallerService::class.java)
|
|
val pendingIntent = PendingIntent.getService(context, 0, callbackIntent, intentFlags)
|
|
try {
|
|
sessionId = packageInstaller.createSession(sessionParams)
|
|
session = packageInstaller.openSession(sessionId)
|
|
folder.listFiles()?.forEach { apk ->
|
|
val inputStream = FileInputStream(apk)
|
|
val outputStream = session.openWrite(apk.name, 0, apk.length())
|
|
val buffer = ByteArray(65536)
|
|
var length: Int
|
|
while (inputStream.read(buffer).also { length = it } > 0) {
|
|
outputStream.write(buffer, 0, length)
|
|
}
|
|
session.fsync(outputStream)
|
|
inputStream.close()
|
|
outputStream.close()
|
|
}
|
|
session.commit(pendingIntent.intentSender)
|
|
} catch (e: Exception) {
|
|
log(INSTALLER_TAG, e.stackTraceToString())
|
|
sendFailure(e.stackTraceToString(), context)
|
|
sendCloseDialog(context)
|
|
} finally {
|
|
session?.close()
|
|
}
|
|
}
|
|
|
|
private fun installSplitApkFilesRoot(apkFiles: List<File>?, context: Context): Boolean {
|
|
val filenames = arrayOf("black.apk", "dark.apk", "blue.apk", "pink.apk", "hash.json")
|
|
log(INSTALLER_TAG, "installing split apk files: ${apkFiles?.map { it.name }}")
|
|
val sessionId =
|
|
Shell.su("pm install-create -r").exec().out.joinToString(" ").filter { it.isDigit() }
|
|
.toIntOrNull()
|
|
|
|
if (sessionId == null) {
|
|
sendFailure("Session ID is null", context)
|
|
sendCloseDialog(context)
|
|
return false
|
|
}
|
|
|
|
apkFiles?.filter { !filenames.contains(it.name) }?.forEach { apkFile ->
|
|
val apkName = apkFile.name
|
|
log(INSTALLER_TAG, "installing APK: $apkName")
|
|
val newPath = "/data/local/tmp/$apkName"
|
|
// Moving apk to avoid permission denials
|
|
Shell.su("mv ${apkFile.path} $newPath").exec()
|
|
val command = Shell.su("pm install-write $sessionId $apkName $newPath").exec()
|
|
Shell.su("rm $newPath").exec()
|
|
if (!command.isSuccess) {
|
|
sendFailure(command.out, context)
|
|
sendCloseDialog(context)
|
|
return false
|
|
}
|
|
}
|
|
log(INSTALLER_TAG, "committing...")
|
|
val installResult = Shell.su("pm install-commit $sessionId").exec()
|
|
if (installResult.isSuccess) {
|
|
return true
|
|
}
|
|
sendFailure(installResult.out, context)
|
|
sendCloseDialog(context)
|
|
return false
|
|
}
|
|
|
|
|
|
//overwrite stock Vanced/Music
|
|
private fun overwriteBase(
|
|
apkFile: File,
|
|
baseApkFiles: List<File>,
|
|
versionCode: Int,
|
|
pkg: String,
|
|
app: String,
|
|
context: Context
|
|
): Boolean {
|
|
if (checkVersion(versionCode, baseApkFiles, pkg, context)) {
|
|
val path = getPackageDir(context, pkg)
|
|
val apath = apkFile.absolutePath
|
|
|
|
setupFolder("$apkInstallPath/${app.capitalize(Locale.ROOT)}")
|
|
if (path != null) {
|
|
val apkFPath = "$apkInstallPath/${app.capitalize(Locale.ROOT)}/base.apk"
|
|
if (moveAPK(apath, apkFPath, pkg, context)) {
|
|
if (chConV(apkFPath, context)) {
|
|
if (setupScript(apkFPath, path, app, pkg, context)) {
|
|
return linkApp(apkFPath, pkg, path)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
private fun setupScript(
|
|
apkFPath: String,
|
|
path: String,
|
|
app: String,
|
|
pkg: String,
|
|
context: Context
|
|
): Boolean {
|
|
try {
|
|
log(INSTALLER_TAG, "Setting up script")
|
|
context.writeServiceDScript(apkFPath, path, app)
|
|
Shell.su("""echo "#!/system/bin/sh\nwhile read line; do echo \${"$"}{line} | grep $pkg | awk '{print \${'$'}2}' | xargs umount -l; done< /proc/mounts" > /data/adb/post-fs-data.d/$app.sh""")
|
|
.exec()
|
|
return Shell.su("chmod 744 /data/adb/service.d/$app.sh").exec().isSuccess
|
|
} catch (e: IOException) {
|
|
sendFailure(e.stackTraceToString(), context)
|
|
sendCloseDialog(context)
|
|
log(INSTALLER_TAG, e.stackTraceToString())
|
|
}
|
|
return false
|
|
}
|
|
|
|
private fun linkApp(apkFPath: String, pkg: String, path: String): Boolean {
|
|
log(INSTALLER_TAG, "Linking app")
|
|
Shell.su("am force-stop $pkg").exec()
|
|
Shell.su("""for i in ${'$'}(ls /data/app/ | grep $pkg | tr " "); do umount -l "/data/app/${"$"}i/base.apk"; done """)
|
|
.exec()
|
|
val response = Shell.su("""su -mm -c "mount -o bind $apkFPath $path"""").exec()
|
|
Thread.sleep(500)
|
|
Shell.su("am force-stop $pkg").exec()
|
|
return response.isSuccess
|
|
}
|
|
|
|
private fun setupFolder(apkInstallPath: String): Boolean {
|
|
return Shell.su("mkdir -p $apkInstallPath").exec().isSuccess
|
|
}
|
|
|
|
//check version and perform action based on result
|
|
private fun checkVersion(
|
|
versionCode: Int,
|
|
baseApkFiles: List<File>,
|
|
pkg: String,
|
|
context: Context
|
|
): Boolean {
|
|
log(INSTALLER_TAG, "Checking stock version")
|
|
val path = getPackageDir(context, pkg)
|
|
if (path != null) {
|
|
if (path.contains("/data/app/")) {
|
|
when (getVersionNumber(pkg, context)?.let { compareVersion(it, versionCode) }) {
|
|
1 -> return fixHigherVer(baseApkFiles, pkg, context)
|
|
-1 -> return installStock(baseApkFiles, pkg, context)
|
|
}
|
|
return true
|
|
}
|
|
return installStock(baseApkFiles, pkg, context)
|
|
}
|
|
return installStock(baseApkFiles, pkg, context)
|
|
}
|
|
|
|
private fun getPkgInfo(pkg: String, context: Context): PackageInfo? {
|
|
return try {
|
|
context.packageManager.getPackageInfo(pkg, 0)
|
|
} catch (e: Exception) {
|
|
log(INSTALLER_TAG, "Unable to get package info")
|
|
null
|
|
}
|
|
}
|
|
|
|
private fun compareVersion(pkgVerCode: Int, versionCode: Int): Int {
|
|
return when {
|
|
pkgVerCode > versionCode -> 1
|
|
pkgVerCode < versionCode -> -1
|
|
else -> 0
|
|
}
|
|
}
|
|
|
|
//uninstall current update and install base that works with patch
|
|
private fun fixHigherVer(apkFiles: List<File>, pkg: String, context: Context): Boolean {
|
|
log(INSTALLER_TAG, "Downgrading stock")
|
|
if (uninstallRootApk(pkg)) {
|
|
return if (pkg == vancedRootPkg) installSplitApkFilesRoot(
|
|
apkFiles,
|
|
context
|
|
) else installRootMusic(apkFiles, context)
|
|
}
|
|
sendFailure(listOf("Failed_Uninstall").toMutableList(), context)
|
|
sendCloseDialog(context)
|
|
return false
|
|
}
|
|
|
|
//install stock youtube matching vanced version
|
|
private fun installStock(baseApkFiles: List<File>, pkg: String, context: Context): Boolean {
|
|
log(INSTALLER_TAG, "Installing stock")
|
|
return if (pkg == vancedRootPkg) installSplitApkFilesRoot(
|
|
baseApkFiles,
|
|
context
|
|
) else installRootMusic(baseApkFiles, context)
|
|
}
|
|
|
|
//set chcon to apk_data_file
|
|
private fun chConV(apkFPath: String, context: Context): Boolean {
|
|
log(INSTALLER_TAG, "Running chcon")
|
|
val response = Shell.su("chcon u:object_r:apk_data_file:s0 $apkFPath").exec()
|
|
//val response = Shell.su("chcon -R u:object_r:system_file:s0 $path").exec()
|
|
return if (response.isSuccess) {
|
|
true
|
|
} else {
|
|
sendFailure(response.out, context)
|
|
sendCloseDialog(context)
|
|
false
|
|
}
|
|
}
|
|
|
|
//move patch to data/app
|
|
private fun moveAPK(apkFile: String, path: String, pkg: String, context: Context): Boolean {
|
|
log(INSTALLER_TAG, "Moving app")
|
|
val apkinF = SuFile.open(apkFile)
|
|
val apkoutF = SuFile.open(path)
|
|
|
|
if (apkinF.exists()) {
|
|
try {
|
|
Shell.su("am force-stop $pkg").exec()
|
|
|
|
//Shell.su("rm -r SuFile.open(path).parent")
|
|
|
|
copy(apkinF, apkoutF)
|
|
Shell.su("chmod 644 $path").exec().isSuccess
|
|
return if (Shell.su("chown system:system $path").exec().isSuccess) {
|
|
true
|
|
} else {
|
|
sendFailure(listOf("Chown_Fail").toMutableList(), context)
|
|
sendCloseDialog(context)
|
|
false
|
|
}
|
|
|
|
} catch (e: IOException) {
|
|
sendFailure(listOf("${e.message}").toMutableList(), context)
|
|
sendCloseDialog(context)
|
|
log(INSTALLER_TAG, e.stackTraceToString())
|
|
return false
|
|
}
|
|
}
|
|
sendFailure(listOf("IFile_Missing").toMutableList(), context)
|
|
sendCloseDialog(context)
|
|
return false
|
|
}
|
|
|
|
|
|
@Throws(IOException::class)
|
|
fun copy(src: File, dst: File) {
|
|
val cmd = Shell.su("mv ${src.absolutePath} ${dst.absolutePath}").exec().isSuccess
|
|
log("ZLog", cmd.toString())
|
|
}
|
|
|
|
@Suppress("DEPRECATION")
|
|
private fun getVersionNumber(pkg: String, context: Context): Int? {
|
|
try {
|
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
|
context.packageManager.getPackageInfo(vancedRootPkg, 0).longVersionCode.and(
|
|
0xFFFFFFFF
|
|
).toInt()
|
|
else
|
|
context.packageManager.getPackageInfo(vancedRootPkg, 0).versionCode
|
|
} catch (e: Exception) {
|
|
val execRes = Shell.su("dumpsys package $pkg | grep versionCode").exec()
|
|
if (execRes.isSuccess) {
|
|
val result = execRes.out
|
|
var version = 0
|
|
result
|
|
.asSequence()
|
|
.map { it.substringAfter("=") }
|
|
.map { it.substringBefore(" ") }
|
|
.filter { version < Integer.valueOf(it) }
|
|
.forEach { version = Integer.valueOf(it) }
|
|
return version
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
//get path of the installed youtube
|
|
fun getPackageDir(context: Context, pkg: String): String? {
|
|
val p = getPkgInfo(pkg, context)
|
|
return if (p != null) {
|
|
p.applicationInfo.sourceDir
|
|
} else {
|
|
val execRes = Shell.su("dumpsys package $pkg | grep codePath").exec()
|
|
if (execRes.isSuccess) {
|
|
val result = execRes.out
|
|
for (line in result) {
|
|
if (line.contains("data/app")) "${line.substringAfter("=")}/base.apk"
|
|
}
|
|
}
|
|
null
|
|
}
|
|
}
|
|
|
|
private fun setInstallerPackage(context: Context, target: String, installer: String) {
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
|
|
try {
|
|
log(INSTALLER_TAG, "Setting installer package to $installer for $target")
|
|
val installerUid = context.packageManager.getPackageUid(installer, 0)
|
|
val res =
|
|
Shell.su("""su $installerUid -c 'pm set-installer $target $installer'""").exec()
|
|
if (res.out.any { line -> line.contains("Success") }) {
|
|
log(INSTALLER_TAG, "Installer package successfully set")
|
|
return
|
|
}
|
|
log(INSTALLER_TAG, "Failed setting installer package")
|
|
} catch (e: PackageManager.NameNotFoundException) {
|
|
log(INSTALLER_TAG, "Installer package $installer not found. Skipping setting installer")
|
|
}
|
|
}
|
|
}
|