feat(patching): add detailed logging to each step

This commit is contained in:
rushiiMachine 2025-04-09 20:19:14 -07:00
parent d64a1767fc
commit 96a463349c
No known key found for this signature in database
GPG key ID: DCBE5952BB3B6420
25 changed files with 220 additions and 48 deletions

View file

@ -32,6 +32,9 @@
static <1>$$serializer INSTANCE; static <1>$$serializer INSTANCE;
} }
# Keep class names of patching steps since they're used via reflection
-keepnames com.aliucord.manager.patcher.steps.**
# Uncomment this to preserve the line number information for # Uncomment this to preserve the line number information for
# debugging stack traces. # debugging stack traces.
-keepattributes SourceFile,LineNumberTable -keepattributes SourceFile,LineNumberTable

View file

@ -24,6 +24,7 @@ import java.util.Locale
*/ */
class InstallLogManager( class InstallLogManager(
private val application: Application, private val application: Application,
private val prefs: PreferencesManager,
private val json: Json, private val json: Json,
) { ) {
val logsDir = application.filesDir.resolve("install-logs").apply { mkdir() } val logsDir = application.filesDir.resolve("install-logs").apply { mkdir() }
@ -51,6 +52,7 @@ class InstallLogManager(
id: String, id: String,
installDate: Instant, installDate: Instant,
options: PatchOptions, options: PatchOptions,
log: String,
error: Throwable?, error: Throwable?,
) { ) {
val path = logsDir.resolve("$id.json") val path = logsDir.resolve("$id.json")
@ -60,7 +62,7 @@ class InstallLogManager(
installDate = installDate, installDate = installDate,
installOptions = options, installOptions = options,
environmentInfo = getEnvironmentInfo(), environmentInfo = getEnvironmentInfo(),
installationLog = "", // TODO: install log installationLog = log,
errorStacktrace = error?.let { Log.getStackTraceString(it).trimEnd() }, errorStacktrace = error?.let { Log.getStackTraceString(it).trimEnd() },
) )
@ -83,12 +85,13 @@ class InstallLogManager(
return """ return """
Aliucord Manager v${BuildConfig.VERSION_NAME} Aliucord Manager v${BuildConfig.VERSION_NAME}
Built from commit ${BuildConfig.GIT_COMMIT} on ${BuildConfig.GIT_BRANCH} $gitChanges Built from commit ${BuildConfig.GIT_COMMIT} on ${BuildConfig.GIT_BRANCH} $gitChanges
Developer mode: ${if (prefs.devMode) "On" else "Off"}
Android API: ${Build.VERSION.SDK_INT} Android API: ${Build.VERSION.SDK_INT}
Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()} Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()}
ROM: Android ${Build.VERSION.RELEASE} (Patch ${Build.VERSION.SECURITY_PATCH}) ROM: Android ${Build.VERSION.RELEASE} (Patch ${Build.VERSION.SECURITY_PATCH})
Device: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE}) Device: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE})
Emulator: $IS_PROBABLY_EMULATOR (guess) Emulator: ${if (IS_PROBABLY_EMULATOR) "Yes" else "No"} (guess)
Play Protect: $playProtect Play Protect: $playProtect
SOC: $soc SOC: $soc
""".trimIndent() """.trimIndent()

View file

@ -12,9 +12,7 @@ class AliucordGithubService(
) { ) {
suspend fun getDataJson(): ApiResponse<BuildInfo> { suspend fun getDataJson(): ApiResponse<BuildInfo> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
http.request { http.request { url(DATA_JSON_URL) }
url("https://raw.githubusercontent.com/$ORG/$MAIN_REPO/builds/data.json")
}
} }
} }
@ -25,6 +23,7 @@ class AliucordGithubService(
const val MAIN_REPO = "Aliucord" const val MAIN_REPO = "Aliucord"
const val MANAGER_REPO = "Manager" const val MANAGER_REPO = "Manager"
const val DATA_JSON_URL = "https://raw.githubusercontent.com/$ORG/$MAIN_REPO/builds/data.json"
const val PATCHED_APKS_INFO_URL = "https://github.com/$ORG/$MANAGER_REPO/blob/main/INFO.md#obtaining-patched-apks" const val PATCHED_APKS_INFO_URL = "https://github.com/$ORG/$MANAGER_REPO/blob/main/INFO.md#obtaining-patched-apks"
} }
} }

View file

