feat: Support split APKs for RNA
This commit is contained in:
parent
69153dbe2d
commit
457347167d
21 changed files with 331 additions and 129 deletions
|
|
@ -12,8 +12,7 @@ import com.aliucord.manager.BuildConfig
|
|||
import com.aliucord.manager.ui.screens.patchopts.PatchOptions
|
||||
import com.aliucord.manager.util.IS_PROBABLY_EMULATOR
|
||||
import com.aliucord.manager.util.isPlayProtectEnabled
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import java.io.IOException
|
||||
|
|
@ -149,7 +148,7 @@ class InstallLogManager(
|
|||
@Serializable
|
||||
data class InstallLogData(
|
||||
val id: String,
|
||||
val installDate: Instant,
|
||||
@Contextual val installDate: Instant,
|
||||
val installDuration: Duration,
|
||||
val installOptions: PatchOptions,
|
||||
val environmentInfo: String,
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ class PathManager(
|
|||
/**
|
||||
* The APK that is worked on during the patching process.
|
||||
*/
|
||||
fun patchedApk() = patchingWorkingDir().resolve("patched.apk")
|
||||
fun patchedApk(part: String = "") = patchingWorkingDir().resolve("patched-${part}.apk")
|
||||
|
||||
private companion object {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -27,15 +27,15 @@ data class InstallMetadata(
|
|||
/**
|
||||
* Version (commit hash) of the Aliuhook build that was injected into the APK.
|
||||
*/
|
||||
val aliuhookVersion: SemVer,
|
||||
val aliuhookVersion: SemVer?,
|
||||
|
||||
/**
|
||||
* Version of the injector build that was injected into the APK.
|
||||
*/
|
||||
val injectorVersion: SemVer,
|
||||
val injectorVersion: SemVer?,
|
||||
|
||||
/**
|
||||
* Version of the smali patches that were applied onto the APK.
|
||||
*/
|
||||
val patchesVersion: SemVer,
|
||||
val patchesVersion: SemVer?,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,10 +42,7 @@ abstract class StepRunner : KoinComponent {
|
|||
* @param completed Only match steps that have finished executing.
|
||||
*/
|
||||
inline fun <reified T : Step> getStep(completed: Boolean = true): T {
|
||||
val step = steps.asSequence()
|
||||
.filterIsInstance<T>()
|
||||
.filter { !completed || it.state.isFinished }
|
||||
.firstOrNull()
|
||||
val step = getStepOrNull<T>(completed)
|
||||
|
||||
if (step == null) {
|
||||
throw IllegalArgumentException("No completed step ${T::class.simpleName} exists in container")
|
||||
|
|
@ -54,6 +51,13 @@ abstract class StepRunner : KoinComponent {
|
|||
return step
|
||||
}
|
||||
|
||||
inline fun <reified T : Step> getStepOrNull(completed: Boolean = true): T? {
|
||||
return steps.asSequence()
|
||||
.filterIsInstance<T>()
|
||||
.filter { !completed || it.state.isFinished }
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a log entry without any associated log level.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
package com.aliucord.manager.patcher
|
||||
|
||||
import com.aliucord.manager.patcher.steps.base.Step
|
||||
import com.aliucord.manager.patcher.steps.download.*
|
||||
import com.aliucord.manager.patcher.steps.install.*
|
||||
import com.aliucord.manager.patcher.steps.patch.*
|
||||
import com.aliucord.manager.ui.screens.patchopts.PatchOptions
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
class WintryPatchRunner(options: PatchOptions) : StepRunner() {
|
||||
override val steps = persistentListOf<Step>(
|
||||
DownloadDiscordRNABaseStep(),
|
||||
DownloadDiscordRNALangStep(),
|
||||
DownloadDiscordRNAResourcesStep(),
|
||||
DownloadDiscordRNALibStep(),
|
||||
CopyDependenciesStep(),
|
||||
|
||||
PatchIconsStep(options),
|
||||
PatchManifestStep(options),
|
||||
// ReorganizeDexStep(),
|
||||
SaveMetadataStep(options),
|
||||
|
||||
AlignmentStep(),
|
||||
SigningStep(),
|
||||
InstallStep(options),
|
||||
// CleanupStep(),
|
||||
)
|
||||
}
|
||||
|
|
@ -22,6 +22,9 @@ abstract class DownloadStep : Step(), KoinComponent {
|
|||
private val context: Context by inject()
|
||||
private val downloader: KtorDownloadManager by inject()
|
||||
|
||||
// Temporary
|
||||
protected val version: Int = 289210
|
||||
|
||||
/**
|
||||
* The remote url to download
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -23,15 +23,30 @@ class CopyDependenciesStep : Step(), KoinComponent {
|
|||
private val application: Application by inject()
|
||||
|
||||
/**
|
||||
* The target APK file which can be modified during patching
|
||||
* The target APK files which can be modified during patching
|
||||
*/
|
||||
val patchedApk: File = paths.patchedApk()
|
||||
var patchedLangApk: File? = null
|
||||
var patchedLibApk: File? = null
|
||||
var patchedResApk: File? = null
|
||||
|
||||
val patchedApks
|
||||
get() = listOfNotNull(
|
||||
patchedApk,
|
||||
patchedLangApk.takeIf { it?.exists() == true },
|
||||
patchedLibApk.takeIf { it?.exists() == true },
|
||||
patchedResApk.takeIf { it?.exists() == true }
|
||||
)
|
||||
|
||||
override val group = StepGroup.Download
|
||||
override val localizedName = R.string.patch_step_copy_deps
|
||||
|
||||
override suspend fun execute(container: StepRunner) {
|
||||
val srcApk = container.getStep<DownloadDiscordStep>().targetFile
|
||||
val langApk = container.getStepOrNull<DownloadDiscordRNALangStep>()?.targetFile
|
||||
val libApk = container.getStepOrNull<DownloadDiscordRNALibStep>()?.targetFile
|
||||
val resApk = container.getStepOrNull<DownloadDiscordRNAResourcesStep>()?.targetFile
|
||||
|
||||
val dir = paths.patchingWorkingDir()
|
||||
|
||||
container.log("Clearing patched directory")
|
||||
|
|
@ -42,7 +57,7 @@ class CopyDependenciesStep : Step(), KoinComponent {
|
|||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
val storageManager = application.getSystemService<StorageManager>()!!
|
||||
val targetFileStorageId = storageManager.getUuidForPath(patchedApk)
|
||||
val fileSize = srcApk.length()
|
||||
val fileSize = srcApk.length() + (langApk?.length() ?: 0) + (libApk?.length() ?: 0) + (resApk?.length() ?: 0)
|
||||
|
||||
// We request 3.5x the size of the APK, to give space for the following:
|
||||
// 1) A copy of the APK
|
||||
|
|
@ -57,7 +72,21 @@ class CopyDependenciesStep : Step(), KoinComponent {
|
|||
}
|
||||
}
|
||||
|
||||
container.log("Copying patched apk from ${srcApk.absolutePath} to ${patchedApk.absolutePath}")
|
||||
fun copyApkSafely(src: File?, destFile: File, onCopied: (File) -> Unit) {
|
||||
src?.let {
|
||||
container.log("Copying patched apk from ${it.absolutePath} to ${destFile.absolutePath}")
|
||||
it.copyTo(destFile)
|
||||
onCopied(destFile)
|
||||
}
|
||||
}
|
||||
|
||||
// Base APK
|
||||
container.log("Copying patched base apk from ${srcApk.absolutePath} to ${patchedApk.absolutePath}")
|
||||
srcApk.copyTo(patchedApk)
|
||||
|
||||
// Optional APKs
|
||||
copyApkSafely(langApk, File(dir, "lang")) { patchedLangApk = it }
|
||||
copyApkSafely(libApk, File(dir, "lib")) { patchedLibApk = it }
|
||||
copyApkSafely(resApk, File(dir, "res")) { patchedResApk = it }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
package com.aliucord.manager.patcher.steps.download
|
||||
|
||||
import com.aliucord.manager.R
|
||||
|
||||
class DownloadDiscordRNABaseStep : DownloadDiscordStep() {
|
||||
override val targetUrl = "https://tracker.vendetta.rocks/tracker/download/$version/base"
|
||||
override val targetFile = paths.discordApkVersionCache(version).resolve("base.apk")
|
||||
override val localizedName = R.string.downloading_base_apk
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.aliucord.manager.patcher.steps.download
|
||||
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.manager.PathManager
|
||||
import com.aliucord.manager.patcher.steps.base.DownloadStep
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class DownloadDiscordRNALangStep : DownloadStep(), KoinComponent {
|
||||
private val paths: PathManager by inject()
|
||||
|
||||
override val targetUrl = "https://tracker.vendetta.rocks/tracker/download/$version/config.en"
|
||||
override val targetFile = paths.discordApkVersionCache(version).resolve("config.en.apk")
|
||||
override val localizedName = R.string.download_libs_apk
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.aliucord.manager.patcher.steps.download
|
||||
|
||||
import android.os.Build
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.manager.PathManager
|
||||
import com.aliucord.manager.patcher.steps.base.DownloadStep
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class DownloadDiscordRNALibStep : DownloadStep(), KoinComponent {
|
||||
private val paths: PathManager by inject()
|
||||
private val arch = Build.SUPPORTED_ABIS.first().replace("-v", "_v")
|
||||
|
||||
override val targetUrl = "https://tracker.vendetta.rocks/tracker/download/$version/config.$arch"
|
||||
override val targetFile = paths.discordApkVersionCache(version).resolve("config.$arch.apk")
|
||||
override val localizedName = R.string.downloading_libs_apk
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.aliucord.manager.patcher.steps.download
|
||||
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.manager.PathManager
|
||||
import com.aliucord.manager.patcher.steps.base.DownloadStep
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class DownloadDiscordRNAResourcesStep : DownloadStep(), KoinComponent {
|
||||
private val paths: PathManager by inject()
|
||||
|
||||
override val targetUrl = "https://tracker.vendetta.rocks/tracker/download/$version/config.xxhdpi"
|
||||
override val targetFile = paths.discordApkVersionCache(version).resolve("config.xxhdpi.apk")
|
||||
override val localizedName = R.string.downloading_resources_apk
|
||||
}
|
||||
|
|
@ -13,8 +13,8 @@ import org.koin.core.component.inject
|
|||
* from a redirect to an APK mirror site provided by the Aliucord backend.
|
||||
*/
|
||||
@Stable
|
||||
class DownloadDiscordStep : DownloadStep(), KoinComponent {
|
||||
private val paths: PathManager by inject()
|
||||
open class DownloadDiscordStep : DownloadStep(), KoinComponent {
|
||||
protected val paths: PathManager by inject()
|
||||
|
||||
override val localizedName = R.string.patch_step_dl_kt_apk
|
||||
override val targetUrl = getDiscordApkUrl(DISCORD_KT_VERSION)
|
||||
|
|
|
|||
|
|
@ -22,98 +22,100 @@ class AlignmentStep : Step(), KoinComponent {
|
|||
|
||||
override suspend fun execute(container: StepRunner) {
|
||||
val currentDeviceArch = Build.SUPPORTED_ABIS.first()
|
||||
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
||||
val apks = container.getStep<CopyDependenciesStep>().patchedApks
|
||||
|
||||
var resourcesArscBytes: ByteArray? = null
|
||||
var dexCount: Int = -1
|
||||
for (apk in apks) {
|
||||
var resourcesArscBytes: ByteArray? = null
|
||||
var dexCount: Int = -1
|
||||
|
||||
// Align resources.arsc due to targeting API 30 for silent install
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
container.log("Extracting resources.arsc to be aligned later")
|
||||
resourcesArscBytes = ZipReader(apk)
|
||||
.use { it.openEntry("resources.arsc")?.read() }
|
||||
?: throw IllegalArgumentException("APK is missing resources.arsc")
|
||||
}
|
||||
// Align resources.arsc due to targeting API 30 for silent install
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
container.log("Extracting resources.arsc to be aligned later")
|
||||
resourcesArscBytes = ZipReader(apk)
|
||||
.use { it.openEntry("resources.arsc")?.read() }
|
||||
?: continue // Alignment is not needed
|
||||
}
|
||||
|
||||
// Align dex files due to using useEmbeddedDex (ref. ManifestPatcher)
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
container.log("Extracting all dex files to be aligned later")
|
||||
ZipReader(apk).use { zip ->
|
||||
// Count the amount of dex files currently in the apk
|
||||
dexCount = zip.entryNames.count { it.endsWith(".dex") }
|
||||
// Align dex files due to using useEmbeddedDex (ref. ManifestPatcher)
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
container.log("Extracting all dex files to be aligned later")
|
||||
ZipReader(apk).use { zip ->
|
||||
// Count the amount of dex files currently in the apk
|
||||
dexCount = zip.entryNames.count { it.endsWith(".dex") }
|
||||
|
||||
// Copy all the dex files that need to be moved out of the apk
|
||||
for (idx in 0..<dexCount) {
|
||||
val bytes = zip.openEntry(getDexName(idx))!!.read()
|
||||
val file = paths.patchingWorkingDir().resolve(getDexName(idx))
|
||||
// Copy all the dex files that need to be moved out of the apk
|
||||
for (idx in 0..<dexCount) {
|
||||
val bytes = zip.openEntry(getDexName(idx))!!.read()
|
||||
val file = paths.patchingWorkingDir().resolve(getDexName(idx))
|
||||
file.writeBytes(bytes)
|
||||
}
|
||||
}
|
||||
container.log("Extracted $dexCount dex files")
|
||||
}
|
||||
|
||||
// Align native libs due to using extractNativeLibs
|
||||
container.log("Extracting native libraries to be aligned later")
|
||||
val nativeLibPaths = ZipReader(apk).use { zip ->
|
||||
val libPaths = zip.entryNames.filter { it.endsWith(".so") }
|
||||
|
||||
// Extract to disk temporarily
|
||||
for ((idx, path) in libPaths.withIndex()) {
|
||||
// Ignore lib architectures that don't match this device
|
||||
if (!path.startsWith("lib/$currentDeviceArch")) {
|
||||
container.log("Skipping native lib $path due to incompatible architecture")
|
||||
continue
|
||||
}
|
||||
|
||||
// Index is just used as a placeholder id to cache on disk
|
||||
val bytes = zip.openEntry(path)!!.read()
|
||||
val file = paths.patchingWorkingDir().resolve("$idx.so")
|
||||
file.writeBytes(bytes)
|
||||
}
|
||||
}
|
||||
container.log("Extracted $dexCount dex files")
|
||||
}
|
||||
|
||||
// Align native libs due to using extractNativeLibs
|
||||
container.log("Extracting native libraries to be aligned later")
|
||||
val nativeLibPaths = ZipReader(apk).use { zip ->
|
||||
val libPaths = zip.entryNames.filter { it.endsWith(".so") }
|
||||
|
||||
// Extract to disk temporarily
|
||||
for ((idx, path) in libPaths.withIndex()) {
|
||||
// Ignore lib architectures that don't match this device
|
||||
if (!path.startsWith("lib/$currentDeviceArch")) {
|
||||
container.log("Skipping native lib $path due to incompatible architecture")
|
||||
continue
|
||||
container.log("Extracted native lib $file")
|
||||
}
|
||||
|
||||
// Index is just used as a placeholder id to cache on disk
|
||||
val bytes = zip.openEntry(path)!!.read()
|
||||
val file = paths.patchingWorkingDir().resolve("$idx.so")
|
||||
file.writeBytes(bytes)
|
||||
container.log("Extracted native lib $file")
|
||||
libPaths
|
||||
}
|
||||
|
||||
libPaths
|
||||
}
|
||||
container.log("Writing entries back aligned")
|
||||
ZipWriter(apk, /* append = */ true).use { zip ->
|
||||
// Delete all the unaligned files from APK
|
||||
container.log("Deleting resources.arsc")
|
||||
if (resourcesArscBytes != null)
|
||||
zip.deleteEntry("resources.arsc")
|
||||
|
||||
container.log("Writing entries back aligned")
|
||||
ZipWriter(apk, /* append = */ true).use { zip ->
|
||||
// Delete all the unaligned files from APK
|
||||
container.log("Deleting resources.arsc")
|
||||
if (resourcesArscBytes != null)
|
||||
zip.deleteEntry("resources.arsc")
|
||||
container.log("Deleting $dexCount dex files")
|
||||
for (i in 0..<dexCount)
|
||||
zip.deleteEntry(getDexName(i))
|
||||
|
||||
container.log("Deleting $dexCount dex files")
|
||||
for (i in 0..<dexCount)
|
||||
zip.deleteEntry(getDexName(i))
|
||||
container.log("Deleting native libraries: $nativeLibPaths")
|
||||
for (path in nativeLibPaths)
|
||||
zip.deleteEntry(path)
|
||||
|
||||
container.log("Deleting native libraries: $nativeLibPaths")
|
||||
for (path in nativeLibPaths)
|
||||
zip.deleteEntry(path)
|
||||
// Write all the files back aligned this time
|
||||
if (resourcesArscBytes != null) {
|
||||
container.log("Writing resources.arsc uncompressed aligned to 4 bytes")
|
||||
zip.writeEntry("resources.arsc", resourcesArscBytes, ZipCompression.NONE, 4)
|
||||
}
|
||||
|
||||
// Write all the files back aligned this time
|
||||
if (resourcesArscBytes != null) {
|
||||
container.log("Writing resources.arsc uncompressed aligned to 4 bytes")
|
||||
zip.writeEntry("resources.arsc", resourcesArscBytes, ZipCompression.NONE, 4)
|
||||
}
|
||||
container.log("Writing dex files uncompressed aligned to 4 bytes")
|
||||
for (idx in 0..<dexCount) {
|
||||
val file = paths.patchingWorkingDir().resolve(getDexName(idx))
|
||||
val bytes = file.readBytes()
|
||||
zip.writeEntry(getDexName(idx), bytes, ZipCompression.NONE, 4)
|
||||
}
|
||||
|
||||
container.log("Writing dex files uncompressed aligned to 4 bytes")
|
||||
for (idx in 0..<dexCount) {
|
||||
val file = paths.patchingWorkingDir().resolve(getDexName(idx))
|
||||
val bytes = file.readBytes()
|
||||
zip.writeEntry(getDexName(idx), bytes, ZipCompression.NONE, 4)
|
||||
}
|
||||
// Write back native libraries aligned to 16KiB page boundary
|
||||
for ((idx, path) in nativeLibPaths.withIndex()) {
|
||||
// Ignore lib architectures that don't match this device
|
||||
if (!path.startsWith("lib/$currentDeviceArch"))
|
||||
continue
|
||||
|
||||
// Write back native libraries aligned to 16KiB page boundary
|
||||
for ((idx, path) in nativeLibPaths.withIndex()) {
|
||||
// Ignore lib architectures that don't match this device
|
||||
if (!path.startsWith("lib/$currentDeviceArch"))
|
||||
continue
|
||||
val file = paths.patchingWorkingDir().resolve("$idx.so")
|
||||
val bytes = file.readBytes()
|
||||
|
||||
val file = paths.patchingWorkingDir().resolve("$idx.so")
|
||||
val bytes = file.readBytes()
|
||||
|
||||
container.log("Writing $path uncompressed aligned to 16KiB")
|
||||
zip.writeEntry(path, bytes, ZipCompression.NONE, 16384)
|
||||
container.log("Writing $path uncompressed aligned to 16KiB")
|
||||
zip.writeEntry(path, bytes, ZipCompression.NONE, 16384)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ private const val READY_NOTIF_ID = 200001
|
|||
/**
|
||||
* Install the final APK with the system's PackageManager.
|
||||
*/
|
||||
class InstallStep(private val options: PatchOptions) : Step(), KoinComponent {
|
||||
open class InstallStep(private val options: PatchOptions) : Step(), KoinComponent {
|
||||
private val context: Context by inject()
|
||||
private val installers: InstallerManager by inject()
|
||||
private val prefs: PreferencesManager by inject()
|
||||
|
|
@ -36,7 +36,7 @@ class InstallStep(private val options: PatchOptions) : Step(), KoinComponent {
|
|||
override val localizedName = R.string.patch_step_install
|
||||
|
||||
override suspend fun execute(container: StepRunner) {
|
||||
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
||||
val apks = container.getStep<CopyDependenciesStep>().patchedApks
|
||||
|
||||
// If app backgrounded, show notification
|
||||
if (ProcessLifecycleOwner.get().lifecycle.currentState == Lifecycle.State.CREATED) {
|
||||
|
|
@ -66,9 +66,9 @@ class InstallStep(private val options: PatchOptions) : Step(), KoinComponent {
|
|||
prefs.showPlayProtectWarning = !neverShowAgain
|
||||
}
|
||||
|
||||
container.log("Installing ${apk.absolutePath}, silent: ${!prefs.devMode}")
|
||||
container.log("Installing ${apks.joinToString(", ") { it.name }}, silent: ${!prefs.devMode}")
|
||||
val result = installers.getActiveInstaller().waitInstall(
|
||||
apks = listOf(apk),
|
||||
apks = apks,
|
||||
silent = !prefs.devMode,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ class SigningStep : Step(), KoinComponent {
|
|||
override val localizedName = R.string.patch_step_signing
|
||||
|
||||
override suspend fun execute(container: StepRunner) {
|
||||
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
||||
|
||||
container.log("Signing apk at ${apk.absolutePath}")
|
||||
Signer.signApk(apk)
|
||||
for (apk in container.getStep<CopyDependenciesStep>().patchedApks) {
|
||||
container.log("Signing apk at ${apk.absolutePath}")
|
||||
Signer.signApk(apk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,25 +18,33 @@ class PatchManifestStep(private val options: PatchOptions) : Step() {
|
|||
override val localizedName = R.string.patch_step_patch_manifests
|
||||
|
||||
override suspend fun execute(container: StepRunner) {
|
||||
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
||||
val deps = container.getStep<CopyDependenciesStep>()
|
||||
for (apk in deps.patchedApks) {
|
||||
container.log("Reading manifest from apk")
|
||||
val manifest = ZipReader(apk)
|
||||
.use { zip -> zip.openEntry("AndroidManifest.xml")?.read() }
|
||||
?: throw IllegalArgumentException("No manifest found in APK")
|
||||
|
||||
container.log("Reading manifest from apk")
|
||||
val manifest = ZipReader(apk)
|
||||
.use { zip -> zip.openEntry("AndroidManifest.xml")?.read() }
|
||||
?: throw IllegalArgumentException("No manifest found in APK")
|
||||
container.log("Patching manifest")
|
||||
val patchedManifest = if (apk == deps.patchedApk)
|
||||
ManifestPatcher.patchManifest(
|
||||
manifestBytes = manifest,
|
||||
packageName = options.packageName,
|
||||
appName = options.appName,
|
||||
debuggable = options.debuggable,
|
||||
)
|
||||
else {
|
||||
ManifestPatcher.patchManifestPackageName(
|
||||
manifestBytes = manifest,
|
||||
packageName = options.packageName,
|
||||
)
|
||||
}
|
||||
|
||||
container.log("Patching manifest")
|
||||
val patchedManifest = ManifestPatcher.patchManifest(
|
||||
manifestBytes = manifest,
|
||||
packageName = options.packageName,
|
||||
appName = options.appName,
|
||||
debuggable = options.debuggable,
|
||||
)
|
||||
|
||||
container.log("Writing patched manifest to apk unaligned compressed")
|
||||
ZipWriter(apk, /* append = */ true).use {
|
||||
it.deleteEntry("AndroidManifest.xml")
|
||||
it.writeEntry("AndroidManifest.xml", patchedManifest)
|
||||
container.log("Writing patched manifest to apk unaligned compressed")
|
||||
ZipWriter(apk, /* append = */ true).use {
|
||||
it.deleteEntry("AndroidManifest.xml", apk == deps.patchedLibApk || apk == deps.patchedApk)
|
||||
it.writeEntry("AndroidManifest.xml", patchedManifest)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,17 +27,17 @@ class SaveMetadataStep(private val options: PatchOptions) : Step(), KoinComponen
|
|||
|
||||
override suspend fun execute(container: StepRunner) {
|
||||
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
||||
val aliuhook = container.getStep<DownloadAliuhookStep>()
|
||||
val injector = container.getStep<DownloadInjectorStep>()
|
||||
val patches = container.getStep<DownloadPatchesStep>()
|
||||
val aliuhook = container.getStepOrNull<DownloadAliuhookStep>()
|
||||
val injector = container.getStepOrNull<DownloadInjectorStep>()
|
||||
val patches = container.getStepOrNull<DownloadPatchesStep>()
|
||||
|
||||
val metadata = InstallMetadata(
|
||||
options = options,
|
||||
customManager = !BuildConfig.RELEASE,
|
||||
managerVersion = SemVer.parse(BuildConfig.VERSION_NAME),
|
||||
aliuhookVersion = aliuhook.targetVersion,
|
||||
injectorVersion = injector.targetVersion,
|
||||
patchesVersion = patches.targetVersion,
|
||||
aliuhookVersion = aliuhook?.targetVersion,
|
||||
injectorVersion = injector?.targetVersion,
|
||||
patchesVersion = patches?.targetVersion,
|
||||
)
|
||||
|
||||
container.log("Writing serialized install metadata to APK")
|
||||
|
|
|
|||
|
|
@ -191,6 +191,72 @@ object ManifestPatcher {
|
|||
return writer.toByteArray()
|
||||
}
|
||||
|
||||
fun patchManifestPackageName(
|
||||
manifestBytes: ByteArray,
|
||||
packageName: String,
|
||||
): ByteArray {
|
||||
val reader = AxmlReader(manifestBytes)
|
||||
val writer = AxmlWriter()
|
||||
|
||||
reader.accept(object : AxmlVisitor(writer) {
|
||||
override fun ns(prefix: String?, uri: String?, ln: Int) {
|
||||
val realUri = uri ?: ANDROID_NAMESPACE
|
||||
super.ns(prefix, realUri, ln)
|
||||
}
|
||||
|
||||
override fun child(ns: String?, name: String?) =
|
||||
object : ReplaceAttrsVisitor(
|
||||
super.child(ns, name),
|
||||
mapOf(PACKAGE to packageName)
|
||||
) {
|
||||
override fun child(ns: String?, name: String): NodeVisitor {
|
||||
val nv = super.child(ns, name)
|
||||
|
||||
return when (name) {
|
||||
"permission" -> object : NodeVisitor(nv) {
|
||||
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
|
||||
super.attr(
|
||||
ns, name, resourceId, type,
|
||||
if (name == "name") {
|
||||
(value as String).replace("com.discord", packageName)
|
||||
} else {
|
||||
value
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"application" -> object : NodeVisitor(nv) {
|
||||
override fun child(ns: String?, name: String): NodeVisitor {
|
||||
val visitor = super.child(ns, name)
|
||||
return when (name) {
|
||||
"provider" -> object : NodeVisitor(visitor) {
|
||||
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
|
||||
super.attr(
|
||||
ns, name, resourceId, type,
|
||||
if (name == "authorities") {
|
||||
(value as String).replace("com.discord", packageName)
|
||||
} else {
|
||||
value
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> visitor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> nv
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return writer.toByteArray()
|
||||
}
|
||||
|
||||
private open class ReplaceAttrsVisitor(
|
||||
nv: NodeVisitor,
|
||||
private val attrs: Map<String, Any>,
|
||||
|
|
|
|||
|
|
@ -250,9 +250,11 @@ class HomeModel(
|
|||
return false
|
||||
}
|
||||
|
||||
// Check that all the installation components are up-to-date
|
||||
return remoteBuildData.injectorVersion == installMetadata.injectorVersion
|
||||
&& remoteBuildData.patchesVersion == installMetadata.patchesVersion
|
||||
&& latestAliuhookVersion == installMetadata.aliuhookVersion
|
||||
// Check that all the installation components are up-to-date (skip not installed)
|
||||
val injectorUpToDate = installMetadata.injectorVersion == null || remoteBuildData.injectorVersion == installMetadata.injectorVersion
|
||||
val patchesUpToDate = installMetadata.patchesVersion == null || remoteBuildData.patchesVersion == installMetadata.patchesVersion
|
||||
val aliuhookUpToDate = installMetadata.aliuhookVersion == null || latestAliuhookVersion == installMetadata.aliuhookVersion
|
||||
|
||||
return injectorUpToDate && patchesUpToDate && aliuhookUpToDate
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import cafe.adriel.voyager.core.model.screenModelScope
|
|||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.manager.*
|
||||
import com.aliucord.manager.patcher.KotlinPatchRunner
|
||||
import com.aliucord.manager.patcher.StepRunner
|
||||
import com.aliucord.manager.patcher.WintryPatchRunner
|
||||
import com.aliucord.manager.patcher.steps.StepGroup
|
||||
import com.aliucord.manager.patcher.steps.base.*
|
||||
import com.aliucord.manager.patcher.steps.install.InstallStep
|
||||
|
|
@ -125,7 +125,7 @@ class PatchingScreenModel(
|
|||
}
|
||||
|
||||
private suspend fun startPatchRunner() {
|
||||
val runner = KotlinPatchRunner(options)
|
||||
val runner = WintryPatchRunner(options)
|
||||
.also { stepRunner = it }
|
||||
|
||||
val newSteps = runner.steps.groupBy { it.group }
|
||||
|
|
|
|||
|
|
@ -245,4 +245,9 @@
|
|||
<string name="fun_fact_10">Aliucord uses the *real* old UI, from mid-2022!</string>
|
||||
<string name="fun_fact_11">There was an attempt to make Aliucord run on iOS…</string>
|
||||
<string name="fun_fact_12" translatable="false">veeeee 😭</string>
|
||||
|
||||
<string name="download_libs_apk">Download libs apk</string>
|
||||
<string name="downloading_base_apk">Downloading base APK</string>
|
||||
<string name="downloading_libs_apk">Downloading libs apk</string>
|
||||
<string name="downloading_resources_apk">Downloading resources apk</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue