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.ui.screens.patchopts.PatchOptions
|
||||||
import com.aliucord.manager.util.IS_PROBABLY_EMULATOR
|
import com.aliucord.manager.util.IS_PROBABLY_EMULATOR
|
||||||
import com.aliucord.manager.util.isPlayProtectEnabled
|
import com.aliucord.manager.util.isPlayProtectEnabled
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.*
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
@ -149,7 +148,7 @@ class InstallLogManager(
|
||||||
@Serializable
|
@Serializable
|
||||||
data class InstallLogData(
|
data class InstallLogData(
|
||||||
val id: String,
|
val id: String,
|
||||||
val installDate: Instant,
|
@Contextual val installDate: Instant,
|
||||||
val installDuration: Duration,
|
val installDuration: Duration,
|
||||||
val installOptions: PatchOptions,
|
val installOptions: PatchOptions,
|
||||||
val environmentInfo: String,
|
val environmentInfo: String,
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ class PathManager(
|
||||||
/**
|
/**
|
||||||
* The APK that is worked on during the patching process.
|
* 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 {
|
private companion object {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -27,15 +27,15 @@ data class InstallMetadata(
|
||||||
/**
|
/**
|
||||||
* Version (commit hash) of the Aliuhook build that was injected into the APK.
|
* 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.
|
* 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.
|
* 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.
|
* @param completed Only match steps that have finished executing.
|
||||||
*/
|
*/
|
||||||
inline fun <reified T : Step> getStep(completed: Boolean = true): T {
|
inline fun <reified T : Step> getStep(completed: Boolean = true): T {
|
||||||
val step = steps.asSequence()
|
val step = getStepOrNull<T>(completed)
|
||||||
.filterIsInstance<T>()
|
|
||||||
.filter { !completed || it.state.isFinished }
|
|
||||||
.firstOrNull()
|
|
||||||
|
|
||||||
if (step == null) {
|
if (step == null) {
|
||||||
throw IllegalArgumentException("No completed step ${T::class.simpleName} exists in container")
|
throw IllegalArgumentException("No completed step ${T::class.simpleName} exists in container")
|
||||||
|
|
@ -54,6 +51,13 @@ abstract class StepRunner : KoinComponent {
|
||||||
return step
|
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.
|
* 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 context: Context by inject()
|
||||||
private val downloader: KtorDownloadManager by inject()
|
private val downloader: KtorDownloadManager by inject()
|
||||||
|
|
||||||
|
// Temporary
|
||||||
|
protected val version: Int = 289210
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The remote url to download
|
* The remote url to download
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -23,15 +23,30 @@ class CopyDependenciesStep : Step(), KoinComponent {
|
||||||
private val application: Application by inject()
|
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()
|
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 group = StepGroup.Download
|
||||||
override val localizedName = R.string.patch_step_copy_deps
|
override val localizedName = R.string.patch_step_copy_deps
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
val srcApk = container.getStep<DownloadDiscordStep>().targetFile
|
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()
|
val dir = paths.patchingWorkingDir()
|
||||||
|
|
||||||
container.log("Clearing patched directory")
|
container.log("Clearing patched directory")
|
||||||
|
|
@ -42,7 +57,7 @@ class CopyDependenciesStep : Step(), KoinComponent {
|
||||||
if (Build.VERSION.SDK_INT >= 26) {
|
if (Build.VERSION.SDK_INT >= 26) {
|
||||||
val storageManager = application.getSystemService<StorageManager>()!!
|
val storageManager = application.getSystemService<StorageManager>()!!
|
||||||
val targetFileStorageId = storageManager.getUuidForPath(patchedApk)
|
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:
|
// We request 3.5x the size of the APK, to give space for the following:
|
||||||
// 1) A copy of the APK
|
// 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)
|
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.
|
* from a redirect to an APK mirror site provided by the Aliucord backend.
|
||||||
*/
|
*/
|
||||||
@Stable
|
@Stable
|
||||||
class DownloadDiscordStep : DownloadStep(), KoinComponent {
|
open class DownloadDiscordStep : DownloadStep(), KoinComponent {
|
||||||
private val paths: PathManager by inject()
|
protected val paths: PathManager by inject()
|
||||||
|
|
||||||
override val localizedName = R.string.patch_step_dl_kt_apk
|
override val localizedName = R.string.patch_step_dl_kt_apk
|
||||||
override val targetUrl = getDiscordApkUrl(DISCORD_KT_VERSION)
|
override val targetUrl = getDiscordApkUrl(DISCORD_KT_VERSION)
|
||||||
|
|
|
||||||
|
|
@ -22,98 +22,100 @@ class AlignmentStep : Step(), KoinComponent {
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
val currentDeviceArch = Build.SUPPORTED_ABIS.first()
|
val currentDeviceArch = Build.SUPPORTED_ABIS.first()
|
||||||
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
val apks = container.getStep<CopyDependenciesStep>().patchedApks
|
||||||
|
|
||||||
var resourcesArscBytes: ByteArray? = null
|
for (apk in apks) {
|
||||||
var dexCount: Int = -1
|
var resourcesArscBytes: ByteArray? = null
|
||||||
|
var dexCount: Int = -1
|
||||||
|
|
||||||
// Align resources.arsc due to targeting API 30 for silent install
|
// Align resources.arsc due to targeting API 30 for silent install
|
||||||
if (Build.VERSION.SDK_INT >= 30) {
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
container.log("Extracting resources.arsc to be aligned later")
|
container.log("Extracting resources.arsc to be aligned later")
|
||||||
resourcesArscBytes = ZipReader(apk)
|
resourcesArscBytes = ZipReader(apk)
|
||||||
.use { it.openEntry("resources.arsc")?.read() }
|
.use { it.openEntry("resources.arsc")?.read() }
|
||||||
?: throw IllegalArgumentException("APK is missing resources.arsc")
|
?: continue // Alignment is not needed
|
||||||
}
|
}
|
||||||
|
|
||||||
// Align dex files due to using useEmbeddedDex (ref. ManifestPatcher)
|
// Align dex files due to using useEmbeddedDex (ref. ManifestPatcher)
|
||||||
if (Build.VERSION.SDK_INT >= 29) {
|
if (Build.VERSION.SDK_INT >= 29) {
|
||||||
container.log("Extracting all dex files to be aligned later")
|
container.log("Extracting all dex files to be aligned later")
|
||||||
ZipReader(apk).use { zip ->
|
ZipReader(apk).use { zip ->
|
||||||
// Count the amount of dex files currently in the apk
|
// Count the amount of dex files currently in the apk
|
||||||
dexCount = zip.entryNames.count { it.endsWith(".dex") }
|
dexCount = zip.entryNames.count { it.endsWith(".dex") }
|
||||||
|
|
||||||
// Copy all the dex files that need to be moved out of the apk
|
// Copy all the dex files that need to be moved out of the apk
|
||||||
for (idx in 0..<dexCount) {
|
for (idx in 0..<dexCount) {
|
||||||
val bytes = zip.openEntry(getDexName(idx))!!.read()
|
val bytes = zip.openEntry(getDexName(idx))!!.read()
|
||||||
val file = paths.patchingWorkingDir().resolve(getDexName(idx))
|
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)
|
file.writeBytes(bytes)
|
||||||
}
|
container.log("Extracted native lib $file")
|
||||||
}
|
|
||||||
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
|
libPaths
|
||||||
val bytes = zip.openEntry(path)!!.read()
|
|
||||||
val file = paths.patchingWorkingDir().resolve("$idx.so")
|
|
||||||
file.writeBytes(bytes)
|
|
||||||
container.log("Extracted native lib $file")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
container.log("Deleting $dexCount dex files")
|
||||||
ZipWriter(apk, /* append = */ true).use { zip ->
|
for (i in 0..<dexCount)
|
||||||
// Delete all the unaligned files from APK
|
zip.deleteEntry(getDexName(i))
|
||||||
container.log("Deleting resources.arsc")
|
|
||||||
if (resourcesArscBytes != null)
|
|
||||||
zip.deleteEntry("resources.arsc")
|
|
||||||
|
|
||||||
container.log("Deleting $dexCount dex files")
|
container.log("Deleting native libraries: $nativeLibPaths")
|
||||||
for (i in 0..<dexCount)
|
for (path in nativeLibPaths)
|
||||||
zip.deleteEntry(getDexName(i))
|
zip.deleteEntry(path)
|
||||||
|
|
||||||
container.log("Deleting native libraries: $nativeLibPaths")
|
// Write all the files back aligned this time
|
||||||
for (path in nativeLibPaths)
|
if (resourcesArscBytes != null) {
|
||||||
zip.deleteEntry(path)
|
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
|
container.log("Writing dex files uncompressed aligned to 4 bytes")
|
||||||
if (resourcesArscBytes != null) {
|
for (idx in 0..<dexCount) {
|
||||||
container.log("Writing resources.arsc uncompressed aligned to 4 bytes")
|
val file = paths.patchingWorkingDir().resolve(getDexName(idx))
|
||||||
zip.writeEntry("resources.arsc", resourcesArscBytes, ZipCompression.NONE, 4)
|
val bytes = file.readBytes()
|
||||||
}
|
zip.writeEntry(getDexName(idx), bytes, ZipCompression.NONE, 4)
|
||||||
|
}
|
||||||
|
|
||||||
container.log("Writing dex files uncompressed aligned to 4 bytes")
|
// Write back native libraries aligned to 16KiB page boundary
|
||||||
for (idx in 0..<dexCount) {
|
for ((idx, path) in nativeLibPaths.withIndex()) {
|
||||||
val file = paths.patchingWorkingDir().resolve(getDexName(idx))
|
// Ignore lib architectures that don't match this device
|
||||||
val bytes = file.readBytes()
|
if (!path.startsWith("lib/$currentDeviceArch"))
|
||||||
zip.writeEntry(getDexName(idx), bytes, ZipCompression.NONE, 4)
|
continue
|
||||||
}
|
|
||||||
|
|
||||||
// Write back native libraries aligned to 16KiB page boundary
|
val file = paths.patchingWorkingDir().resolve("$idx.so")
|
||||||
for ((idx, path) in nativeLibPaths.withIndex()) {
|
val bytes = file.readBytes()
|
||||||
// Ignore lib architectures that don't match this device
|
|
||||||
if (!path.startsWith("lib/$currentDeviceArch"))
|
|
||||||
continue
|
|
||||||
|
|
||||||
val file = paths.patchingWorkingDir().resolve("$idx.so")
|
container.log("Writing $path uncompressed aligned to 16KiB")
|
||||||
val bytes = file.readBytes()
|
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.
|
* 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 context: Context by inject()
|
||||||
private val installers: InstallerManager by inject()
|
private val installers: InstallerManager by inject()
|
||||||
private val prefs: PreferencesManager 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 val localizedName = R.string.patch_step_install
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
val apks = container.getStep<CopyDependenciesStep>().patchedApks
|
||||||
|
|
||||||
// If app backgrounded, show notification
|
// If app backgrounded, show notification
|
||||||
if (ProcessLifecycleOwner.get().lifecycle.currentState == Lifecycle.State.CREATED) {
|
if (ProcessLifecycleOwner.get().lifecycle.currentState == Lifecycle.State.CREATED) {
|
||||||
|
|
@ -66,9 +66,9 @@ class InstallStep(private val options: PatchOptions) : Step(), KoinComponent {
|
||||||
prefs.showPlayProtectWarning = !neverShowAgain
|
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(
|
val result = installers.getActiveInstaller().waitInstall(
|
||||||
apks = listOf(apk),
|
apks = apks,
|
||||||
silent = !prefs.devMode,
|
silent = !prefs.devMode,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@ class SigningStep : Step(), KoinComponent {
|
||||||
override val localizedName = R.string.patch_step_signing
|
override val localizedName = R.string.patch_step_signing
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
for (apk in container.getStep<CopyDependenciesStep>().patchedApks) {
|
||||||
|
container.log("Signing apk at ${apk.absolutePath}")
|
||||||
container.log("Signing apk at ${apk.absolutePath}")
|
Signer.signApk(apk)
|
||||||
Signer.signApk(apk)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,25 +18,33 @@ class PatchManifestStep(private val options: PatchOptions) : Step() {
|
||||||
override val localizedName = R.string.patch_step_patch_manifests
|
override val localizedName = R.string.patch_step_patch_manifests
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
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")
|
container.log("Patching manifest")
|
||||||
val manifest = ZipReader(apk)
|
val patchedManifest = if (apk == deps.patchedApk)
|
||||||
.use { zip -> zip.openEntry("AndroidManifest.xml")?.read() }
|
ManifestPatcher.patchManifest(
|
||||||
?: throw IllegalArgumentException("No manifest found in APK")
|
manifestBytes = manifest,
|
||||||
|
packageName = options.packageName,
|
||||||
|
appName = options.appName,
|
||||||
|
debuggable = options.debuggable,
|
||||||
|
)
|
||||||
|
else {
|
||||||
|
ManifestPatcher.patchManifestPackageName(
|
||||||
|
manifestBytes = manifest,
|
||||||
|
packageName = options.packageName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
container.log("Patching manifest")
|
container.log("Writing patched manifest to apk unaligned compressed")
|
||||||
val patchedManifest = ManifestPatcher.patchManifest(
|
ZipWriter(apk, /* append = */ true).use {
|
||||||
manifestBytes = manifest,
|
it.deleteEntry("AndroidManifest.xml", apk == deps.patchedLibApk || apk == deps.patchedApk)
|
||||||
packageName = options.packageName,
|
it.writeEntry("AndroidManifest.xml", patchedManifest)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,17 +27,17 @@ class SaveMetadataStep(private val options: PatchOptions) : Step(), KoinComponen
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
||||||
val aliuhook = container.getStep<DownloadAliuhookStep>()
|
val aliuhook = container.getStepOrNull<DownloadAliuhookStep>()
|
||||||
val injector = container.getStep<DownloadInjectorStep>()
|
val injector = container.getStepOrNull<DownloadInjectorStep>()
|
||||||
val patches = container.getStep<DownloadPatchesStep>()
|
val patches = container.getStepOrNull<DownloadPatchesStep>()
|
||||||
|
|
||||||
val metadata = InstallMetadata(
|
val metadata = InstallMetadata(
|
||||||
options = options,
|
options = options,
|
||||||
customManager = !BuildConfig.RELEASE,
|
customManager = !BuildConfig.RELEASE,
|
||||||
managerVersion = SemVer.parse(BuildConfig.VERSION_NAME),
|
managerVersion = SemVer.parse(BuildConfig.VERSION_NAME),
|
||||||
aliuhookVersion = aliuhook.targetVersion,
|
aliuhookVersion = aliuhook?.targetVersion,
|
||||||
injectorVersion = injector.targetVersion,
|
injectorVersion = injector?.targetVersion,
|
||||||
patchesVersion = patches.targetVersion,
|
patchesVersion = patches?.targetVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
container.log("Writing serialized install metadata to APK")
|
container.log("Writing serialized install metadata to APK")
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,72 @@ object ManifestPatcher {
|
||||||
return writer.toByteArray()
|
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(
|
private open class ReplaceAttrsVisitor(
|
||||||
nv: NodeVisitor,
|
nv: NodeVisitor,
|
||||||
private val attrs: Map<String, Any>,
|
private val attrs: Map<String, Any>,
|
||||||
|
|
|
||||||
|
|
@ -250,9 +250,11 @@ class HomeModel(
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that all the installation components are up-to-date
|
// Check that all the installation components are up-to-date (skip not installed)
|
||||||
return remoteBuildData.injectorVersion == installMetadata.injectorVersion
|
val injectorUpToDate = installMetadata.injectorVersion == null || remoteBuildData.injectorVersion == installMetadata.injectorVersion
|
||||||
&& remoteBuildData.patchesVersion == installMetadata.patchesVersion
|
val patchesUpToDate = installMetadata.patchesVersion == null || remoteBuildData.patchesVersion == installMetadata.patchesVersion
|
||||||
&& latestAliuhookVersion == installMetadata.aliuhookVersion
|
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.BuildConfig
|
||||||
import com.aliucord.manager.R
|
import com.aliucord.manager.R
|
||||||
import com.aliucord.manager.manager.*
|
import com.aliucord.manager.manager.*
|
||||||
import com.aliucord.manager.patcher.KotlinPatchRunner
|
|
||||||
import com.aliucord.manager.patcher.StepRunner
|
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.StepGroup
|
||||||
import com.aliucord.manager.patcher.steps.base.*
|
import com.aliucord.manager.patcher.steps.base.*
|
||||||
import com.aliucord.manager.patcher.steps.install.InstallStep
|
import com.aliucord.manager.patcher.steps.install.InstallStep
|
||||||
|
|
@ -125,7 +125,7 @@ class PatchingScreenModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun startPatchRunner() {
|
private suspend fun startPatchRunner() {
|
||||||
val runner = KotlinPatchRunner(options)
|
val runner = WintryPatchRunner(options)
|
||||||
.also { stepRunner = it }
|
.also { stepRunner = it }
|
||||||
|
|
||||||
val newSteps = runner.steps.groupBy { it.group }
|
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_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_11">There was an attempt to make Aliucord run on iOS…</string>
|
||||||
<string name="fun_fact_12" translatable="false">veeeee 😭</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>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue