further implement root installer

This commit is contained in:
X1nto 2021-12-28 14:08:20 +04:00
parent 20e67bbb7c
commit f2107b1477
31 changed files with 493 additions and 358 deletions

View File

@ -1,83 +1,67 @@
package com.vanced.manager.core.downloader.base
import com.vanced.manager.core.downloader.util.DownloadStatus
import com.vanced.manager.core.io.writeFile
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.awaitResponse
import java.io.FileOutputStream
typealias DownloadCall = Call<ResponseBody>
abstract class AppDownloader {
data class DownloadFile(
val fileName: String,
val call: DownloadCall,
val call: Call<ResponseBody>,
)
private lateinit var call: DownloadCall
sealed class DownloadStatus {
object Success : DownloadStatus()
data class Error(val error: String, val fileName: String) : DownloadStatus()
val isSuccess
get() = this is Success
val isError
get() = this is Error
}
abstract suspend fun download(
appVersions: List<String>?,
onStatus: (DownloadStatus) -> Unit
)
onProgress: (Float) -> Unit,
onFile: (String) -> Unit
): DownloadStatus
abstract suspend fun downloadRoot(
appVersions: List<String>?,
onProgress: (Float) -> Unit,
onFile: (String) -> Unit
): DownloadStatus
abstract fun getSavedFilePath(): String
suspend fun downloadFiles(
downloadFiles: Array<DownloadFile>,
onFile: (String) -> Unit,
suspend inline fun downloadFiles(
files: Array<DownloadFile>,
onProgress: (Float) -> Unit,
onError: (error: String, fileName: String) -> Unit,
onSuccess: () -> Unit
) {
for (downloadFile in downloadFiles) {
onFile: (String) -> Unit
): DownloadStatus {
for (file in files) {
try {
this.call = downloadFile.call
onFile(file.fileName)
onFile(downloadFile.fileName)
val response = call.awaitResponse()
val response = file.call.awaitResponse()
if (response.isSuccessful) {
val body = response.body()
if (body != null) {
writeFile(body, downloadFile.fileName, onProgress)
}
response.body()?.writeFile(getSavedFilePath() + "/${file.fileName}", onProgress)
continue
}
val error = response.errorBody()?.toString()
if (error != null) {
onError(error, downloadFile.fileName)
return
return DownloadStatus.Error(error, file.fileName)
}
} catch (e: Exception) {
onError(e.stackTraceToString(), downloadFile.fileName)
return
return DownloadStatus.Error(e.stackTraceToString(), file.fileName)
}
}
onSuccess()
}
private inline fun writeFile(
body: ResponseBody,
fileName: String,
onProgress: (Float) -> Unit
) {
val inputStream = body.byteStream()
val outputStream = FileOutputStream(getSavedFilePath() + "/$fileName")
val totalBytes = body.contentLength()
val fileReader = ByteArray(4096)
var downloadedBytes = 0L
var read: Int
while (inputStream.read(fileReader).also { read = it } != -1) {
outputStream.write(fileReader, 0, read)
downloadedBytes += read
onProgress((downloadedBytes * 100 / totalBytes).toFloat())
}
inputStream.close()
outputStream.close()
return DownloadStatus.Success
}
}

View File

@ -3,7 +3,6 @@ package com.vanced.manager.core.downloader.impl
import android.content.Context
import com.vanced.manager.core.downloader.api.MicrogAPI
import com.vanced.manager.core.downloader.base.AppDownloader
import com.vanced.manager.core.downloader.util.DownloadStatus
import com.vanced.manager.core.downloader.util.getMicrogPath
import java.io.File
@ -14,33 +13,31 @@ class MicrogDownloader(
override suspend fun download(
appVersions: List<String>?,
onStatus: (DownloadStatus) -> Unit
) {
downloadFiles(
downloadFiles = arrayOf(
onProgress: (Float) -> Unit,
onFile: (String) -> Unit
): DownloadStatus {
val downloadStatus = downloadFiles(
files = arrayOf(
DownloadFile(
call = microgAPI.getFile(),
fileName = "microg.apk"
)
),
onProgress = { progress ->
onStatus(DownloadStatus.Progress(progress))
},
onFile = { fileName ->
onStatus(DownloadStatus.File(fileName))
},
onSuccess = {
onStatus(DownloadStatus.StartInstall)
},
onError = { error, fileName ->
onStatus(
DownloadStatus.Error(
displayError = "Failed to download $fileName",
stacktrace = error
)
)
}
onProgress = onProgress,
onFile = onFile
)
if (downloadStatus.isError)
return downloadStatus
return DownloadStatus.Success
}
override suspend fun downloadRoot(
appVersions: List<String>?,
onProgress: (Float) -> Unit,
onFile: (String) -> Unit
): DownloadStatus {
throw IllegalAccessException("Vanced microG does not have a root downloader")
}
override fun getSavedFilePath(): String {

View File

@ -3,8 +3,7 @@ package com.vanced.manager.core.downloader.impl
import android.content.Context
import com.vanced.manager.core.downloader.api.MusicAPI
import com.vanced.manager.core.downloader.base.AppDownloader
import com.vanced.manager.core.downloader.util.DownloadStatus
import com.vanced.manager.core.downloader.util.getVancedMusicPath
import com.vanced.manager.core.downloader.util.getVancedYoutubeMusicPath
import com.vanced.manager.core.preferences.holder.managerVariantPref
import com.vanced.manager.core.preferences.holder.musicVersionPref
import com.vanced.manager.core.util.getLatestOrProvidedAppVersion
@ -19,12 +18,13 @@ class MusicDownloader(
override suspend fun download(
appVersions: List<String>?,
onStatus: (DownloadStatus) -> Unit
) {
onProgress: (Float) -> Unit,
onFile: (String) -> Unit
): DownloadStatus {
absoluteVersion = getLatestOrProvidedAppVersion(musicVersionPref, appVersions)
downloadFiles(
downloadFiles = arrayOf(
val downloadStatus = downloadFiles(
files = arrayOf(
DownloadFile(
call = musicAPI.getFiles(
version = absoluteVersion,
@ -33,28 +33,25 @@ class MusicDownloader(
fileName = "music.apk"
)
),
onProgress = { progress ->
onStatus(DownloadStatus.Progress(progress))
},
onFile = { fileName ->
onStatus(DownloadStatus.File(fileName))
},
onSuccess = {
onStatus(DownloadStatus.StartInstall)
},
onError = { error, fileName ->
onStatus(
DownloadStatus.Error(
displayError = "Failed to download $fileName",
stacktrace = error
)
)
}
onProgress = onProgress,
onFile = onFile
)
if (downloadStatus.isError)
return downloadStatus
return DownloadStatus.Success
}
override suspend fun downloadRoot(
appVersions: List<String>?,
onProgress: (Float) -> Unit,
onFile: (String) -> Unit
): DownloadStatus {
return DownloadStatus.Success
}
override fun getSavedFilePath(): String {
val directory = File(getVancedMusicPath(absoluteVersion, managerVariantPref, context))
val directory = File(getVancedYoutubeMusicPath(absoluteVersion, managerVariantPref, context))
if (!directory.exists())
directory.mkdirs()

View File

@ -3,7 +3,6 @@ package com.vanced.manager.core.downloader.impl
import android.content.Context
import com.vanced.manager.core.downloader.api.VancedAPI
import com.vanced.manager.core.downloader.base.AppDownloader
import com.vanced.manager.core.downloader.util.DownloadStatus
import com.vanced.manager.core.downloader.util.getVancedYoutubePath
import com.vanced.manager.core.preferences.holder.managerVariantPref
import com.vanced.manager.core.preferences.holder.vancedLanguagesPref
@ -22,8 +21,9 @@ class VancedDownloader(
override suspend fun download(
appVersions: List<String>?,
onStatus: (DownloadStatus) -> Unit
) {
onProgress: (Float) -> Unit,
onFile: (String) -> Unit
): DownloadStatus {
absoluteVersion = getLatestOrProvidedAppVersion(vancedVersionPref, appVersions)
val files = arrayOf(
@ -42,26 +42,23 @@ class VancedDownloader(
)
}
downloadFiles(
downloadFiles = files,
onProgress = { progress ->
onStatus(DownloadStatus.Progress(progress))
},
onFile = { fileName ->
onStatus(DownloadStatus.File(fileName))
},
onSuccess = {
onStatus(DownloadStatus.StartInstall)
},
onError = { error, fileName ->
onStatus(
DownloadStatus.Error(
displayError = "Failed to download $fileName",
stacktrace = error
)
)
}
val downloadStatus = downloadFiles(
files = files,
onProgress = onProgress,
onFile = onFile,
)
if (downloadStatus.isError)
return downloadStatus
return DownloadStatus.Success
}
override suspend fun downloadRoot(
appVersions: List<String>?,
onProgress: (Float) -> Unit,
onFile: (String) -> Unit
): DownloadStatus {
return DownloadStatus.Success
}
override fun getSavedFilePath(): String {

View File

@ -6,19 +6,34 @@ fun getVancedYoutubePath(
version: String,
variant: String,
context: Context
) = context.getExternalFilesDir("vanced_youtube")!!.path + "/$version/$variant"
) = context.getExternalFilesDirPath("vanced_youtube") + "/$version/$variant"
fun getVancedYoutubeMusicPath(
version: String,
variant: String,
context: Context
) = context.getExternalFilesDirPath("vanced_music") + "/$version/$variant"
fun getMicrogPath(
context: Context
) = context.getExternalFilesDirPath("microg")
fun getStockYoutubePath(
version: String,
context: Context
) = context.getExternalFilesDir("stock_youtube")!!.path + "/$version"
) = context.getExternalFilesDirPath("stock_youtube") + "/$version"
fun getVancedMusicPath(
fun getStockYoutubeMusicPath(
version: String,
variant: String,
context: Context
) = context.getExternalFilesDir("vanced_music")!!.path + "/$version/$variant"
) = context.getExternalFilesDirPath("stock_youtube_music") + "/$version"
fun getMicrogPath(
context: Context
) = context.getExternalFilesDir("microg")!!.path
private fun Context.getExternalFilesDirPath(
type: String
): String {
val filesDir = getExternalFilesDir(type)!! //fuck null safety, amirite?
if (!filesDir.exists())
filesDir.mkdirs()
return filesDir.path
}

View File

@ -1,16 +0,0 @@
package com.vanced.manager.core.downloader.util
sealed class DownloadStatus {
object StartInstall : DownloadStatus()
data class File(val fileName: String) : DownloadStatus()
data class Progress(val progress: Float) : DownloadStatus()
data class Error(
val displayError: String,
val stacktrace: String,
) : DownloadStatus()
}

View File

@ -1,9 +1,11 @@
package com.vanced.manager.core.installer.base
import com.vanced.manager.core.installer.util.PMRootResult
abstract class AppInstaller {
abstract fun install(appVersions: List<String>?)
abstract fun installRoot(appVersions: List<String>?)
abstract fun installRoot(appVersions: List<String>?): PMRootResult<Nothing>
}

View File

@ -4,6 +4,7 @@ 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.PM
import com.vanced.manager.core.installer.util.PMRootResult
import java.io.File
class MicrogInstaller(
@ -18,7 +19,7 @@ class MicrogInstaller(
PM.installApp(musicApk, context)
}
override fun installRoot(appVersions: List<String>?) {
override fun installRoot(appVersions: List<String>?): PMRootResult<Nothing> {
throw IllegalAccessException("Vanced microG does not have a root installer")
}

View File

@ -1,11 +1,14 @@
package com.vanced.manager.core.installer.impl
import android.content.Context
import com.vanced.manager.core.downloader.util.getVancedMusicPath
import com.vanced.manager.core.downloader.util.getStockYoutubeMusicPath
import com.vanced.manager.core.downloader.util.getVancedYoutubeMusicPath
import com.vanced.manager.core.installer.base.AppInstaller
import com.vanced.manager.core.installer.util.PM
import com.vanced.manager.core.installer.util.*
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.VANCED_MUSIC_PACKAGE_ROOT
import com.vanced.manager.core.util.getLatestOrProvidedAppVersion
import java.io.File
@ -18,13 +21,39 @@ class MusicInstaller(
) {
val absoluteVersion = getLatestOrProvidedAppVersion(musicVersionPref, appVersions)
val musicApk = File(getVancedMusicPath(absoluteVersion, managerVariantPref, context) + "/music.apk")
val musicApk = File(getVancedYoutubeMusicPath(absoluteVersion, managerVariantPref, context) + "/music.apk")
PM.installApp(musicApk, context)
}
override fun installRoot(appVersions: List<String>?) {
override fun installRoot(appVersions: List<String>?): PMRootResult<Nothing> {
val absoluteVersion = getLatestOrProvidedAppVersion(vancedVersionPref, appVersions)
val stockPath = getStockYoutubeMusicPath(absoluteVersion, context) + "/base.apk"
val vancedPath = getVancedYoutubeMusicPath(absoluteVersion, "root", context) + "/base.apk"
val prepareStock = RootPatchHelper.prepareStock(
stockPackage = VANCED_MUSIC_PACKAGE_ROOT,
stockVersion = absoluteVersion
) {
PMRoot.installApp(stockPath)
}
if (prepareStock.isError)
return prepareStock
val patchStock = RootPatchHelper.patchStock(
patchPath = vancedPath,
stockPackage = VANCED_MUSIC_PACKAGE_ROOT,
app = APP_KEY
)
if (patchStock.isError)
return patchStock
return PMRootResult.Success()
}
companion object {
const val APP_KEY = "youtube_music_vanced"
}
}

View File

@ -1,13 +1,12 @@
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.*
import com.vanced.manager.core.preferences.holder.vancedVersionPref
import com.vanced.manager.core.util.Message
import com.vanced.manager.core.util.VANCED_YOUTUBE_PACKAGE_ROOT
import com.vanced.manager.core.util.getLatestOrProvidedAppVersion
import java.io.File
@ -26,8 +25,34 @@ class VancedInstaller(
PM.installSplitApp(apks!!, context)
}
override fun installRoot(appVersions: List<String>?) {
override fun installRoot(appVersions: List<String>?): PMRootResult<Nothing> {
val absoluteVersion = getLatestOrProvidedAppVersion(vancedVersionPref, appVersions)
val stockApks = File(getStockYoutubePath(absoluteVersion, context))
.listFiles()?.map { it.absolutePath }
val vancedBaseApk = getVancedYoutubePath(absoluteVersion, "root", context) + "/base.apk"
val prepareStock = RootPatchHelper.prepareStock(
stockPackage = VANCED_YOUTUBE_PACKAGE_ROOT,
stockVersion = absoluteVersion,
) {
PMRoot.installSplitApp(stockApks!!)
}
if (prepareStock.isError)
return prepareStock
val patchStock = RootPatchHelper.patchStock(
patchPath = vancedBaseApk,
stockPackage = VANCED_YOUTUBE_PACKAGE_ROOT,
app = APP_KEY
)
if (patchStock.isError)
return patchStock
return PMRootResult.Success()
}
companion object {
const val APP_KEY = "youtube_vanced"
}
}

View File

@ -9,7 +9,6 @@ import android.os.Build
import com.vanced.manager.core.installer.service.AppInstallService
import com.vanced.manager.core.installer.service.AppUninstallService
import java.io.File
import java.io.FileInputStream
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
@ -19,7 +18,7 @@ object PM {
val packageInstaller = context.packageManager.packageInstaller
val session =
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
writeApkToSession(apk, session)
session.writeApk(apk)
session.commit(context.installIntentSender)
session.close()
}
@ -29,7 +28,7 @@ object PM {
val session =
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
for (apk in apks) {
writeApkToSession(apk, session)
session.writeApk(apk)
}
session.commit(context.installIntentSender)
session.close()
@ -41,17 +40,13 @@ object PM {
}
}
private fun writeApkToSession(
apk: File,
session: PackageInstaller.Session
) {
val inputStream = FileInputStream(apk)
val outputStream = session.openWrite(apk.name, 0, apk.length())
inputStream.copyTo(outputStream, byteArraySize)
session.fsync(outputStream)
inputStream.close()
outputStream.flush()
outputStream.close()
private fun PackageInstaller.Session.writeApk(apk: File) {
apk.inputStream().use { inputStream ->
openWrite(apk.name, 0, apk.length()).use { outputStream ->
inputStream.copyTo(outputStream, byteArraySize)
fsync(outputStream)
}
}
}
private val intentFlags

View File

@ -10,10 +10,10 @@ import java.io.IOException
object PMRoot {
fun installApp(apkPath: String): PMRootStatus<Nothing> {
fun installApp(apkPath: String): PMRootResult<Nothing> {
val apk = File(apkPath)
val tmpApk = copyApkToTemp(apk) { error ->
return PMRootStatus.Error(PMRootStatusType.SESSION_FAILED_COPY, error)
val tmpApk = copyApkToTemp(apk).getOrElse { exception ->
return PMRootResult.Error(PMRootStatus.SESSION_FAILED_COPY, exception.stackTraceToString())
}
val install = Shell.su("pm", "install", "-r", tmpApk.absolutePath).exec()
@ -22,27 +22,27 @@ object PMRoot {
if (!install.isSuccess) {
val errString = install.errString
return PMRootStatus.Error(getEnumForInstallFailed(errString), errString)
return PMRootResult.Error(getEnumForInstallFailed(errString), errString)
}
return PMRootStatus.Success()
return PMRootResult.Success()
}
fun installSplitApp(apkPaths: List<String>): PMRootStatus<Nothing> {
fun installSplitApp(apkPaths: List<String>): PMRootResult<Nothing> {
val installCreate = Shell.su("pm", "install-create", "-r").exec()
if (!installCreate.isSuccess)
return PMRootStatus.Error(PMRootStatusType.SESSION_FAILED_CREATE, installCreate.errString)
return PMRootResult.Error(PMRootStatus.SESSION_FAILED_CREATE, installCreate.errString)
val sessionId = installCreate.outString
if (sessionId.toIntOrNull() == null)
return PMRootStatus.Error(PMRootStatusType.SESSION_INVALID_ID, installCreate.errString)
return PMRootResult.Error(PMRootStatus.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 tmpApk = copyApkToTemp(apk).getOrElse { exception ->
return PMRootResult.Error(PMRootStatus.SESSION_FAILED_COPY, exception.stackTraceToString())
}
val installWrite =
@ -52,62 +52,82 @@ object PMRoot {
tmpApk.delete()
if (!installWrite.isSuccess)
return PMRootStatus.Error(PMRootStatusType.SESSION_FAILED_WRITE, installWrite.errString)
return PMRootResult.Error(PMRootStatus.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 PMRootResult.Error(getEnumForInstallFailed(errString), errString)
}
return PMRootStatus.Success()
return PMRootResult.Success()
}
fun uninstallApp(pkg: String): PMRootStatus<Nothing> {
fun uninstallApp(pkg: String): PMRootResult<Nothing> {
val uninstall = Shell.su("pm", "uninstall", pkg).exec()
if (!uninstall.isSuccess)
return PMRootStatus.Error(PMRootStatusType.UNINSTALL_FAILED, uninstall.errString)
return PMRootResult.Error(PMRootStatus.UNINSTALL_FAILED, uninstall.errString)
return PMRootStatus.Success()
return PMRootResult.Success()
}
fun setInstallerPackage(targetPkg: String, installerPkg: String): PMRootStatus<Nothing> {
fun setInstallerPackage(targetPkg: String, installerPkg: String): PMRootResult<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 PMRootResult.Error(PMRootStatus.ACTION_FAILED_SET_INSTALLER, setInstaller.errString)
return PMRootStatus.Success()
return PMRootResult.Success()
}
fun forceStopApp(pkg: String): PMRootStatus<Nothing> {
fun forceStopApp(pkg: String): PMRootResult<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 PMRootResult.Error(PMRootStatus.ACTION_FAILED_FORCE_STOP_APP, stopApp.errString)
return PMRootStatus.Success()
return PMRootResult.Success()
}
fun getPackageDir(pkg: String): PMRootStatus<String> {
val delimeter = "path: "
val dumpsys = Shell.su("dumpsys", "package", pkg, "|", "grep", delimeter).exec()
fun getPackageVersionName(pkg: String): PMRootResult<String> {
val keyword = "versionName="
val dumpsys = Shell.su("dumpsys", "package", pkg, "|", "grep", keyword).exec()
if (!dumpsys.isSuccess)
return PMRootStatus.Error(PMRootStatusType.ACTION_FAILED_GET_PACKAGE_DIR, dumpsys.errString)
return PMRootResult.Error(PMRootStatus.ACTION_FAILED_GET_PACKAGE_VERSION_NAME,
dumpsys.errString)
return PMRootStatus.Success(dumpsys.outString.removePrefix(delimeter))
return PMRootResult.Success(dumpsys.outString.removePrefix(keyword))
}
fun getPackageVersionCode(pkg: String): PMRootResult<Long> {
val keyword = "versionCode="
val dumpsys = Shell.su("dumpsys", "package", pkg, "|", "grep", keyword).exec()
if (!dumpsys.isSuccess)
return PMRootResult.Error(PMRootStatus.ACTION_FAILED_GET_PACKAGE_VERSION_CODE,
dumpsys.errString)
return PMRootResult.Success(dumpsys.outString.removePrefix(keyword).substringAfter("minSdk")
.toLong())
}
fun getPackageDir(pkg: String): PMRootResult<String> {
val keyword = "path: "
val dumpsys = Shell.su("dumpsys", "package", pkg, "|", "grep", keyword).exec()
if (!dumpsys.isSuccess)
return PMRootResult.Error(PMRootStatus.ACTION_FAILED_GET_PACKAGE_DIR, dumpsys.errString)
return PMRootResult.Success(dumpsys.outString.removePrefix(keyword))
}
}
private inline fun copyApkToTemp(
apk: File,
onError: (String) -> Unit
): SuFile {
private fun copyApkToTemp(apk: File, ): Result<SuFile> {
val tmpPath = "/data/local/tmp/${apk.name}"
val tmpApk = SuFile(tmpPath).apply {
@ -120,20 +140,20 @@ private inline fun copyApkToTemp(
it.flush()
}
} catch (e: IOException) {
onError(e.stackTraceToString())
return Result.failure(e)
}
return tmpApk
return Result.success(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
outString.contains("INSTALL_FAILED_ABORTED") -> PMRootStatus.INSTALL_FAILED_ABORTED
outString.contains("INSTALL_FAILED_ALREADY_EXISTS") -> PMRootStatus.INSTALL_FAILED_ALREADY_EXISTS
outString.contains("INSTALL_FAILED_CPU_ABI_INCOMPATIBLE") -> PMRootStatus.INSTALL_FAILED_CPU_ABI_INCOMPATIBLE
outString.contains("INSTALL_FAILED_INSUFFICIENT_STORAGE") -> PMRootStatus.INSTALL_FAILED_INSUFFICIENT_STORAGE
outString.contains("INSTALL_FAILED_INVALID_APK") -> PMRootStatus.INSTALL_FAILED_INVALID_APK
outString.contains("INSTALL_FAILED_VERSION_DOWNGRADE") -> PMRootStatus.INSTALL_FAILED_VERSION_DOWNGRADE
outString.contains("INSTALL_PARSE_FAILED_NO_CERTIFICATES") -> PMRootStatus.INSTALL_FAILED_PARSE_NO_CERTIFICATES
else -> PMRootStatus.INSTALL_FAILED_UNKNOWN
}

View File

@ -1,8 +1,10 @@
package com.vanced.manager.core.installer.util
enum class PMRootStatusType {
enum class PMRootStatus {
ACTION_FAILED_SET_INSTALLER,
ACTION_FAILED_GET_PACKAGE_DIR,
ACTION_FAILED_GET_PACKAGE_VERSION_NAME,
ACTION_FAILED_GET_PACKAGE_VERSION_CODE,
ACTION_FAILED_FORCE_STOP_APP,
INSTALL_SUCCESSFUL,
@ -38,35 +40,20 @@ enum class PMRootStatusType {
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>()
sealed class PMRootResult<out V> {
data class Success<out V>(val value: V? = null) : PMRootResult<V>()
data class Error(val error: PMRootStatus, val message: String) : PMRootResult<Nothing>()
val isError
get() = this is Error
val isSuccess
get() = this is Success
}
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)
}
inline fun <R, T : R> PMRootResult<T>.getOrElse(onError: (PMRootResult.Error) -> R): R? {
return when (this) {
is PMRootResult.Error -> onError(this)
is PMRootResult.Success -> return this.value
}
}

View File

@ -13,27 +13,29 @@ object Patcher {
fun setupScript(
app: String,
pkg: String,
stockPackage: String,
stockPath: String,
): PMRootStatus<Nothing> {
): PMRootResult<Nothing> {
val postFsDataScriptPath = getAppPostFsScriptPath(app)
val serviceDScriptPath = getAppServiceDScriptPath(app)
val postFsDataScript = getPostFsDataScript(pkg)
val postFsDataScript = getPostFsDataScript(stockPackage)
val serviceDScript = getServiceDScript(getAppPatchPath(app), stockPath)
copyScriptToDestination(postFsDataScriptPath, postFsDataScript) { error ->
return PMRootStatus.Error(PMRootStatusType.SCRIPT_FAILED_SETUP_POST_FS, error)
}
val copyServiceDScript = copyScriptToDestination(postFsDataScript, postFsDataScriptPath)
if (copyServiceDScript.isFailure)
return PMRootResult.Error(PMRootStatus.SCRIPT_FAILED_SETUP_POST_FS,
copyServiceDScript.exceptionOrNull()!!.stackTraceToString())
copyScriptToDestination(serviceDScriptPath, serviceDScript) { error ->
return PMRootStatus.Error(PMRootStatusType.SCRIPT_FAILED_SETUP_SERVICE_D, error)
}
val copyPostFsDataScript = copyScriptToDestination(serviceDScript, serviceDScriptPath)
if (copyPostFsDataScript.isFailure)
return PMRootResult.Error(PMRootStatus.SCRIPT_FAILED_SETUP_SERVICE_D,
copyPostFsDataScript.exceptionOrNull()!!.stackTraceToString())
return PMRootStatus.Success()
return PMRootResult.Success()
}
fun movePatchToDataAdb(patchPath: String, app: String): PMRootStatus<Nothing> {
fun movePatchToDataAdb(patchPath: String, app: String): PMRootResult<Nothing> {
val newPatchPath = getAppPatchPath(app)
val patchApk = File(patchPath)
@ -47,38 +49,38 @@ object Patcher {
try {
patchApk.copyTo(newPatchApk)
} catch (e: IOException) {
return PMRootStatus.Error(PMRootStatusType.PATCH_FAILED_COPY, e.stackTraceToString())
return PMRootResult.Error(PMRootStatus.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)
return PMRootResult.Error(PMRootStatus.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 PMRootResult.Error(PMRootStatus.PATCH_FAILED_CHOWN, chown.errString)
return PMRootStatus.Success()
return PMRootResult.Success()
}
fun chconPatch(app: String): PMRootStatus<Nothing> {
fun chconPatch(app: String): PMRootResult<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 PMRootResult.Error(PMRootStatus.PATCH_FAILED_CHCON, chcon.errString)
return PMRootStatus.Success()
return PMRootResult.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()
fun linkPatch(app: String, stockPackage: String, stockPath: String): PMRootResult<Nothing> {
val umount = Shell.su("""for i in ${'$'}(ls /data/app/ | grep $stockPackage | tr " "); do umount -l "/data/app/${"$"}i/base.apk"; done """).exec()
if (!umount.isSuccess)
return PMRootStatus.Error(PMRootStatusType.LINK_FAILED_UNMOUNT, umount.errString)
return PMRootResult.Error(PMRootStatus.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 PMRootResult.Error(PMRootStatus.LINK_FAILED_MOUNT, mount.errString)
return PMRootStatus.Success()
return PMRootResult.Success()
}
fun destroyPatch(app: String) =
@ -112,42 +114,41 @@ private fun getServiceDScript(patchPath: String, stockPath: String) =
mount -o bind $patchPath $stockPath
""".trimIndent()
private fun getPostFsDataScript(pkg: String) =
private fun getPostFsDataScript(stockPackage: String) =
"""
#!/system/bin/sh
while read line; do echo \${'$'}{line} | grep $pkg | awk '{print \${'$'}2}' | xargs umount -l; done< /proc/mounts
while read line; do echo \${'$'}{line} | grep $stockPackage | awk '{print \${'$'}2}' | xargs umount -l; done< /proc/mounts
""".trimIndent()
private fun cleanPatchFiles(
postFsPath: String,
serviceDPath: String,
patchPath: String,
): PMRootStatus<Nothing> {
): PMRootResult<Nothing> {
val files = mapOf(
postFsPath to PMRootStatusType.SCRIPT_FAILED_DESTROY_POST_FS,
serviceDPath to PMRootStatusType.SCRIPT_FAILED_DESTROY_SERVICE_D,
patchPath to PMRootStatusType.PATCH_FAILED_DESTROY,
postFsPath to PMRootStatus.SCRIPT_FAILED_DESTROY_POST_FS,
serviceDPath to PMRootStatus.SCRIPT_FAILED_DESTROY_SERVICE_D,
patchPath to PMRootStatus.PATCH_FAILED_DESTROY,
)
for ((filePath, statusType) in files) {
for ((filePath, errorStatusType) in files) {
try {
with(ManagerSuFile(filePath)) {
if (exists()) delete()
}
} catch (e: SUIOException) {
return PMRootStatus.Error(statusType, e.stackTraceToString())
return PMRootResult.Error(errorStatusType, e.stackTraceToString())
}
}
return PMRootStatus.Success()
return PMRootResult.Success()
}
private inline fun copyScriptToDestination(
scriptDestination: String,
private fun copyScriptToDestination(
script: String,
onError: (String) -> Unit
) {
val scriptFile = SuFile(scriptDestination)
destination: String,
): Result<Nothing?> {
val scriptFile = SuFile(destination)
.apply {
if (!exists()) createNewFile()
}
@ -159,9 +160,11 @@ private inline fun copyScriptToDestination(
}
val chmod = Shell.su("chmod", "744", scriptFile.absolutePath).exec()
if (!chmod.isSuccess) {
onError(chmod.errString)
return Result.failure(Exception(chmod.errString))
}
} catch (e: IOException) {
onError(e.stackTraceToString())
return Result.failure(e)
}
return Result.success(null)
}

View File

@ -0,0 +1,64 @@
package com.vanced.manager.core.installer.util
object RootPatchHelper {
fun cleanPatches(app: String): PMRootResult<Nothing> {
val cleanOldPatches = Patcher.destroyOldPatch(app)
if (cleanOldPatches.isError)
return cleanOldPatches
val cleanPatches = Patcher.destroyPatch(app)
if (cleanOldPatches.isError)
return cleanPatches
return PMRootResult.Success()
}
inline fun prepareStock(
stockPackage: String,
stockVersion: String,
install: () -> PMRootResult<Nothing>
): PMRootResult<Nothing> {
val stockYoutubeVersion = PMRoot.getPackageVersionName(stockPackage)
.getOrElse { null }
if (stockYoutubeVersion != stockVersion) {
val uninstallStock = PMRoot.uninstallApp(stockPackage)
if (uninstallStock.isError)
return uninstallStock
val installStock = install()
if (installStock.isError)
return installStock
}
return PMRootResult.Success()
}
fun patchStock(
patchPath: String,
stockPackage: String,
app: String
): PMRootResult<Nothing> {
val movePatch = Patcher.movePatchToDataAdb(patchPath, app)
if (movePatch.isError)
return movePatch
val chconPatch = Patcher.chconPatch(app)
if (chconPatch.isError)
return chconPatch
val stockPackageDir = PMRoot.getPackageDir(stockPackage)
.getOrElse { error -> return error }!!
val setupScript = Patcher.setupScript(app, stockPackage, stockPackageDir)
if (setupScript is PMRootResult.Error)
return setupScript
val linkPatch = Patcher.linkPatch(app, stockPackage, stockPackageDir)
if (linkPatch is PMRootResult.Error)
return linkPatch
return PMRootResult.Success()
}
}

View File

@ -0,0 +1,36 @@
package com.vanced.manager.core.io
import okhttp3.ResponseBody
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
inline fun ResponseBody.writeFile(
filePath: String,
onProgress: (Float) -> Unit
) {
byteStream().use { inputStream ->
FileOutputStream(filePath).use { outputStream ->
val totalBytes = contentLength()
inputStream.copyTo(outputStream, 8192) { bytes ->
onProgress((bytes * 100 / totalBytes).toFloat())
}
}
}
}
inline fun InputStream.copyTo(
outputStream: OutputStream,
bufferSize: Int,
onProgress: (Long) -> Unit
) {
val buffer = ByteArray(bufferSize)
var bytesCopied: Long = 0
var bytes = read(buffer)
while (bytes >= 0) {
outputStream.write(buffer, 0, bytes)
bytesCopied += bytes
bytes = read(buffer)
onProgress(bytesCopied)
}
}

View File

@ -0,0 +1,7 @@
package com.vanced.manager.core.util
const val VANCED_YOUTUBE_PACKAGE = "com.vanced.android.youtube"
const val VANCED_YOUTUBE_PACKAGE_ROOT = "com.google.android.youtube"
const val VANCED_MUSIC_PACKAGE = "com.vanced.android.apps.youtube.music"
const val VANCED_MUSIC_PACKAGE_ROOT = "com.google.android.apps.youtube.music"

View File

@ -0,0 +1,14 @@
package com.vanced.manager.core.util
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.util.concurrent.Executor
import kotlin.coroutines.CoroutineContext
fun CoroutineContext.asExecutor(): Executor = object : Executor {
private val scope = CoroutineScope(this@asExecutor)
override fun execute(command: Runnable) {
scope.launch { command.run() }
}
}

View File

@ -1,9 +0,0 @@
package com.vanced.manager.core.util
sealed class Message {
data class Success(val message: String) : Message()
data class Warning(val message: String) : Message()
data class Error(val message: String) : Message()
}
val downloadLogs = mutableListOf<Message>()

View File

@ -1,37 +0,0 @@
package com.vanced.manager.core.util
import android.util.Log
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
data class LogContent(
val body: AnnotatedString,
)
val logs = mutableListOf<LogContent>()
fun log(tag: String, message: String) {
Log.i(tag, message)
logs.add(
LogContent(
body = buildAnnotatedString {
withStyle(
SpanStyle(
color = Color(0xFF2E73FF),
fontWeight = FontWeight.Bold
)
) {
append("$tag:")
}
append("")
withStyle(SpanStyle(color = Color.Magenta)) {
append(message)
}
}
)
)
}

View File

@ -1,6 +1,8 @@
package com.vanced.manager.core.util
import com.topjohnwu.superuser.Shell
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
val Shell.Result.outString
get() = out.joinToString("\n")
@ -9,5 +11,10 @@ val Shell.Result.errString
get() = err.joinToString("\n")
val isMagiskInstalled
get() = Shell.su("magisk", "-c").exec().isSuccess
get() = Shell.rootAccess() && Shell.su("magisk", "-c").exec().isSuccess
suspend fun Shell.Job.await() =
suspendCoroutine<Shell.Result> { continuation ->
submit(/*continuation.context.asExecutor(),*/ continuation::resume)
}

View File

@ -1,3 +0,0 @@
package com.vanced.manager.core.util
const val VANCED_PACKAGE = ""

View File

@ -16,7 +16,7 @@ import com.vanced.manager.ui.component.topappbar.ManagerTopAppBar
import com.vanced.manager.ui.resources.managerString
import com.vanced.manager.ui.util.Screen
import com.vanced.manager.ui.widget.layout.managerCategory
import com.vanced.manager.ui.widget.screens.settings.*
import com.vanced.manager.ui.widget.settings.*
@ExperimentalMaterial3Api
@Composable

View File

@ -7,13 +7,15 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vanced.manager.core.downloader.base.AppDownloader
import com.vanced.manager.core.downloader.impl.MicrogDownloader
import com.vanced.manager.core.downloader.impl.MusicDownloader
import com.vanced.manager.core.downloader.impl.VancedDownloader
import com.vanced.manager.core.downloader.util.DownloadStatus
import com.vanced.manager.core.installer.impl.MicrogInstaller
import com.vanced.manager.core.installer.impl.MusicInstaller
import com.vanced.manager.core.installer.impl.VancedInstaller
import com.vanced.manager.core.installer.util.PMRootResult
import com.vanced.manager.core.preferences.holder.managerVariantPref
import com.vanced.manager.network.util.MICROG_NAME
import com.vanced.manager.network.util.MUSIC_NAME
import com.vanced.manager.network.util.VANCED_NAME
@ -30,6 +32,9 @@ class InstallViewModel(
private val microgInstaller: MicrogInstaller,
) : ViewModel() {
private val isRoot
get() = managerVariantPref == "root"
sealed class Log {
data class Info(val infoText: String) : Log()
data class Success(val successText: String) : Log()
@ -72,16 +77,6 @@ class InstallViewModel(
}
}
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))
}
}
fun clear() {
logs.clear()
status = Status.Idle
@ -93,21 +88,29 @@ class InstallViewModel(
) {
val downloader = getDownloader(appName)
downloader.download(appVersions) { downloadStatus ->
when (downloadStatus) {
is DownloadStatus.File -> log(Log.Info("Downloading ${downloadStatus.fileName}"))
is DownloadStatus.Error -> log(
Log.Error(
displayText = downloadStatus.displayError,
stacktrace = downloadStatus.stacktrace
)
)
is DownloadStatus.Progress -> status =
Status.Progress(downloadStatus.progress / 100)
is DownloadStatus.StartInstall -> {
log(Log.Success("Successfully downloaded $appName"))
installApp(appName, appVersions)
}
val onProgress: (Float) -> Unit = { progress ->
status = Status.Progress(progress / 100)
}
val onFile: (String) -> Unit = { file ->
log(Log.Info("Downloading $file"))
}
val download =
if (isRoot)
downloader.downloadRoot(appVersions, onProgress, onFile)
else
downloader.download(appVersions, onProgress, onFile)
when (download) {
is AppDownloader.DownloadStatus.Success -> {
log(Log.Success("Successfully downloaded $appName"))
installApp(appName, appVersions)
}
is AppDownloader.DownloadStatus.Error -> {
log(Log.Error(
displayText = "Failed to download ${download.fileName}",
stacktrace = download.error
))
}
}
}
@ -120,7 +123,20 @@ class InstallViewModel(
status = Status.Installing
installer.install(appVersions)
if (isRoot) {
when (val installStatus = installer.installRoot(appVersions)) {
is PMRootResult.Success -> {
status = Status.Installed
log(Log.Success("Successfully installed"))
}
is PMRootResult.Error -> {
status = Status.Failure
log(Log.Error("Failed to install app", installStatus.message))
}
}
} else {
installer.install(appVersions)
}
}
private fun getDownloader(

View File

@ -10,7 +10,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.vanced.manager.core.installer.util.uninstallPackage
import com.vanced.manager.core.installer.util.PM
import com.vanced.manager.core.preferences.holder.managerVariantPref
import com.vanced.manager.domain.model.App
import com.vanced.manager.network.util.MICROG_NAME
@ -80,7 +80,7 @@ class MainViewModel(
fun uninstallApp(
appPackage: String,
) {
uninstallPackage(appPackage, app)
PM.uninstallPackage(appPackage, app)
}
private suspend fun fetchData(

View File

@ -1,4 +1,4 @@
package com.vanced.manager.ui.widget.screens.settings
package com.vanced.manager.ui.widget.settings
import androidx.compose.runtime.Composable

View File

@ -1,4 +1,4 @@
package com.vanced.manager.ui.widget.screens.settings
package com.vanced.manager.ui.widget.settings
import androidx.compose.runtime.Composable
import com.vanced.manager.R

View File

@ -1,4 +1,4 @@
package com.vanced.manager.ui.widget.screens.settings
package com.vanced.manager.ui.widget.settings
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource

View File

@ -1,9 +1,10 @@
package com.vanced.manager.ui.widget.screens.settings
package com.vanced.manager.ui.widget.settings
import androidx.compose.runtime.*
import com.vanced.manager.R
import com.vanced.manager.core.preferences.RadioButtonPreference
import com.vanced.manager.core.preferences.holder.managerVariantPref
import com.vanced.manager.core.util.isMagiskInstalled
import com.vanced.manager.ui.component.preference.SingleSelectDialogPreference
import com.vanced.manager.ui.resources.managerString
@ -36,6 +37,9 @@ fun SettingsManagerVariantItem() {
selectedKey = managerVariantPref
},
onItemClick = {
if (it == "root" && !isMagiskInstalled)
return@SingleSelectDialogPreference
selectedKey = it
},
onSave = {

View File

@ -1,4 +1,4 @@
package com.vanced.manager.ui.widget.screens.settings
package com.vanced.manager.ui.widget.settings
import androidx.compose.runtime.Composable
import com.vanced.manager.core.preferences.managerBooleanPreference

View File

@ -1,4 +1,4 @@
package com.vanced.manager.ui.widget.screens.settings
package com.vanced.manager.ui.widget.settings
import androidx.compose.runtime.*
import com.vanced.manager.R