diff --git a/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt b/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt index 00b122fbd..d14d76285 100644 --- a/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt +++ b/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt @@ -141,4 +141,18 @@ class PathManager( fun cachedKotlinDex(version: SemVer) = patchingDownloadDir .resolve("kotlin-stdlib") .resolve("$version.dex") + + /** + * Resolve a specific path for a versioned cached Aliuvoice build + */ + fun cachedAliuvoiceAAR(version: SemVer) = patchingDownloadDir + .resolve("aliuvoice") + .resolve("$version.aar") + + /** + * Resolve a specific path for a cached Discord split APK carrying libdiscord.so. + */ + fun cachedLibdiscordApk(version: Int, abi: String) = patchingDownloadDir + .resolve("libdiscord/$version") + .resolve("config-$abi.apk") } diff --git a/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordMavenService.kt b/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordMavenService.kt index e42fac356..e1d798225 100644 --- a/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordMavenService.kt +++ b/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordMavenService.kt @@ -8,9 +8,9 @@ import io.ktor.client.request.url import io.ktor.http.* class AliucordMavenService(private val http: HttpService) { - suspend fun getAliuhookVersion(force: Boolean = false): ApiResponse { + suspend fun getMavenMeta(artifactName: String, force: Boolean): ApiResponse { val metadataResponse = http.request { - url("${BuildConfig.MAVEN_URL}/com/aliucord/Aliuhook/maven-metadata.xml") + url("${BuildConfig.MAVEN_URL}/com/aliucord/$artifactName/maven-metadata.xml") if (!force) { cacheControl(CacheControl.MaxAge(maxAgeSeconds = 60 * 30)) // 30 min @@ -23,13 +23,25 @@ class AliucordMavenService(private val http: HttpService) { val versionString = "(.+?)".toRegex() .find(it) ?.groupValues?.get(1) - ?: return ApiResponse.Error(ApiError(HttpStatusCode.OK, "No version in the aliuhook artifact metadata")) + ?: return ApiResponse.Error(ApiError(HttpStatusCode.OK, "No version in the $artifactName artifact metadata")) SemVer.parseOrNull(versionString) - ?: return ApiResponse.Error(ApiError(HttpStatusCode.OK, "Invalid latest aliuhook version!")) + ?: return ApiResponse.Error(ApiError(HttpStatusCode.OK, "Invalid latest $artifactName version!")) } } + suspend fun getAliuhookVersion(force: Boolean = false): ApiResponse = + getMavenMeta("Aliuhook", force) + + suspend fun getAliuvoiceVersion(force: Boolean = false): ApiResponse = + getMavenMeta("Aliuvoice", force) + fun getAliuhookUrl(version: SemVer): String = "${BuildConfig.MAVEN_URL}/com/aliucord/Aliuhook/$version/Aliuhook-$version.aar" + + fun getAliuvoiceUrl(version: SemVer): String = + "${BuildConfig.MAVEN_URL}/com/aliucord/Aliuvoice/$version/Aliuvoice-$version.aar" + + fun getLibraryApkUrl(version: Int, abi: String): String = + "${BuildConfig.MAVEN_URL}/com/discord/discord/$version/config-$abi.apk" } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/KotlinPatchRunner.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/KotlinPatchRunner.kt index f56f7cd6c..c3b694907 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/KotlinPatchRunner.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/KotlinPatchRunner.kt @@ -24,6 +24,8 @@ class KotlinPatchRunner( DownloadInjectorStep(options.customInjector), DownloadAliuhookStep(), DownloadKotlinStep(), + DownloadAliuvoiceStep(), + DownloadLibdiscordStep(), DownloadPatchesStep(options.customPatches), CopyDependenciesStep(), @@ -34,6 +36,7 @@ class KotlinPatchRunner( PatchCertsStep(), ReorganizeDexStep(), AddAliuhookLibsStep(), + ReplaceLibdiscordStep(), SaveMetadataStep(options), // Install diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadAliuvoiceStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadAliuvoiceStep.kt new file mode 100644 index 000000000..5957bb679 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadAliuvoiceStep.kt @@ -0,0 +1,64 @@ +package com.aliucord.manager.patcher.steps.download + +import androidx.compose.runtime.Stable +import com.aliucord.manager.R +import com.aliucord.manager.manager.PathManager +import com.aliucord.manager.network.services.AliucordMavenService +import com.aliucord.manager.network.utils.SemVer +import com.aliucord.manager.patcher.StepRunner +import com.aliucord.manager.patcher.steps.base.DownloadStep +import com.aliucord.manager.patcher.steps.base.IDexProvider +import com.aliucord.manager.patcher.steps.patch.ReorganizeDexStep +import com.aliucord.manager.patcher.steps.patch.ReplaceLibdiscordStep +import com.aliucord.manager.patcher.steps.prepare.FetchInfoStep +import com.github.diamondminer88.zip.ZipReader +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +/** + * Download a packaged AAR of the latest Aliuvoice build from the Aliucord maven. + * The AAR carries the prebuilt webrtc.dex (org.webrtc). libdiscord.so is sourced + * separately by [ReplaceLibdiscordStep] from the Discord split APK. + * Provides [ReorganizeDexStep] with the dex through the [IDexProvider] implementation. + */ +@Stable +class DownloadAliuvoiceStep : DownloadStep(), IDexProvider, KoinComponent { + private val paths: PathManager by inject() + private val maven: AliucordMavenService by inject() + + override val localizedName = R.string.patch_step_dl_aliuvoice + + override fun getRemoteUrl(container: StepRunner) = + maven.getAliuvoiceUrl(getVersion(container)) + + override fun getVersion(container: StepRunner) = + container.getStep().aliuvoiceVersion + + override fun getStoredFile(container: StepRunner) = + paths.cachedAliuvoiceAAR(getVersion(container)) + + override val dexPriority = 1 + override val dexCount get() = dexFiles?.size ?: 1 + + // R8/d8 may split across webrtc.dex + classes*.dex + private var dexFiles: List? = null + + override suspend fun execute(container: StepRunner) { + super.execute(container) + + dexFiles = ZipReader(getStoredFile(container)).use { aar -> + aar.entryNames + .filter { !it.contains('/') && it.endsWith(".dex") } + .sorted() + .map { name -> + aar.openEntry(name)?.read() ?: throw IllegalStateException("Failed to read $name from aliuvoice aar") + } + } + + if (dexFiles!!.isEmpty()) + throw IllegalStateException("No prebuilt dex files in downloaded aliuvoice build") + } + + override fun getDexFiles(container: StepRunner): List = + dexFiles ?: throw IllegalStateException("Aliuvoice dex files not loaded, download step likely failed") +} diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadLibdiscordStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadLibdiscordStep.kt new file mode 100644 index 000000000..c63c2fa44 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadLibdiscordStep.kt @@ -0,0 +1,36 @@ +package com.aliucord.manager.patcher.steps.download + +import android.os.Build +import androidx.compose.runtime.Stable +import com.aliucord.manager.R +import com.aliucord.manager.manager.PathManager +import com.aliucord.manager.network.services.AliucordMavenService +import com.aliucord.manager.patcher.StepRunner +import com.aliucord.manager.patcher.steps.base.DownloadStep +import com.aliucord.manager.patcher.steps.patch.ReplaceLibdiscordStep +import com.aliucord.manager.patcher.steps.prepare.FetchInfoStep +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +/** + * Download the Discord split APK matching the device's primary ABI, which carries the + * libdiscord.so used as the voice engine (consumed by [ReplaceLibdiscordStep]). + */ +@Stable +class DownloadLibdiscordStep : DownloadStep(), KoinComponent { + private val paths: PathManager by inject() + private val maven: AliucordMavenService by inject() + + val currentDeviceArch: String = Build.SUPPORTED_ABIS.first() + + override val localizedName = R.string.patch_step_dl_libdiscord + + override fun getVersion(container: StepRunner) = + container.getStep().libdiscordVersion + + override fun getRemoteUrl(container: StepRunner) = + maven.getLibraryApkUrl(getVersion(container), currentDeviceArch) + + override fun getStoredFile(container: StepRunner) = + paths.cachedLibdiscordApk(getVersion(container), currentDeviceArch) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/ReplaceLibdiscordStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/ReplaceLibdiscordStep.kt new file mode 100644 index 000000000..8137de51c --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/ReplaceLibdiscordStep.kt @@ -0,0 +1,45 @@ +package com.aliucord.manager.patcher.steps.patch + +import android.os.Build +import com.aliucord.manager.R +import com.aliucord.manager.patcher.StepRunner +import com.aliucord.manager.patcher.steps.StepGroup +import com.aliucord.manager.patcher.steps.base.Step +import com.aliucord.manager.patcher.steps.download.CopyDependenciesStep +import com.aliucord.manager.patcher.steps.download.DownloadLibdiscordStep +import com.github.diamondminer88.zip.* +import org.koin.core.component.KoinComponent + + +/** + * Replace Discord's bundled libdiscord.so with the one from 333.5 (333205), + * which Sunflower's Kotlin media-engine layer. + * + * Written uncompressed + unaligned here; [com.aliucord.manager.patcher.steps.install.AlignmentStep] re-aligns all .so to 16KiB. + */ +class ReplaceLibdiscordStep : Step(), KoinComponent { + override val group = StepGroup.Patch + override val localizedName = R.string.patch_step_replace_libdiscord + + override suspend fun execute(container: StepRunner) { + val currentDeviceArch = Build.SUPPORTED_ABIS.first() + val apk = container.getStep().apk + val libApk = container.getStep().getStoredFile(container) + + val libBytes = ZipReader(libApk).use { it.openEntry("lib/$currentDeviceArch/libdiscord.so")?.read() } + + if (libBytes == null) { + container.log("No libdiscord.so for arch $currentDeviceArch in split apk; leaving original engine in place") + return + } + + val apkLibPath = "lib/$currentDeviceArch/libdiscord.so" + val existing = ZipReader(apk).use { it.entryNames.toHashSet() } + + ZipWriter(apk, true).use { zip -> + container.log("Writing $apkLibPath from libdiscord split (${libBytes.size} bytes); existing=${apkLibPath in existing}") + if (apkLibPath in existing) zip.deleteEntry(apkLibPath) + zip.writeEntry(apkLibPath, libBytes, ZipCompression.NONE) + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/FetchInfoStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/FetchInfoStep.kt index 1c416ee5f..52c66ff75 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/FetchInfoStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/FetchInfoStep.kt @@ -36,6 +36,17 @@ class FetchInfoStep : Step(), KoinComponent { lateinit var aliuhookVersion: SemVer private set + /** + * Remote data about the latest Aliuvoice version available from the Aliucord maven. + */ + lateinit var aliuvoiceVersion: SemVer + private set + + /** + * Discord build whose libdiscord.so is used as the voice engine. + */ + val libdiscordVersion: Int = 333205 + override suspend fun execute(container: StepRunner) { container.log("Fetching ${AliucordGithubService.DATA_JSON_URL}") data = github.getBuildData(force = true).getOrThrow() @@ -44,5 +55,9 @@ class FetchInfoStep : Step(), KoinComponent { container.log("Obtaining latest aliuhook version") aliuhookVersion = maven.getAliuhookVersion(force = true).getOrThrow() container.log("Fetched aliuhook version: $aliuhookVersion") + + container.log("Obtaining latest aliuvoice version") + aliuvoiceVersion = maven.getAliuvoiceVersion(force = true).getOrThrow() + container.log("Fetched aliuvoice version: $aliuvoiceVersion") } } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/util/ManifestPatcher.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/util/ManifestPatcher.kt index 5a41eaa61..2b128130f 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/util/ManifestPatcher.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/util/ManifestPatcher.kt @@ -18,6 +18,8 @@ object ManifestPatcher { private const val REQUEST_LEGACY_EXTERNAL_STORAGE = "requestLegacyExternalStorage" private const val LABEL = "label" private const val PACKAGE = "package" + private const val FOREGROUND_SERVICE_TYPE = "foregroundServiceType" + private const val FOREGROUND_SERVICE_TYPES = 0xE0 private const val COMPILE_SDK_VERSION = "compileSdkVersion" private const val COMPILE_SDK_VERSION_CODENAME = "compileSdkVersionCodename" @@ -65,10 +67,14 @@ object ManifestPatcher { addHorizonOsPerm = false // Add permissions for Aliucord to work properly for Meta Quest VR headsets + // The VR headsets need the first three, the rest is for API 34, older devices ignore them for (permission in arrayOf( "horizonos.permission.HEADSET_CAMERA", "com.oculus.permission.PLAY_AUDIO_BACKGROUND", - "com.oculus.permission.RECORD_AUDIO_BACKGROUND" + "com.oculus.permission.RECORD_AUDIO_BACKGROUND", + "android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION", + "android.permission.FOREGROUND_SERVICE_MICROPHONE", + "android.permission.FOREGROUND_SERVICE_CAMERA", )) { super.child(null, "uses-permission") .attr(ANDROID_NAMESPACE, "name", android.R.attr.name, TYPE_STRING, permission) @@ -154,6 +160,30 @@ object ManifestPatcher { return when (name) { "activity" -> ReplaceAttrsVisitor(visitor, mapOf("label" to appName)) + "service" -> object : NodeVisitor(visitor) { + private var foreServiceType = true + + override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) { + if (name == FOREGROUND_SERVICE_TYPE) { + foreServiceType = false + super.attr(ns, name, resourceId, type, (value as Int) or FOREGROUND_SERVICE_TYPES) + } else { + super.attr(ns, name, resourceId, type, value) + } + } + + override fun end() { + if (foreServiceType && Build.VERSION.SDK_INT >= 29) super.attr( + ANDROID_NAMESPACE, + FOREGROUND_SERVICE_TYPE, + android.R.attr.foregroundServiceType, + TYPE_FIRST_INT, + FOREGROUND_SERVICE_TYPES + ) + super.end() + } + } + "provider" -> object : NodeVisitor(visitor) { override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) { super.attr( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9fb601a1a..e3623f681 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -158,6 +158,8 @@ Restoring from cache Downloading Discord APK Downloading Kotlin library + Downloading Aliuvoice library + Downloading voice engine library Downloading Aliucord Injector Downloading Aliuhook library Downloading smali patches @@ -166,6 +168,7 @@ Adding modern root certificates Patching app icon Adding Aliuhook library + Replacing voice engine Applying smali patches Adding .dex files to APK Store metadata