Various fixes and improvements (#19)

This commit is contained in:
TymanWasTaken 2022-09-23 15:00:09 -06:00 • committed by GitHub
parent d5a785190e
commit 5a9a22962f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 259 additions and 56 deletions

View file

@ -10,6 +10,6 @@ val viewModelModule = module {
viewModelOf(::HomeViewModel)
viewModelOf(::PluginsViewModel)
viewModelOf(::AboutViewModel)
viewModel { params -> InstallViewModel(get(), get(), get(), params.get())}
viewModel { params -> InstallViewModel(get(), get(), get(), get(), params.get())}
viewModelOf(::SettingsViewModel)
}

View file

@ -5,6 +5,7 @@ import android.content.*
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import java.io.File
import java.net.URL
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@ -21,7 +22,7 @@ class DownloadManager(
return download("$BACKEND_HOST/download/discord?v=$version&split=$split", "$split-$version.apk")
}
private suspend fun download(url: String, fileName: String): File {
suspend fun download(url: String, fileName: String): File {
return suspendCoroutine { continuation ->
val receiver = object : BroadcastReceiver() {
var downloadId = 0L
@ -31,7 +32,7 @@ class DownloadManager(
if (downloadId == id) {
context.unregisterReceiver(this)
continuation.resume(context.filesDir)
continuation.resume(context.externalCacheDir!!.resolve(fileName))
}
}
}

View file

@ -15,5 +15,5 @@ class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager
var dexLocation by stringPreference("dex_location", "$aliucordDir/Injector.dex")
var appName by stringPreference("app_name", "Aliucord")
var packageName by stringPreference("package_name", "com.aliucord")
var version by stringPreference("version", "124206")
var version by stringPreference("version", "146108")
}

View file

@ -15,4 +15,8 @@ class GithubRepository(
.sortedByDescending(GithubUser::contributions)
suspend fun getVersion() = service.getVersion()
suspend fun getHermesReleases() = service.getHermesReleases()
suspend fun getAliucordNativeReleases() = service.getAliucordNativeReleases()
}

View file

@ -5,14 +5,17 @@
package com.aliucord.manager.installer.util
import android.Manifest
import pxb.android.axml.*
object ManifestPatcher {
private const val MANAGE_EXTERNAL_STORAGE = "android.permission.MANAGE_EXTERNAL_STORAGE"
private const val ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android"
private const val USES_CLEARTEXT_TRAFFIC = "usesCleartextTraffic"
private const val DEBUGGABLE = "debuggable"
private const val LABEL = "label"
private const val PACKAGE = "package"
private const val COMPILE_SDK_VERSION = "compileSdkVersion"
private const val COMPILE_SDK_VERSION_CODENAME = "compileSdkVersionCodename"
fun patchManifest(
manifestBytes: ByteArray,
@ -25,19 +28,40 @@ object ManifestPatcher {
reader.accept(object : AxmlVisitor(writer) {
override fun child(ns: String?, name: String?) =
object : ReplaceAttrsVisitor(super.child(ns, name), mapOf("package" to packageName)) {
private var addManagePerm = true
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
super.attr(ns, name, resourceId, type, value)
if (value == MANAGE_EXTERNAL_STORAGE) addManagePerm = false
}
object : ReplaceAttrsVisitor(
super.child(ns, name),
mapOf(
PACKAGE to packageName,
COMPILE_SDK_VERSION to 23,
COMPILE_SDK_VERSION_CODENAME to "6.0-2438415"
)
) {
private var addExternalStoragePerm = false
override fun child(ns: String?, name: String): NodeVisitor {
val nv = super.child(ns, name)
// Add MANAGE_EXTERNAL_STORAGE when necessary
if (addExternalStoragePerm) {
super.child(null, "uses-permission").attr(
// hardcoded id here is necessary to make it work, otherwise the permission is greyed out.
// TODO: Check if this is consistent, if not find an alternative
ANDROID_NAMESPACE, "name", 16842755, TYPE_STRING, Manifest.permission.MANAGE_EXTERNAL_STORAGE
)
addExternalStoragePerm = false
}
return when (name) {
"uses-permission" -> object : NodeVisitor(nv) {
override fun attr(ns: String?, name: String?, resourceId: Int, type: Int, obj: Any?) {
if (name != "maxSdkVersion") super.attr(ns, name, resourceId, type, obj)
// Set the add external storage permission to be added after WRITE_EXTERNAL_STORAGE (which is after read)
if (name == "name" && (obj as String) == Manifest.permission.READ_EXTERNAL_STORAGE) {
addExternalStoragePerm = true
}
}
}
"application" -> object : ReplaceAttrsVisitor(
nv,
mapOf(
@ -63,11 +87,15 @@ object ManifestPatcher {
"provider" -> object : NodeVisitor(visitor) {
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
super.attr(
/* ns = */ ns,
/* name = */ name,
/* resourceId = */ resourceId,
/* type = */ type,
/* obj = */ if (name == "authorities") (value as String).replace("com.discord", packageName) else value
ns,
name,
resourceId,
type,
if (name == "authorities") {
(value as String).replace("com.discord", packageName)
} else {
value
}
)
}
}
@ -81,16 +109,10 @@ object ManifestPatcher {
super.end()
}
}
else -> nv
}
}
override fun end() {
if (addManagePerm) super.child(null, "uses-permission").attr(
ANDROID_NAMESPACE, "name", -1, TYPE_STRING, MANAGE_EXTERNAL_STORAGE
)
super.end()
}
}
})

View file

@ -0,0 +1,20 @@
package com.aliucord.manager.network.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GithubRelease(
@SerialName("created_at")
val createdAt: String,
val assets: List<GithubReleaseAssets>,
@SerialName("tag_name")
val tagName: String
) {
@Serializable
data class GithubReleaseAssets(
val name: String,
@SerialName("browser_download_url")
val browserDownloadUrl: String
)
}

View file

@ -30,12 +30,25 @@ class GithubService(
httpClient.get(CONTRIBUTORS_URL).body()
}
suspend fun getHermesReleases(): List<GithubRelease> = withContext(Dispatchers.IO) {
httpClient.get(HERMES_RELEASES_URL).body()
}
suspend fun getAliucordNativeReleases(): List<GithubRelease> = withContext(Dispatchers.IO) {
httpClient.get(ALIUCORD_NATIVE_RELEASES_URL).body()
}
companion object {
private const val ORG = "Aliucord"
private const val REPO = "Aliucord"
private const val HERMES_REPO = "Hermes"
private const val NATIVE_REPO = "AliucordNative"
private const val COMMITS_URL = "https://api.github.com/repos/$ORG/$REPO/commits"
private const val CONTRIBUTORS_URL = "https://api.github.com/repos/$ORG/$REPO/contributors"
private const val DATA_URL = "https://raw.githubusercontent.com/$ORG/$REPO/builds/data.json"
private const val HERMES_RELEASES_URL = "https://api.github.com/repos/$ORG/$HERMES_REPO/releases"
private const val ALIUCORD_NATIVE_RELEASES_URL = "https://api.github.com/repos/$ORG/$NATIVE_REPO/releases"
}
}

View file

@ -12,27 +12,33 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aliucord.manager.domain.manager.DownloadManager
import com.aliucord.manager.domain.manager.PreferencesManager
import com.aliucord.manager.domain.repository.GithubRepository
import com.aliucord.manager.installer.service.InstallService
import com.aliucord.manager.installer.util.ManifestPatcher
import com.aliucord.manager.installer.util.Signer
import com.aliucord.manager.network.service.GithubService
import com.aliucord.manager.ui.screen.InstallData
import com.android.zipflinger.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import java.io.File
import java.time.Instant
import java.util.zip.Deflater
class InstallViewModel(
private val application: Application,
private val downloadManager: DownloadManager,
private val preferences: PreferencesManager,
private val installData: InstallData,
private val githubRepository: GithubRepository,
private val installData: InstallData
) : ViewModel() {
private val assetManager = application.assets
private val externalCacheDir = application.externalCacheDir!!
private val filesDir = application.filesDir!!
private val packageInstaller = application.packageManager.packageInstaller
private val dexRegex = Regex("classes(\\d)?\\.dex")
private val _returnToHome = MutableSharedFlow<Boolean>()
val returnToHome = _returnToHome.asSharedFlow()
@ -54,55 +60,149 @@ class InstallViewModel(
withContext(Dispatchers.IO) {
installationRunning = true
externalCacheDir.resolve("patched").runCatching { deleteRecursively() }
val arch = Build.SUPPORTED_ABIS.first()
val supportedVersion = preferences.version
val baseApk = externalCacheDir.resolve("base-${supportedVersion}.apk").also { file ->
@Suppress("DEPRECATION")
val archiveInfo = application.packageManager.getPackageArchiveInfo(file.path, 0)
if (archiveInfo?.versionCode.toString().startsWith(supportedVersion)) return@also run { log += "Using cached base APK\n" }
// Download base.apk
val baseApkFile = externalCacheDir.resolve("base-${supportedVersion}.apk").also { file ->
if (file.exists()) return@also run { log += "Using cached base APK\n" }
log += "Downloading Discord APK... "
downloadManager.downloadDiscordApk(supportedVersion)
log += "Done\n"
downloadManager.downloadDiscordApk(supportedVersion).apply {
copyTo(
externalCacheDir
.resolve("patched")
.resolve(this.name),
true
)
}.also { log += "Done\n" }
}
// Download the native libraries split
val libArch = arch.replace("-v", "_v")
val libsApk = externalCacheDir.resolve("config.$libArch-${supportedVersion}.apk").also { file ->
val libsApkFile = externalCacheDir.resolve("config.$libArch-${supportedVersion}.apk").also { file ->
if (file.exists()) return@also run { log += "Using cached libs APK\n" }
log += "Downloading libs APK... "
downloadManager.downloadSplit(supportedVersion, "config.$libArch")
log += "Done\n"
downloadManager.downloadSplit(
version = supportedVersion,
split = "config.$libArch"
).apply {
copyTo(
externalCacheDir
.resolve("patched")
.resolve(this.name),
true
)
}.also { log += "Done\n" }
}
val localeApk = externalCacheDir.resolve("config.en-${supportedVersion}.apk").also { file ->
// Download the locale split
val localeApkFile = externalCacheDir.resolve("config.en-${supportedVersion}.apk").also { file ->
if (file.exists()) return@also run { log += "Using cached locale APK\n" }
log += "Downloading locale APK... "
downloadManager.downloadSplit(supportedVersion, "config.en")
log += "Done\n"
downloadManager.downloadSplit(
version = supportedVersion,
split = "config.en"
).apply {
copyTo(
externalCacheDir
.resolve("patched")
.resolve(this.name),
true
)
}.also { log += "Done\n" }
}
val xxhdpiApk = externalCacheDir.resolve("config.xxhdpi-${supportedVersion}.apk").also { file ->
// Download the drawables split
val xxhdpiApkFile = externalCacheDir.resolve("config.xxhdpi-${supportedVersion}.apk").also { file ->
if (file.exists()) return@also run { log += "Using cached drawables APK\n" }
log += "Downloading drawables APK... "
downloadManager.downloadSplit(supportedVersion, "config.xxhdpi")
downloadManager.downloadSplit(
version = supportedVersion,
split = "config.xxhdpi"
).apply {
copyTo(
externalCacheDir
.resolve("patched")
.resolve(this.name),
true
)
}.also { log += "Done\n" }
}
// Fetch gh releases for Aliucord/Hermes
val latestHermesRelease = githubRepository
.getHermesReleases().reduce { current, new ->
if (Instant.parse(current.createdAt).isBefore(Instant.parse(new.createdAt))) new else current
}
// Download the hermes-release.aar file to replace in the apk
val hermesLibrary = externalCacheDir.resolve("hermes-release-${latestHermesRelease.tagName}.aar").also { file ->
if (file.exists()) return@also run { log += "Using cached patched hermes library\n" }
log += "Downloading cached patched hermes library... "
downloadManager.download(
url = latestHermesRelease.assets.find { it.name == "hermes-release.aar" }!!.browserDownloadUrl,
fileName = "hermes-release-${latestHermesRelease.tagName}.aar"
)
log += "Done\n"
}
val apks = arrayOf(baseApk, libsApk, localeApk, xxhdpiApk)
// Download the hermes-cppruntime-release.aar file to replace in the apk
val cppRuntimeLibrary = externalCacheDir.resolve("hermes-cppruntime-release-${latestHermesRelease.tagName}.aar").also { file ->
if (file.exists()) return@also run { log += "Using cached patched c++ runtime library\n" }
log += "Patching manifest...\n"
log += "Downloading patched c++ runtime library... "
downloadManager.download(
url = latestHermesRelease.assets.find { it.name == "hermes-cppruntime-release.aar" }!!.browserDownloadUrl,
fileName = "hermes-cppruntime-release-${latestHermesRelease.tagName}.aar"
)
log += "Done\n"
}
// Fetch the gh releases for Aliucord/AliucordNative
val latestAliucordNativeRelease = githubRepository
.getAliucordNativeReleases().reduce { current, new ->
if (Instant.parse(current.createdAt).isBefore(Instant.parse(new.createdAt))) {
new
} else {
current
}
}
// Download the Aliucord classes.dex file to add to the apk
val aliucordDexFile = externalCacheDir.resolve("classes-${latestAliucordNativeRelease.tagName}.dex").also { file ->
if (file.exists()) return@also run { log += "Using cached Aliucord dex file\n" }
log += "Downloading Aliucord dex file... "
downloadManager.download(
url = latestAliucordNativeRelease.assets.find { it.name == "classes.dex" }!!.browserDownloadUrl,
fileName = "classes-${latestAliucordNativeRelease.tagName}.dex"
).apply {
copyTo(
externalCacheDir
.resolve("patched")
.resolve(this.name),
true
)
}.also { log += "Done\n" }
}
val apks = arrayOf(baseApkFile, libsApkFile, localeApkFile, xxhdpiApkFile)
log += "Patching manifest... "
try {
val byteArray = ZipRepo(baseApk.absolutePath)
val byteArray = ZipRepo(baseApkFile.absolutePath)
.use { zip -> zip.getContent("AndroidManifest.xml") }
.array()
ZipArchive(baseApk.toPath()).use { zip ->
ZipArchive(baseApkFile.toPath()).use { zip ->
val patchedBytesArray = ManifestPatcher.patchManifest(
manifestBytes = byteArray,
packageName = preferences.packageName,
@ -135,22 +235,65 @@ class InstallViewModel(
return@withContext
}
log += "Done\n"
log += "Patching libraries...\n"
log += "Patching java classes... "
ZipArchive(libsApk.toPath()).use { zip ->
val libs = listOf("libhermes.so", "libc++_shared.so")
ZipArchive(baseApkFile.toPath()).use { baseApk ->
// List all files in the base apk, in order to increment the ordinals of all dex files
baseApk.listEntries().mapNotNull {
// Check if the file is a dex file, if not ignore it by returning null
val match = dexRegex.matchEntire(it) ?: return@mapNotNull null
// Return a Pair<Int, String> with the ordinal of the classes.dex file and the actual filename
(match.groups[1]?.value?.toInt() ?: 1) to it
}.sortedByDescending { (ordinal) ->
// Sort the dex files by ordinal (descending) so they can be renamed without any conflicts
ordinal
}.forEach { (ordinal, fileName) ->
println("Moving $fileName ($ordinal) to classes${ordinal + 1}.dex")
// Read the dex file
val inputStream = baseApk.getInputStream(fileName)
// Construct a new BytesSource with the same data, but incrementing
// the ordinal by one to make room for the Aliucord classes.dex
val incrementedDex = BytesSource(inputStream, "classes${ordinal + 1}.dex", Deflater.DEFAULT_COMPRESSION)
// Add the incremented dex file to the apk
baseApk.add(incrementedDex)
// Remove the original dex file
baseApk.delete(fileName)
}
// Add the Aliucord classes.dex file
baseApk.add(
BytesSource(aliucordDexFile.inputStream(), "classes.dex", Deflater.DEFAULT_COMPRESSION)
)
}
log += "Done\n"
libs.forEach { lib -> zip.delete("lib/$arch/$lib") }
libs.forEach { lib ->
val source = BytesSource(assetManager.open("lib/$arch/$lib"), "lib/$arch/$lib", Deflater.NO_COMPRESSION).apply {
align(4096)
log += "Patching libraries... "
ZipArchive(libsApkFile.toPath()).use { libsApk ->
// Loop over the aar files
listOf(hermesLibrary, cppRuntimeLibrary).forEach {
// Open each aar file as a zip file
ZipArchive(it.toPath()).use { aar ->
// Map the aar name to the library name
val lib = with(it.name) {
when {
startsWith("hermes-release") -> "libhermes.so"
startsWith("hermes-cppruntime-release") -> "libc++_shared.so"
else -> throw Error("Unable to map .aar name to .so name")
}
}
// Delete existing library
libsApk.delete("lib/$arch/$lib")
// Add new library
val source = BytesSource(aar.getInputStream("jni/$arch/$lib"), "lib/$arch/$lib", Deflater.NO_COMPRESSION).apply {
align(4096)
}
libsApk.add(source)
}
zip.add(source)
}
}
log += "Done\n"
log += "Signing APKs... "
apks.forEach(Signer::signApk)