@ -1,8 +1,10 @@
package com.aliucord.manager.patcher package com.aliucord.manager.patcher
import android.content.Context import android.content.Context
import android.util.Log
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.manager.PreferencesManager import com.aliucord.manager.manager.PreferencesManager
import com.aliucord.manager.patcher.steps.StepGroup import com.aliucord.manager.patcher.steps.StepGroup
@ -30,6 +32,8 @@ abstract class StepRunner : KoinComponent {
private val context: Context by inject() private val context: Context by inject()
private val preferences: PreferencesManager by inject() private val preferences: PreferencesManager by inject()
private val logEntries: MutableList<String> = mutableListOf()
abstract val steps: ImmutableList<Step> abstract val steps: ImmutableList<Step>
/** /**
@ -50,14 +54,36 @@ abstract class StepRunner : KoinComponent {
return step return step
} }
/**
* Adds a log entry without any associated log level.
*/
fun log(text: String) {
logEntries += text
Log.i(BuildConfig.TAG, text)
}
/**
* Combines all the log entries into a single formatted log.
*/
fun getLog(): String = logEntries.joinToString(separator = "\n")
suspend fun executeAll(): Throwable? { suspend fun executeAll(): Throwable? {
log("Starting step runner")
log("Registered steps: " + steps.joinToString { it.javaClass.simpleName })
for (step in steps) { for (step in steps) {
val stepName = step.javaClass.simpleName
log("Running step: $stepName")
val error = step.executeCatching(this@StepRunner) val error = step.executeCatching(this@StepRunner)
if (error != null) { if (error != null) {
log("Failed on step: $stepName after ${step.getDuration()}ms")
showErrorNotification() showErrorNotification()
// If this is a patch step and it failed, then clear download cache just in case // If this is a patch step and it failed, then clear download cache just in case
if (step.group == StepGroup.Patch && !preferences.devMode) { if (step.group == StepGroup.Patch && !preferences.devMode) {
log("Deleting download cache")
for (downloadStep in steps.asSequence().filterIsInstance<DownloadStep>()) { for (downloadStep in steps.asSequence().filterIsInstance<DownloadStep>()) {
downloadStep.targetFile.delete() downloadStep.targetFile.delete()
} }
@ -71,8 +97,12 @@ abstract class StepRunner : KoinComponent {
if (!preferences.devMode && duration < MINIMUM_STEP_DELAY) { if (!preferences.devMode && duration < MINIMUM_STEP_DELAY) {
delay(MINIMUM_STEP_DELAY - duration) delay(MINIMUM_STEP_DELAY - duration)
} }
log("Completed step: $stepName in ${duration}ms")
} }
log("Successfully finished all steps in ${steps.sumOf { it.getDuration() }}ms")
return null return null
} }

View file

@ -1,6 +1,7 @@
package com.aliucord.manager.patcher.steps.base package com.aliucord.manager.patcher.steps.base
import android.content.Context import android.content.Context
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
@ -10,6 +11,7 @@ import com.aliucord.manager.manager.download.KtorDownloadManager
import com.aliucord.manager.patcher.StepRunner import com.aliucord.manager.patcher.StepRunner
import com.aliucord.manager.patcher.steps.StepGroup import com.aliucord.manager.patcher.steps.StepGroup
import com.aliucord.manager.util.showToast import com.aliucord.manager.util.showToast
import com.aliucord.manager.util.toPrecision
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -48,24 +50,43 @@ abstract class DownloadStep : Step(), KoinComponent {
override val group = StepGroup.Download override val group = StepGroup.Download
override suspend fun execute(container: StepRunner) { override suspend fun execute(container: StepRunner) {
if (targetFile.exists()) { container.log("Checking if file cached: ${targetFile.absolutePath}")
if (targetFile.length() > 0) {
state = StepState.Skipped
return
}
targetFile.delete() if (targetFile.exists()) {
container.log("File exists, verifying...")
try {
verify()
state = StepState.Skipped
container.log("File verified, skipping download")
return
} catch (t: Throwable) {
targetFile.delete()
container.log("Verification error: " + Log.getStackTraceString(t))
container.log("File failed verification, deleting and redownloading")
}
} }
container.log("Downloading file at url: $targetUrl")
var lastLogProgress = 0f
val result = downloader.download(targetUrl, targetFile) { newProgress -> val result = downloader.download(targetUrl, targetFile) { newProgress ->
progress = newProgress ?: -1f progress = newProgress ?: -1f
if (newProgress != null && newProgress > lastLogProgress + 0.1f) {
container.log("Download progress: ${(newProgress * 100.0).toPrecision(0)}% after ${getDuration()}ms")
lastLogProgress = newProgress
}
} }
when (result) { when (result) {
is IDownloadManager.Result.Cancelled -> is IDownloadManager.Result.Cancelled -> {
state = StepState.Error state = StepState.Error
container.log("Download cancelled!")
}
is IDownloadManager.Result.Success -> { is IDownloadManager.Result.Success -> {
container.log("Successfully downloaded file, verifying...")
try { try {
verify() verify()
} catch (t: Throwable) { } catch (t: Throwable) {
@ -73,9 +94,12 @@ abstract class DownloadStep : Step(), KoinComponent {
context.showToast(R.string.installer_dl_verify_fail) context.showToast(R.string.installer_dl_verify_fail)
} }
container.log("Failed to verify file, deleting...")
targetFile.delete() targetFile.delete()
throw t throw t
} }
container.log("Verified downloaded file")
} }
is IDownloadManager.Result.Error -> { is IDownloadManager.Result.Error -> {
@ -86,6 +110,7 @@ abstract class DownloadStep : Step(), KoinComponent {
Toast.makeText(context, toastText, Toast.LENGTH_LONG).show() Toast.makeText(context, toastText, Toast.LENGTH_LONG).show()
} }
container.log("Failed to download file")
throw Error("Failed to download: $result") throw Error("Failed to download: $result")
} }
} }

View file

@ -27,11 +27,13 @@ class CopyDependenciesStep : Step(), KoinComponent {
override suspend fun execute(container: StepRunner) { override suspend fun execute(container: StepRunner) {
val dir = paths.patchingWorkingDir() val dir = paths.patchingWorkingDir()
container.log("Clearing patched directory")
if (!dir.deleteRecursively()) if (!dir.deleteRecursively())
throw Error("Failed to clear existing patched dir") throw Error("Failed to clear existing patched dir")
val srcApk = container.getStep<DownloadDiscordStep>().targetFile val srcApk = container.getStep<DownloadDiscordStep>().targetFile
container.log("Copying patched apk from ${srcApk.absolutePath} to ${patchedApk.absolutePath}")
srcApk.copyTo(patchedApk) srcApk.copyTo(patchedApk)
} }
} }

View file

@ -34,7 +34,9 @@ class DownloadAliuhookStep : DownloadStep(), IDexProvider, KoinComponent {
override val targetFile get() = paths.cachedAliuhookAAR(targetVersion) override val targetFile get() = paths.cachedAliuhookAAR(targetVersion)
override suspend fun execute(container: StepRunner) { override suspend fun execute(container: StepRunner) {
container.log("Obtaining latest aliuhook version")
targetVersion = maven.getAliuhookVersion().getOrThrow() targetVersion = maven.getAliuhookVersion().getOrThrow()
container.log("Fetched aliuhook version: $targetVersion")
super.execute(container) super.execute(container)
} }

View file

@ -38,11 +38,12 @@ class DownloadInjectorStep : DownloadStep(), IDexProvider, KoinComponent {
get() = paths.cachedInjectorDex(targetVersion, custom = isCustomVersion) get() = paths.cachedInjectorDex(targetVersion, custom = isCustomVersion)
override suspend fun execute(container: StepRunner) { override suspend fun execute(container: StepRunner) {
var customVersions = mutableStateListOf<SemVer>() val customVersions = mutableStateListOf<SemVer>()
.apply { addAll(paths.customInjectorDexs()) } .apply { addAll(paths.customInjectorDexs()) }
// Prompt to select or manage custom versions instead of downloading // Prompt to select or manage custom versions instead of downloading
if (customVersions.isNotEmpty()) { if (customVersions.isNotEmpty()) {
container.log("Custom versions present, waiting for selection: ${customVersions.joinToString()}")
val selectedVersion = overlays.startComposableForResult { callback -> val selectedVersion = overlays.startComposableForResult { callback ->
CustomComponentVersionPicker( CustomComponentVersionPicker(
componentTitle = "Injector", componentTitle = "Injector",
@ -63,16 +64,21 @@ class DownloadInjectorStep : DownloadStep(), IDexProvider, KoinComponent {
onCancel = { callback(null) }, onCancel = { callback(null) },
) )
} }
if (selectedVersion != null) { if (selectedVersion != null) {
isCustomVersion = true isCustomVersion = true
targetVersion = selectedVersion targetVersion = selectedVersion
state = StepState.Skipped state = StepState.Skipped
container.log("Using local custom injector version $selectedVersion")
return return
} else {
container.log("Selection dialog cancelled, continuing as normal")
} }
} }
targetVersion = container.getStep<FetchInfoStep>() targetVersion = container.getStep<FetchInfoStep>()
.data.injectorVersion .data.injectorVersion
container.log("Downloading injector version $targetVersion")
super.execute(container) super.execute(container)
} }

View file

@ -36,11 +36,12 @@ class DownloadPatchesStep : DownloadStep(), KoinComponent {
override val targetFile get() = paths.cachedSmaliPatches(targetVersion, isCustomVersion) override val targetFile get() = paths.cachedSmaliPatches(targetVersion, isCustomVersion)
override suspend fun execute(container: StepRunner) { override suspend fun execute(container: StepRunner) {
var customVersions = mutableStateListOf<SemVer>() val customVersions = mutableStateListOf<SemVer>()
.apply { addAll(paths.customSmaliPatches()) } .apply { addAll(paths.customSmaliPatches()) }
// Prompt to select or manage custom versions instead of downloading // Prompt to select or manage custom versions instead of downloading
if (customVersions.isNotEmpty()) { if (customVersions.isNotEmpty()) {
container.log("Custom versions present, waiting for selection: ${customVersions.joinToString()}")
val selectedVersion = overlays.startComposableForResult { callback -> val selectedVersion = overlays.startComposableForResult { callback ->
CustomComponentVersionPicker( CustomComponentVersionPicker(
componentTitle = "Smali Patches", componentTitle = "Smali Patches",
@ -61,16 +62,21 @@ class DownloadPatchesStep : DownloadStep(), KoinComponent {
onCancel = { callback(null) }, onCancel = { callback(null) },
) )
} }
if (selectedVersion != null) { if (selectedVersion != null) {
isCustomVersion = true isCustomVersion = true
targetVersion = selectedVersion targetVersion = selectedVersion
state = StepState.Skipped state = StepState.Skipped
container.log("Using local custom patches version $selectedVersion")
return return
} else {
container.log("Selection dialog cancelled, continuing as normal")
} }
} }
targetVersion = container.getStep<FetchInfoStep>() targetVersion = container.getStep<FetchInfoStep>()
.data.patchesVersion .data.patchesVersion
container.log("Downloading patches version $targetVersion")
super.execute(container) super.execute(container)
} }

View file

@ -25,11 +25,11 @@ class AlignmentStep : Step(), KoinComponent {
val apk = container.getStep<CopyDependenciesStep>().patchedApk val apk = container.getStep<CopyDependenciesStep>().patchedApk
var resourcesArscBytes: ByteArray? = null var resourcesArscBytes: ByteArray? = null
var nativeLibPaths: List<String>? = null
var dexCount: Int = -1 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")
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") ?: throw IllegalArgumentException("APK is missing resources.arsc")
@ -37,6 +37,7 @@ class AlignmentStep : Step(), KoinComponent {
// 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")
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") }
@ -48,42 +49,54 @@ class AlignmentStep : Step(), KoinComponent {
file.writeBytes(bytes) file.writeBytes(bytes)
} }
} }
container.log("Extracted $dexCount dex files")
} }
// Align native libs due to using extractNativeLibs // Align native libs due to using extractNativeLibs
nativeLibPaths = ZipReader(apk).use { zip -> container.log("Extracting native libraries to be aligned later")
val nativeLibPaths = ZipReader(apk).use { zip ->
val libPaths = zip.entryNames.filter { it.endsWith(".so") } val libPaths = zip.entryNames.filter { it.endsWith(".so") }
// Extract to disk temporarily // Extract to disk temporarily
for ((idx, path) in libPaths.withIndex()) { for ((idx, path) in libPaths.withIndex()) {
// Ignore lib architectures that don't match this device // Ignore lib architectures that don't match this device
if (!path.startsWith("lib/$currentDeviceArch")) if (!path.startsWith("lib/$currentDeviceArch")) {
container.log("Skipping native lib $path due to incompatible architecture")
continue continue
}
// Index is just used as a placeholder id to cache on disk // Index is just used as a placeholder id to cache on disk
val bytes = zip.openEntry(path)!!.read() val bytes = zip.openEntry(path)!!.read()
val file = paths.patchingWorkingDir().resolve("$idx.so") val file = paths.patchingWorkingDir().resolve("$idx.so")
file.writeBytes(bytes) file.writeBytes(bytes)
container.log("Extracted native lib $file")
} }
libPaths libPaths
} }
container.log("Writing entries back aligned")
ZipWriter(apk, /* append = */ true).use { zip -> ZipWriter(apk, /* append = */ true).use { zip ->
// Delete all the unaligned files from APK // Delete all the unaligned files from APK
container.log("Deleting resources.arsc")
if (resourcesArscBytes != null) if (resourcesArscBytes != null)
zip.deleteEntry("resources.arsc") zip.deleteEntry("resources.arsc")
container.log("Deleting $dexCount dex files")
for (i in 0..<dexCount) for (i in 0..<dexCount)
zip.deleteEntry(getDexName(i)) zip.deleteEntry(getDexName(i))
container.log("Deleting native libraries: $nativeLibPaths")
for (path in nativeLibPaths) for (path in nativeLibPaths)
zip.deleteEntry(path) zip.deleteEntry(path)
// Write all the files back aligned this time // Write all the files back aligned this time
if (resourcesArscBytes != null) if (resourcesArscBytes != null) {
container.log("Writing resources.arsc uncompressed aligned to 4 bytes")
zip.writeEntry("resources.arsc", resourcesArscBytes, ZipCompression.NONE, 4) zip.writeEntry("resources.arsc", resourcesArscBytes, ZipCompression.NONE, 4)
}
container.log("Writing dex files uncompressed aligned to 4 bytes")
for (idx in 0..<dexCount) { for (idx in 0..<dexCount) {
val file = paths.patchingWorkingDir().resolve(getDexName(idx)) val file = paths.patchingWorkingDir().resolve(getDexName(idx))
val bytes = file.readBytes() val bytes = file.readBytes()
@ -98,6 +111,8 @@ class AlignmentStep : Step(), KoinComponent {
val file = paths.patchingWorkingDir().resolve("$idx.so") val file = paths.patchingWorkingDir().resolve("$idx.so")
val bytes = file.readBytes() val bytes = file.readBytes()
container.log("Writing $path uncompressed aligned to 16KiB")
zip.writeEntry(path, bytes, ZipCompression.NONE, 16384) zip.writeEntry(path, bytes, ZipCompression.NONE, 16384)
} }
} }

View file

@ -22,8 +22,10 @@ class CleanupStep : Step(), KoinComponent {
override suspend fun execute(container: StepRunner) { override suspend fun execute(container: StepRunner) {
if (prefs.keepPatchedApks) { if (prefs.keepPatchedApks) {
container.log("keepPatchedApks enabled, skipping...")
state = StepState.Skipped state = StepState.Skipped
} else { } else {
container.log("Deleting patching working dir")
if (!paths.patchingWorkingDir().deleteRecursively()) if (!paths.patchingWorkingDir().deleteRecursively())
throw IllegalStateException("Failed to delete patching working dir") throw IllegalStateException("Failed to delete patching working dir")
} }

View file

@ -46,6 +46,8 @@ class InstallStep(private val options: PatchOptions) : Step(), KoinComponent {
title = R.string.notif_install_ready_title, title = R.string.notif_install_ready_title,
description = R.string.notif_install_ready_desc, description = R.string.notif_install_ready_desc,
) )
container.log("Waiting until manager is resumed to continue installation")
} }
// Wait until app resumed // Wait until app resumed
@ -53,24 +55,32 @@ class InstallStep(private val options: PatchOptions) : Step(), KoinComponent {
// Show [PlayProtectDialog] and wait until it gets dismissed // Show [PlayProtectDialog] and wait until it gets dismissed
if (!context.isPackageInstalled(options.packageName) && context.isPlayProtectEnabled() == true) { if (!context.isPackageInstalled(options.packageName) && context.isPlayProtectEnabled() == true) {
container.log("Showing play protect warning dialog")
overlays.startComposableForResult { callback -> overlays.startComposableForResult { callback ->
PlayProtectDialog(onDismiss = { callback(Unit) }) PlayProtectDialog(onDismiss = { callback(Unit) })
} }
} }
container.log("Installing ${apk.absolutePath}, silent: ${!prefs.devMode}")
val result = installers.getActiveInstaller().waitInstall( val result = installers.getActiveInstaller().waitInstall(
apks = listOf(apk), apks = listOf(apk),
silent = !prefs.devMode, silent = !prefs.devMode,
) )
when (result) { when (result) {
is InstallerResult.Error -> throw Error("Failed to install APKs: ${result.getDebugReason()}") is InstallerResult.Error -> {
container.log("Installation failed")
throw Error("Failed to install APKs: ${result.getDebugReason()}")
}
is InstallerResult.Cancelled -> { is InstallerResult.Cancelled -> {
// The install screen is automatically closed immediately once cleanup finishes // The install screen is automatically closed immediately once cleanup finishes
state = StepState.Skipped state = StepState.Skipped
container.log("Installation was cancelled by user")
} }
else -> {} InstallerResult.Success ->
container.log("Installation successful")
} }
} }
} }

View file

@ -18,6 +18,7 @@ class SigningStep : Step(), KoinComponent {
override suspend fun execute(container: StepRunner) { override suspend fun execute(container: StepRunner) {
val apk = container.getStep<CopyDependenciesStep>().patchedApk val apk = container.getStep<CopyDependenciesStep>().patchedApk
container.log("Signing apk at ${apk.absolutePath}")
Signer.signApk(apk) Signer.signApk(apk)
} }
} }

View file

@ -27,10 +27,14 @@ class AddAliuhookLibsStep : Step(), KoinComponent {
ZipWriter(apk, /* append = */ true).use { patchedApk -> ZipWriter(apk, /* append = */ true).use { patchedApk ->
ZipReader(aliuhook).use { aliuhook -> ZipReader(aliuhook).use { aliuhook ->
for (libFile in arrayOf("libaliuhook.so", "libc++_shared.so", "liblsplant.so")) { for (libFile in arrayOf("libaliuhook.so", "libc++_shared.so", "liblsplant.so")) {
val bytes = aliuhook.openEntry("jni/$currentDeviceArch/$libFile")?.read() container.log("Reading aliuhook lib $libFile with arch $currentDeviceArch")
val apkLibPath = "lib/$currentDeviceArch/$libFile"
val libBytes = aliuhook.openEntry("jni/$currentDeviceArch/$libFile")?.read()
?: throw IllegalStateException("Failed to read $libFile from aliuhook aar") ?: throw IllegalStateException("Failed to read $libFile from aliuhook aar")
patchedApk.writeEntry("lib/$currentDeviceArch/$libFile", bytes, ZipCompression.NONE) container.log("Writing to $apkLibPath in APK unaligned uncompressed")
patchedApk.writeEntry(apkLibPath, libBytes, ZipCompression.NONE)
} }
} }
} }

View file

@ -20,10 +20,12 @@ class PatchManifestStep(private val options: PatchOptions) : Step() {
override suspend fun execute(container: StepRunner) { override suspend fun execute(container: StepRunner) {
val apk = container.getStep<CopyDependenciesStep>().patchedApk val apk = container.getStep<CopyDependenciesStep>().patchedApk
container.log("Reading manifest from apk")
val manifest = ZipReader(apk) val manifest = ZipReader(apk)
.use { zip -> zip.openEntry("AndroidManifest.xml")?.read() } .use { zip -> zip.openEntry("AndroidManifest.xml")?.read() }
?: throw IllegalArgumentException("No manifest found in APK") ?: throw IllegalArgumentException("No manifest found in APK")
container.log("Patching manifest")
val patchedManifest = ManifestPatcher.patchManifest( val patchedManifest = ManifestPatcher.patchManifest(
manifestBytes = manifest, manifestBytes = manifest,
packageName = options.packageName, packageName = options.packageName,
@ -31,6 +33,7 @@ class PatchManifestStep(private val options: PatchOptions) : Step() {
debuggable = options.debuggable, debuggable = options.debuggable,
) )
container.log("Writing patched manifest to apk unaligned compressed")
ZipWriter(apk, /* append = */ true).use { ZipWriter(apk, /* append = */ true).use {
it.deleteEntry("AndroidManifest.xml") it.deleteEntry("AndroidManifest.xml")
it.writeEntry("AndroidManifest.xml", patchedManifest) it.writeEntry("AndroidManifest.xml", patchedManifest)

View file

@ -26,17 +26,24 @@ class ReorganizeDexStep : Step(), KoinComponent {
.filter { it.dexPriority > 0 } .filter { it.dexPriority > 0 }
.sumOf { it.dexCount } .sumOf { it.dexCount }
container.log("dexProviders: " + dexProviders.joinToString { it.javaClass.simpleName })
container.log("priorityDexCount: $priorityDexCount")
var dexCount = 0 var dexCount = 0
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") }
container.log("Existing dex files in apk: $dexCount")
// 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,
// to ensure there's space for our higher priority dex files
container.log("Copying dex files out of apk to be moved to a lesser priority")
for (idx in 0..<priorityDexCount) { for (idx in 0..<priorityDexCount) {
// Not enough dex files to move // Not enough dex files to move
if (idx + 1 > dexCount) break if (idx + 1 > dexCount) break
container.log("Extracting ${getDexName(idx)} from apk")
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) file.writeBytes(bytes)
@ -44,11 +51,13 @@ class ReorganizeDexStep : Step(), KoinComponent {
} }
ZipWriter(apk, /* append = */ true).use { zip -> ZipWriter(apk, /* append = */ true).use { zip ->
container.log("Deleting dex files to be replaced with a higher priorty dex file")
// Delete all the old dex files from the apk // Delete all the old dex files from the apk
for (idx in 0..<priorityDexCount) { for (idx in 0..<priorityDexCount) {
// Not enough dex files to move // Not enough dex files to move
if (idx + 1 > dexCount) break if (idx + 1 > dexCount) break
container.log("Deleting ${getDexName(idx)} from apk")
zip.deleteEntry(getDexName(idx)) zip.deleteEntry(getDexName(idx))
} }
@ -57,7 +66,13 @@ class ReorganizeDexStep : Step(), KoinComponent {
for (dexProvider in dexProviders) { for (dexProvider in dexProviders) {
if (dexProvider.dexPriority <= 0) continue if (dexProvider.dexPriority <= 0) continue
container.log(
"Writing custom high priority dex files from step: " +
"${dexProvider.javaClass.simpleName} with priority of ${dexProvider.dexPriority}"
)
for (dexBytes in dexProvider.getDexFiles()) { for (dexBytes in dexProvider.getDexFiles()) {
container.log("Writing dex file ${getDexName(idx)} unaligned uncompressed")
zip.writeEntry(getDexName(idx++), dexBytes, ZipCompression.NONE) zip.writeEntry(getDexName(idx++), dexBytes, ZipCompression.NONE)
} }
} }
@ -67,6 +82,8 @@ class ReorganizeDexStep : Step(), KoinComponent {
// Not enough dex files to move // Not enough dex files to move
if (idx + 1 > dexCount) break if (idx + 1 > dexCount) break
container.log("Moving old low priority dex file back into apk unaligned uncompressed: " + getDexName(dexCount + idx))
val file = paths.patchingWorkingDir().resolve(getDexName(idx)) val file = paths.patchingWorkingDir().resolve(getDexName(idx))
val bytes = file.readBytes() val bytes = file.readBytes()
zip.writeEntry(getDexName(dexCount + idx), bytes, ZipCompression.NONE) zip.writeEntry(getDexName(dexCount + idx), bytes, ZipCompression.NONE)
@ -78,7 +95,13 @@ class ReorganizeDexStep : Step(), KoinComponent {
for (dexProvider in dexProviders) { for (dexProvider in dexProviders) {
if (dexProvider.dexPriority > 0) continue if (dexProvider.dexPriority > 0) continue
container.log(
"Writing remaining low priority dex files into apk from step:" +
"${dexProvider.javaClass.simpleName} with priority of ${dexProvider.dexPriority}"
)
for (dexBytes in dexProvider.getDexFiles()) { for (dexBytes in dexProvider.getDexFiles()) {
container.log("Writing dex file ${getDexName(idx)} unaligned uncompressed")
zip.writeEntry(getDexName(dexCount++), dexBytes, ZipCompression.NONE) zip.writeEntry(getDexName(dexCount++), dexBytes, ZipCompression.NONE)
} }
} }

View file

@ -41,18 +41,22 @@ class ReplaceIconStep(private val options: PatchOptions) : Step(), KoinComponent
override suspend fun execute(container: StepRunner) { override suspend fun execute(container: StepRunner) {
val isAdaptiveIconsAvailable = Build.VERSION.SDK_INT >= 28 val isAdaptiveIconsAvailable = Build.VERSION.SDK_INT >= 28
val isMonochromeIconsAvailable = Build.VERSION.SDK_INT >= 31 val isMonochromeIconsAvailable = Build.VERSION.SDK_INT >= 31
container.log("isAdaptiveIconsAvailable: $isAdaptiveIconsAvailable, isMonochromeIconsAvailable: $isMonochromeIconsAvailable")
// Adaptive icons are available starting with Android 8 // Adaptive icons are available starting with Android 8
// Monochrome icons are always patched starting with Android 12 // Monochrome icons are always patched starting with Android 12
// Skip if adaptive icons are unavailable or { not patching main icon AND monochrome icons are unavailable } // Skip if adaptive icons are unavailable or { not patching main icon AND monochrome icons are unavailable }
if (!isAdaptiveIconsAvailable || (!isMonochromeIconsAvailable && options.iconReplacement is IconReplacement.Original)) { if (!isAdaptiveIconsAvailable || (!isMonochromeIconsAvailable && options.iconReplacement is IconReplacement.Original)) {
container.log("No patching necessary, skipping step")
state = StepState.Skipped state = StepState.Skipped
return return
} }
container.log("Parsing resources.arsc")
val apk = container.getStep<CopyDependenciesStep>().patchedApk val apk = container.getStep<CopyDependenciesStep>().patchedApk
val arsc = ArscUtil.readArsc(apk) val arsc = ArscUtil.readArsc(apk)
container.log("Parsing AndroidManifest.xml and obtaining adaptive square/round icon file paths")
val iconRscIds = AxmlUtil.readManifestIconInfo(apk) val iconRscIds = AxmlUtil.readManifestIconInfo(apk)
val squareIconFile = arsc.getMainArscChunk().getResourceFileName(iconRscIds.squareIcon, "anydpi-v26") val squareIconFile = arsc.getMainArscChunk().getResourceFileName(iconRscIds.squareIcon, "anydpi-v26")
val roundIconFile = arsc.getMainArscChunk().getResourceFileName(iconRscIds.roundIcon, "anydpi-v26") val roundIconFile = arsc.getMainArscChunk().getResourceFileName(iconRscIds.roundIcon, "anydpi-v26")
@ -63,6 +67,8 @@ class ReplaceIconStep(private val options: PatchOptions) : Step(), KoinComponent
// Add the monochrome resource and add the resource file later // Add the monochrome resource and add the resource file later
if (isMonochromeIconsAvailable) { if (isMonochromeIconsAvailable) {
container.log("Adding monochrome icon resource to arsc")
val filePathIdx = arsc.getMainArscChunk().stringPool val filePathIdx = arsc.getMainArscChunk().stringPool
.addString("res/ic_aliucord_monochrome.xml") .addString("res/ic_aliucord_monochrome.xml")
@ -77,12 +83,16 @@ class ReplaceIconStep(private val options: PatchOptions) : Step(), KoinComponent
// Add a new color resource to use // Add a new color resource to use
if (options.iconReplacement is IconReplacement.CustomColor) { if (options.iconReplacement is IconReplacement.CustomColor) {
container.log("Adding icon color resource to arsc")
backgroundIcon = arsc.getPackageChunk() backgroundIcon = arsc.getPackageChunk()
.addColorResource("aliucord", options.iconReplacement.color) .addColorResource("icon_background_replacement", options.iconReplacement.color)
} }
// Add a new mipmap resource and the file to be added later // Add a new mipmap resource and the file to be added later
if (options.iconReplacement is IconReplacement.CustomImage) { if (options.iconReplacement is IconReplacement.CustomImage) {
container.log("Adding custom icon foreground to arsc")
val iconPathIdx = arsc.getMainArscChunk().stringPool val iconPathIdx = arsc.getMainArscChunk().stringPool
.addString("res/ic_foreground_replacement.png") .addString("res/ic_foreground_replacement.png")
@ -96,6 +106,7 @@ class ReplaceIconStep(private val options: PatchOptions) : Step(), KoinComponent
} }
for (rscFile in setOf(squareIconFile, roundIconFile)) { // setOf to not possibly patch same file twice for (rscFile in setOf(squareIconFile, roundIconFile)) { // setOf to not possibly patch same file twice
container.log("Patching and writing adaptive icon AXML at $rscFile")
AxmlUtil.patchAdaptiveIcon( AxmlUtil.patchAdaptiveIcon(
apk = apk, apk = apk,
resourcePath = rscFile, resourcePath = rscFile,
@ -105,16 +116,21 @@ class ReplaceIconStep(private val options: PatchOptions) : Step(), KoinComponent
) )
} }
container.log("Writing other patched files back to apk")
ZipWriter(apk, /* append = */ true).use { ZipWriter(apk, /* append = */ true).use {
if (isMonochromeIconsAvailable) { if (isMonochromeIconsAvailable) {
container.log("Writing monochrome icon AXML to apk")
it.writeEntry("res/ic_aliucord_monochrome.xml", context.getResBytes(R.drawable.ic_discord_monochrome)) it.writeEntry("res/ic_aliucord_monochrome.xml", context.getResBytes(R.drawable.ic_discord_monochrome))
} }
if (options.iconReplacement is IconReplacement.CustomImage) { if (options.iconReplacement is IconReplacement.CustomImage) {
container.log("Writing custom icon foreground to apk")
it.writeEntry("res/ic_foreground_replacement.png", options.iconReplacement.imageBytes) it.writeEntry("res/ic_foreground_replacement.png", options.iconReplacement.imageBytes)
} }
container.log("Writing resources unaligned compressed")
it.deleteEntry("resources.arsc") it.deleteEntry("resources.arsc")
// This doesn't need to be aligned and uncompressed here, since it that is done during AlignmentSte
it.writeEntry("resources.arsc", arsc.toByteArray()) it.writeEntry("resources.arsc", arsc.toByteArray())
} }
} }

View file

@ -12,20 +12,20 @@ import com.aliucord.manager.ui.screens.patchopts.PatchOptions
import com.aliucord.manager.util.IS_CUSTOM_BUILD import com.aliucord.manager.util.IS_CUSTOM_BUILD
import com.github.diamondminer88.zip.ZipWriter import com.github.diamondminer88.zip.ZipWriter
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
/** /**
* Store the install options and additional data inside the APK for future use, * Store the install options and additional data inside the APK for future use,
* for example checking what library versions were used, or performing "updates" while * for example checking what library versions were used, or performing "updates" while
* maintaining the same install options as what was used upon first install. * maintaining the same install options as what was used upon first install.
*/ */
class SaveMetadataStep(private val options: PatchOptions) : Step() { class SaveMetadataStep(private val options: PatchOptions) : Step(), KoinComponent {
private val json: Json by inject()
override val group = StepGroup.Patch override val group = StepGroup.Patch
override val localizedName = R.string.patch_step_save_metadata override val localizedName = R.string.patch_step_save_metadata
private val json = Json {
prettyPrint = true
}
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.getStep<DownloadAliuhookStep>()
@ -41,6 +41,7 @@ class SaveMetadataStep(private val options: PatchOptions) : Step() {
patchesVersion = patches.targetVersion, patchesVersion = patches.targetVersion,
) )
container.log("Writing serialized install metadata to APK")
ZipWriter(apk, /* append = */ true).use { ZipWriter(apk, /* append = */ true).use {
it.writeEntry("aliucord.json", json.encodeToString<InstallMetadata>(metadata)) it.writeEntry("aliucord.json", json.encodeToString<InstallMetadata>(metadata))
} }

View file

@ -39,8 +39,10 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
val patches = mutableListOf<LoadedPatch>() val patches = mutableListOf<LoadedPatch>()
// Load and parse all the patches from the smali patch archive // Load and parse all the patches from the smali patch archive
container.log("Loading patches from smali patch archive: ${patchesZip.absolutePath}")
ZipReader(patchesZip).use { zip -> ZipReader(patchesZip).use { zip ->
for (patchFile in zip.entryNames) { for (patchFile in zip.entryNames) {
container.log("Parsing patch file $patchFile")
if (!patchFile.endsWith(".patch")) continue if (!patchFile.endsWith(".patch")) continue
val lines = zip.openEntry(patchFile)!!.read() val lines = zip.openEntry(patchFile)!!.read()
@ -55,6 +57,7 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
patch = UnifiedDiffUtils.parseUnifiedDiff(lines), patch = UnifiedDiffUtils.parseUnifiedDiff(lines),
) )
patches.add(patch) patches.add(patch)
container.log("Loaded patch file for class ${patch.fullClassName}")
} catch (t: Throwable) { } catch (t: Throwable) {
throw Error("Failed to parse patch file $patchFile", t) throw Error("Failed to parse patch file $patchFile", t)
} }
@ -62,9 +65,11 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
} }
// Disassemble all the classes we have patches for from all the dex files // Disassemble all the classes we have patches for from all the dex files
container.log("Disassembling target classes in APK")
ZipReader(apk).use { zip -> ZipReader(apk).use { zip ->
for (file in zip.entryNames) { for (file in zip.entryNames) {
if (!file.endsWith(".dex")) continue if (!file.endsWith(".dex")) continue
container.log("Disassembling dex $file")
val dexFile = try { val dexFile = try {
DexBackedDexFile( DexBackedDexFile(
@ -88,11 +93,15 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
} }
assert(result) { "Failed to disassemble dex $file (unknown reason)" } assert(result) { "Failed to disassemble dex $file (unknown reason)" }
container.log("Disassembled dex file for potential target classes")
} }
} }
// Apply all the patches to the smali files // Apply all the patches to the smali files
container.log("Applying smali patches to disassembled files")
for ((fullClassName, patch) in patches) { for ((fullClassName, patch) in patches) {
container.log("Applying patch to class $fullClassName")
val smaliFile = smaliDir.resolve("$fullClassName.smali") val smaliFile = smaliDir.resolve("$fullClassName.smali")
if (!smaliFile.exists()) { if (!smaliFile.exists()) {
throw FileNotFoundException("Target smali file $fullClassName.smali not found for patching!") throw FileNotFoundException("Target smali file $fullClassName.smali not found for patching!")
@ -110,6 +119,7 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
} }
// Assemble the patched classes back into a single dex // Assemble the patched classes back into a single dex
container.log("Reassembling patches smali classes into new dex")
smaliDir.mkdir() smaliDir.mkdir()
Smali.assemble( Smali.assemble(
SmaliOptions().apply { SmaliOptions().apply {

View file

@ -29,21 +29,28 @@ class DowngradeCheckStep(private val options: PatchOptions) : Step(), KoinCompon
override val localizedName = R.string.patch_step_downgrade_check override val localizedName = R.string.patch_step_downgrade_check
override suspend fun execute(container: StepRunner) { override suspend fun execute(container: StepRunner) {
container.log("Fetching version of package ${options.packageName}")
val (_, currentVersion) = try { val (_, currentVersion) = try {
context.getPackageVersion(options.packageName) context.getPackageVersion(options.packageName)
} }
// Package is not installed // Package is not installed
catch (_: NameNotFoundException) { catch (_: NameNotFoundException) {
state = StepState.Skipped state = StepState.Skipped
container.log("Package not uninstalled, skipping check")
return return
} }
container.log("Version of installed Discord app: $currentVersion")
val targetVersion = container val targetVersion = container
.getStep<FetchInfoStep>() .getStep<FetchInfoStep>()
.data.discordVersionCode.toIntOrNull() .data.discordVersionCode.toIntOrNull()
?: throw IllegalArgumentException("Invalid remote target Discord version") ?: throw IllegalArgumentException("Invalid remote target Discord version")
container.log("Target discord version: $targetVersion")
if (currentVersion > targetVersion) { if (currentVersion > targetVersion) {
container.log("Current installed version is greater than target, forcing uninstallation")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
context.showToast(R.string.installer_uninstall_new) context.showToast(R.string.installer_uninstall_new)
} }

View file

@ -25,6 +25,8 @@ class FetchInfoStep : Step(), KoinComponent {
private set private set
override suspend fun execute(container: StepRunner) { override suspend fun execute(container: StepRunner) {
container.log("Fetching ${AliucordGithubService.DATA_JSON_URL}")
data = github.getDataJson().getOrThrow() data = github.getDataJson().getOrThrow()
container.log("Fetched build data: $data")
} }
} }

View file

@ -86,7 +86,7 @@ fun LogScreenContent(
item("ENVIRONMENT_INFO") { item("ENVIRONMENT_INFO") {
PatchOption( PatchOption(
name = stringResource(R.string.log_section_env_info), name = stringResource(R.string.log_section_env_info),
description = stringResource(R.string.log_section_env_info_desc), description = null,
) { ) {
SelectableTextArea( SelectableTextArea(
text = data.environmentInfo, text = data.environmentInfo,
@ -109,17 +109,17 @@ fun LogScreenContent(
} }
} }
// item("LOG") { item("LOG") {
// PatchOption( PatchOption(
// name = stringResource(R.string.log_section_log), name = stringResource(R.string.log_section_log),
// description = null, description = null,
// ) { ) {
// SelectableTextArea( SelectableTextArea(
// text = data.installationLog, text = data.installationLog,
// modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
// ) )
// } }
// } }
} }
} }
} }

View file

@ -245,7 +245,7 @@ class PatchingScreen(private val data: PatchOptions) : Screen, Parcelable {
MainActionButton( MainActionButton(
text = stringResource(R.string.action_retry_install), text = stringResource(R.string.action_retry_install),
icon = painterResource(R.drawable.ic_refresh), icon = painterResource(R.drawable.ic_refresh),
onClick = model::launchApp, onClick = model::install,
) )
MainActionButton( MainActionButton(

View file

@ -51,7 +51,7 @@ class PatchingScreenModel(
private set private set
init { init {
restart() install()
// Rotate fun facts every so often // Rotate fun facts every so often
screenModelScope.launch { screenModelScope.launch {
@ -81,7 +81,7 @@ class PatchingScreenModel(
application.showToast(R.string.action_cleared_cache) application.showToast(R.string.action_cleared_cache)
} }
fun restart() { fun install() = screenModelScope.launch {
runnerJob?.cancel("Manual cancellation") runnerJob?.cancel("Manual cancellation")
steps = null steps = null
@ -105,6 +105,7 @@ class PatchingScreenModel(
id = installId!!, id = installId!!,
installDate = startTime!!, installDate = startTime!!,
options = options, options = options,
log = "- Failed to initialize patch runner",
error = error, error = error,
) )
} }
@ -124,9 +125,9 @@ class PatchingScreenModel(
.mapValues { it.value.toUnsafeImmutable() } .mapValues { it.value.toUnsafeImmutable() }
.toUnsafeImmutable() .toUnsafeImmutable()
// Intentionally delay to show the state change of the first step in UI when it runs // Intentionally delay to show the state change of the first step when it runs in the UI.
// without it, on a fast internet it just immediately shows as "Success" // Without this, on a fast internet connection the step just immediately shows as "Success".
delay(600) delay(400)
// Execute all the steps and catch any errors // Execute all the steps and catch any errors
when (val error = runner.executeAll()) { when (val error = runner.executeAll()) {
@ -143,6 +144,7 @@ class PatchingScreenModel(
id = installId!!, id = installId!!,
installDate = startTime!!, installDate = startTime!!,
options = options, options = options,
log = runner.getLog(),
error = null, error = null,
) )
} }
@ -155,6 +157,7 @@ class PatchingScreenModel(
id = installId!!, id = installId!!,
installDate = startTime!!, installDate = startTime!!,
options = options, options = options,
log = runner.getLog(),
error = error, error = error,
) )
} }

View file

@ -188,7 +188,6 @@
<string name="log_title">Log</string> <string name="log_title">Log</string>
<string name="log_section_install_info">Installation Info</string> <string name="log_section_install_info">Installation Info</string>
<string name="log_section_env_info">Environment Info</string> <string name="log_section_env_info">Environment Info</string>
<string name="log_section_env_info_desc">Information about your device that may be crucial for debugging.</string>
<string name="log_section_error">Error Stacktrace</string> <string name="log_section_error">Error Stacktrace</string>
<string name="log_section_log">Installation Log</string> <string name="log_section_log">Installation Log</string>
<string name="log_action_export">Export log</string> <string name="log_action_export">Export log</string>