diff --git a/app/src/main/assets/lib/arm64-v8a/libc++_shared.so b/app/src/main/assets/lib/arm64-v8a/libc++_shared.so deleted file mode 100644 index 42b418c..0000000 Binary files a/app/src/main/assets/lib/arm64-v8a/libc++_shared.so and /dev/null differ diff --git a/app/src/main/assets/lib/arm64-v8a/libhermes.so b/app/src/main/assets/lib/arm64-v8a/libhermes.so deleted file mode 100644 index 3ca47a9..0000000 Binary files a/app/src/main/assets/lib/arm64-v8a/libhermes.so and /dev/null differ diff --git a/app/src/main/assets/lib/armeabi-v7a/libc++_shared.so b/app/src/main/assets/lib/armeabi-v7a/libc++_shared.so deleted file mode 100644 index 906f251..0000000 Binary files a/app/src/main/assets/lib/armeabi-v7a/libc++_shared.so and /dev/null differ diff --git a/app/src/main/assets/lib/armeabi-v7a/libhermes.so b/app/src/main/assets/lib/armeabi-v7a/libhermes.so deleted file mode 100644 index 72a0e8f..0000000 Binary files a/app/src/main/assets/lib/armeabi-v7a/libhermes.so and /dev/null differ diff --git a/app/src/main/assets/lib/x86/libc++_shared.so b/app/src/main/assets/lib/x86/libc++_shared.so deleted file mode 100644 index 379ead8..0000000 Binary files a/app/src/main/assets/lib/x86/libc++_shared.so and /dev/null differ diff --git a/app/src/main/assets/lib/x86/libhermes.so b/app/src/main/assets/lib/x86/libhermes.so deleted file mode 100644 index ac63754..0000000 Binary files a/app/src/main/assets/lib/x86/libhermes.so and /dev/null differ diff --git a/app/src/main/assets/lib/x86_64/libc++_shared.so b/app/src/main/assets/lib/x86_64/libc++_shared.so deleted file mode 100644 index e37b298..0000000 Binary files a/app/src/main/assets/lib/x86_64/libc++_shared.so and /dev/null differ diff --git a/app/src/main/assets/lib/x86_64/libhermes.so b/app/src/main/assets/lib/x86_64/libhermes.so deleted file mode 100644 index f6b0320..0000000 Binary files a/app/src/main/assets/lib/x86_64/libhermes.so and /dev/null differ diff --git a/app/src/main/kotlin/com/aliucord/manager/di/ViewModelModule.kt b/app/src/main/kotlin/com/aliucord/manager/di/ViewModelModule.kt index 749b283..c344b6e 100644 --- a/app/src/main/kotlin/com/aliucord/manager/di/ViewModelModule.kt +++ b/app/src/main/kotlin/com/aliucord/manager/di/ViewModelModule.kt @@ -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) } diff --git a/app/src/main/kotlin/com/aliucord/manager/domain/manager/DownloadManager.kt b/app/src/main/kotlin/com/aliucord/manager/domain/manager/DownloadManager.kt index 899a2e0..3790940 100644 --- a/app/src/main/kotlin/com/aliucord/manager/domain/manager/DownloadManager.kt +++ b/app/src/main/kotlin/com/aliucord/manager/domain/manager/DownloadManager.kt @@ -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)) } } } diff --git a/app/src/main/kotlin/com/aliucord/manager/domain/manager/PreferencesManager.kt b/app/src/main/kotlin/com/aliucord/manager/domain/manager/PreferencesManager.kt index deb32cb..28ae2d0 100644 --- a/app/src/main/kotlin/com/aliucord/manager/domain/manager/PreferencesManager.kt +++ b/app/src/main/kotlin/com/aliucord/manager/domain/manager/PreferencesManager.kt @@ -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") } diff --git a/app/src/main/kotlin/com/aliucord/manager/domain/repository/GithubRepository.kt b/app/src/main/kotlin/com/aliucord/manager/domain/repository/GithubRepository.kt index f0fdd89..0d91371 100644 --- a/app/src/main/kotlin/com/aliucord/manager/domain/repository/GithubRepository.kt +++ b/app/src/main/kotlin/com/aliucord/manager/domain/repository/GithubRepository.kt @@ -15,4 +15,8 @@ class GithubRepository( .sortedByDescending(GithubUser::contributions) suspend fun getVersion() = service.getVersion() + + suspend fun getHermesReleases() = service.getHermesReleases() + + suspend fun getAliucordNativeReleases() = service.getAliucordNativeReleases() } diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/util/ManifestPatcher.kt b/app/src/main/kotlin/com/aliucord/manager/installer/util/ManifestPatcher.kt index d15d60f..05731ff 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/util/ManifestPatcher.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/util/ManifestPatcher.kt @@ -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() - } } }) diff --git a/app/src/main/kotlin/com/aliucord/manager/network/dto/GithubRelease.kt b/app/src/main/kotlin/com/aliucord/manager/network/dto/GithubRelease.kt new file mode 100644 index 0000000..8767c82 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/network/dto/GithubRelease.kt @@ -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, + @SerialName("tag_name") + val tagName: String +) { + @Serializable + data class GithubReleaseAssets( + val name: String, + @SerialName("browser_download_url") + val browserDownloadUrl: String + ) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/network/service/GithubService.kt b/app/src/main/kotlin/com/aliucord/manager/network/service/GithubService.kt index 961c518..5c89e89 100644 --- a/app/src/main/kotlin/com/aliucord/manager/network/service/GithubService.kt +++ b/app/src/main/kotlin/com/aliucord/manager/network/service/GithubService.kt @@ -30,12 +30,25 @@ class GithubService( httpClient.get(CONTRIBUTORS_URL).body() } + suspend fun getHermesReleases(): List = withContext(Dispatchers.IO) { + httpClient.get(HERMES_RELEASES_URL).body() + } + + suspend fun getAliucordNativeReleases(): List = 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" } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/InstallViewModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/InstallViewModel.kt index b984c92..847ec00 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/InstallViewModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/InstallViewModel.kt @@ -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() 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 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)