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;
}
# 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
# debugging stack traces.
-keepattributes SourceFile,LineNumberTable

View file

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

View file

@ -12,9 +12,7 @@ class AliucordGithubService(
) {
suspend fun getDataJson(): ApiResponse<BuildInfo> {
return withContext(Dispatchers.IO) {
http.request {
url("https://raw.githubusercontent.com/$ORG/$MAIN_REPO/builds/data.json")
}
http.request { url(DATA_JSON_URL) }
}
}
@ -25,6 +23,7 @@ class AliucordGithubService(
const val MAIN_REPO = "Aliucord"
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"
}
}

View file

@ -1,8 +1,10 @@
package com.aliucord.manager.patcher
import android.content.Context
import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R
import com.aliucord.manager.manager.PreferencesManager
import com.aliucord.manager.patcher.steps.StepGroup
@ -30,6 +32,8 @@ abstract class StepRunner : KoinComponent {
private val context: Context by inject()
private val preferences: PreferencesManager by inject()
private val logEntries: MutableList<String> = mutableListOf()
abstract val steps: ImmutableList<Step>
/**
@ -50,14 +54,36 @@ abstract class StepRunner : KoinComponent {
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? {
log("Starting step runner")
log("Registered steps: " + steps.joinToString { it.javaClass.simpleName })
for (step in steps) {
val stepName = step.javaClass.simpleName
log("Running step: $stepName")
val error = step.executeCatching(this@StepRunner)
if (error != null) {
log("Failed on step: $stepName after ${step.getDuration()}ms")
showErrorNotification()
// If this is a patch step and it failed, then clear download cache just in case
if (step.group == StepGroup.Patch && !preferences.devMode) {
log("Deleting download cache")
for (downloadStep in steps.asSequence().filterIsInstance<DownloadStep>()) {
downloadStep.targetFile.delete()
}
@ -71,8 +97,12 @@ abstract class StepRunner : KoinComponent {
if (!preferences.devMode && duration < MINIMUM_STEP_DELAY) {
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
}

View file

@ -1,6 +1,7 @@
package com.aliucord.manager.patcher.steps.base
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.annotation.CallSuper
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.steps.StepGroup
import com.aliucord.manager.util.showToast
import com.aliucord.manager.util.toPrecision
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
@ -48,24 +50,43 @@ abstract class DownloadStep : Step(), KoinComponent {
override val group = StepGroup.Download
override suspend fun execute(container: StepRunner) {
if (targetFile.exists()) {
if (targetFile.length() > 0) {
state = StepState.Skipped
return
}
container.log("Checking if file cached: ${targetFile.absolutePath}")
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 ->
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) {
is IDownloadManager.Result.Cancelled ->
is IDownloadManager.Result.Cancelled -> {
state = StepState.Error
container.log("Download cancelled!")
}
is IDownloadManager.Result.Success -> {
container.log("Successfully downloaded file, verifying...")
try {
verify()
} catch (t: Throwable) {
@ -73,9 +94,12 @@ abstract class DownloadStep : Step(), KoinComponent {
context.showToast(R.string.installer_dl_verify_fail)
}
container.log("Failed to verify file, deleting...")
targetFile.delete()
throw t
}
container.log("Verified downloaded file")
}
is IDownloadManager.Result.Error -> {
@ -86,6 +110,7 @@ abstract class DownloadStep : Step(), KoinComponent {
Toast.makeText(context, toastText, Toast.LENGTH_LONG).show()
}
container.log("Failed to download file")
throw Error("Failed to download: $result")
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -25,11 +25,11 @@ class AlignmentStep : Step(), KoinComponent {
val apk = container.getStep<CopyDependenciesStep>().patchedApk
var resourcesArscBytes: ByteArray? = null
var nativeLibPaths: List<String>? = 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")
@ -37,6 +37,7 @@ class AlignmentStep : Step(), KoinComponent {
// 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") }
@ -48,42 +49,54 @@ class AlignmentStep : Step(), KoinComponent {
file.writeBytes(bytes)
}
}
container.log("Extracted $dexCount dex files")
}
// 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") }
// Extract to disk temporarily
for ((idx, path) in libPaths.withIndex()) {
// 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
}
// 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
}
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 native libraries: $nativeLibPaths")
for (path in nativeLibPaths)
zip.deleteEntry(path)
// 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)
}
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()
@ -98,6 +111,8 @@ class AlignmentStep : Step(), KoinComponent {
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)
}
}

View file

@ -22,8 +22,10 @@ class CleanupStep : Step(), KoinComponent {
override suspend fun execute(container: StepRunner) {
if (prefs.keepPatchedApks) {
container.log("keepPatchedApks enabled, skipping...")
state = StepState.Skipped
} else {
container.log("Deleting patching working dir")
if (!paths.patchingWorkingDir().deleteRecursively())
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,
description = R.string.notif_install_ready_desc,
)
container.log("Waiting until manager is resumed to continue installation")
}
// Wait until app resumed
@ -53,24 +55,32 @@ class InstallStep(private val options: PatchOptions) : Step(), KoinComponent {
// Show [PlayProtectDialog] and wait until it gets dismissed
if (!context.isPackageInstalled(options.packageName) && context.isPlayProtectEnabled() == true) {
container.log("Showing play protect warning dialog")
overlays.startComposableForResult { callback ->
PlayProtectDialog(onDismiss = { callback(Unit) })
}
}
container.log("Installing ${apk.absolutePath}, silent: ${!prefs.devMode}")
val result = installers.getActiveInstaller().waitInstall(
apks = listOf(apk),
silent = !prefs.devMode,
)
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 -> {
// The install screen is automatically closed immediately once cleanup finishes
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) {
val apk = container.getStep<CopyDependenciesStep>().patchedApk
container.log("Signing apk at ${apk.absolutePath}")
Signer.signApk(apk)
}
}

View file

@ -27,10 +27,14 @@ class AddAliuhookLibsStep : Step(), KoinComponent {
ZipWriter(apk, /* append = */ true).use { patchedApk ->
ZipReader(aliuhook).use { aliuhook ->
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")
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) {
val apk = container.getStep<CopyDependenciesStep>().patchedApk
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 = ManifestPatcher.patchManifest(
manifestBytes = manifest,
packageName = options.packageName,
@ -31,6 +33,7 @@ class PatchManifestStep(private val options: PatchOptions) : Step() {
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)

View file

@ -26,17 +26,24 @@ class ReorganizeDexStep : Step(), KoinComponent {
.filter { it.dexPriority > 0 }
.sumOf { it.dexCount }
container.log("dexProviders: " + dexProviders.joinToString { it.javaClass.simpleName })
container.log("priorityDexCount: $priorityDexCount")
var dexCount = 0
ZipReader(apk).use { zip ->
// Count the amount of dex files currently in the apk
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) {
// Not enough dex files to move
if (idx + 1 > dexCount) break
container.log("Extracting ${getDexName(idx)} from apk")
val bytes = zip.openEntry(getDexName(idx))!!.read()
val file = paths.patchingWorkingDir().resolve(getDexName(idx))
file.writeBytes(bytes)
@ -44,11 +51,13 @@ class ReorganizeDexStep : Step(), KoinComponent {
}
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
for (idx in 0..<priorityDexCount) {
// Not enough dex files to move
if (idx + 1 > dexCount) break
container.log("Deleting ${getDexName(idx)} from apk")
zip.deleteEntry(getDexName(idx))
}
@ -57,7 +66,13 @@ class ReorganizeDexStep : Step(), KoinComponent {
for (dexProvider in dexProviders) {
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()) {
container.log("Writing dex file ${getDexName(idx)} unaligned uncompressed")
zip.writeEntry(getDexName(idx++), dexBytes, ZipCompression.NONE)
}
}
@ -67,6 +82,8 @@ class ReorganizeDexStep : Step(), KoinComponent {
// Not enough dex files to move
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 bytes = file.readBytes()
zip.writeEntry(getDexName(dexCount + idx), bytes, ZipCompression.NONE)
@ -78,7 +95,13 @@ class ReorganizeDexStep : Step(), KoinComponent {
for (dexProvider in dexProviders) {
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()) {
container.log("Writing dex file ${getDexName(idx)} unaligned uncompressed")
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) {
val isAdaptiveIconsAvailable = Build.VERSION.SDK_INT >= 28
val isMonochromeIconsAvailable = Build.VERSION.SDK_INT >= 31
container.log("isAdaptiveIconsAvailable: $isAdaptiveIconsAvailable, isMonochromeIconsAvailable: $isMonochromeIconsAvailable")
// Adaptive icons are available starting with Android 8
// 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 }
if (!isAdaptiveIconsAvailable || (!isMonochromeIconsAvailable && options.iconReplacement is IconReplacement.Original)) {
container.log("No patching necessary, skipping step")
state = StepState.Skipped
return
}
container.log("Parsing resources.arsc")
val apk = container.getStep<CopyDependenciesStep>().patchedApk
val arsc = ArscUtil.readArsc(apk)
container.log("Parsing AndroidManifest.xml and obtaining adaptive square/round icon file paths")
val iconRscIds = AxmlUtil.readManifestIconInfo(apk)
val squareIconFile = arsc.getMainArscChunk().getResourceFileName(iconRscIds.squareIcon, "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
if (isMonochromeIconsAvailable) {
container.log("Adding monochrome icon resource to arsc")
val filePathIdx = arsc.getMainArscChunk().stringPool
.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
if (options.iconReplacement is IconReplacement.CustomColor) {
container.log("Adding icon color resource to arsc")
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
if (options.iconReplacement is IconReplacement.CustomImage) {
container.log("Adding custom icon foreground to arsc")
val iconPathIdx = arsc.getMainArscChunk().stringPool
.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
container.log("Patching and writing adaptive icon AXML at $rscFile")
AxmlUtil.patchAdaptiveIcon(
apk = apk,
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 {
if (isMonochromeIconsAvailable) {
container.log("Writing monochrome icon AXML to apk")
it.writeEntry("res/ic_aliucord_monochrome.xml", context.getResBytes(R.drawable.ic_discord_monochrome))
}
if (options.iconReplacement is IconReplacement.CustomImage) {
container.log("Writing custom icon foreground to apk")
it.writeEntry("res/ic_foreground_replacement.png", options.iconReplacement.imageBytes)
}
container.log("Writing resources unaligned compressed")
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())
}
}

View file

@ -12,20 +12,20 @@ import com.aliucord.manager.ui.screens.patchopts.PatchOptions
import com.aliucord.manager.util.IS_CUSTOM_BUILD
import com.github.diamondminer88.zip.ZipWriter
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,
* for example checking what library versions were used, or performing "updates" while
* 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 localizedName = R.string.patch_step_save_metadata
private val json = Json {
prettyPrint = true
}
override suspend fun execute(container: StepRunner) {
val apk = container.getStep<CopyDependenciesStep>().patchedApk
val aliuhook = container.getStep<DownloadAliuhookStep>()
@ -41,6 +41,7 @@ class SaveMetadataStep(private val options: PatchOptions) : Step() {
patchesVersion = patches.targetVersion,
)
container.log("Writing serialized install metadata to APK")
ZipWriter(apk, /* append = */ true).use {
it.writeEntry("aliucord.json", json.encodeToString<InstallMetadata>(metadata))
}

View file

@ -39,8 +39,10 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
val patches = mutableListOf<LoadedPatch>()
// 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 ->
for (patchFile in zip.entryNames) {
container.log("Parsing patch file $patchFile")
if (!patchFile.endsWith(".patch")) continue
val lines = zip.openEntry(patchFile)!!.read()
@ -55,6 +57,7 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
patch = UnifiedDiffUtils.parseUnifiedDiff(lines),
)
patches.add(patch)
container.log("Loaded patch file for class ${patch.fullClassName}")
} catch (t: Throwable) {
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
container.log("Disassembling target classes in APK")
ZipReader(apk).use { zip ->
for (file in zip.entryNames) {
if (!file.endsWith(".dex")) continue
container.log("Disassembling dex $file")
val dexFile = try {
DexBackedDexFile(
@ -88,11 +93,15 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
}
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
container.log("Applying smali patches to disassembled files")
for ((fullClassName, patch) in patches) {
container.log("Applying patch to class $fullClassName")
val smaliFile = smaliDir.resolve("$fullClassName.smali")
if (!smaliFile.exists()) {
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
container.log("Reassembling patches smali classes into new dex")
smaliDir.mkdir()
Smali.assemble(
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 suspend fun execute(container: StepRunner) {
container.log("Fetching version of package ${options.packageName}")
val (_, currentVersion) = try {
context.getPackageVersion(options.packageName)
}
// Package is not installed
catch (_: NameNotFoundException) {
state = StepState.Skipped
container.log("Package not uninstalled, skipping check")
return
}
container.log("Version of installed Discord app: $currentVersion")
val targetVersion = container
.getStep<FetchInfoStep>()
.data.discordVersionCode.toIntOrNull()
?: throw IllegalArgumentException("Invalid remote target Discord version")
container.log("Target discord version: $targetVersion")
if (currentVersion > targetVersion) {
container.log("Current installed version is greater than target, forcing uninstallation")
withContext(Dispatchers.Main) {
context.showToast(R.string.installer_uninstall_new)
}

View file

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

View file

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

View file

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

View file

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

View file

@ -188,7 +188,6 @@
<string name="log_title">Log</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_desc">Information about your device that may be crucial for debugging.</string>
<string name="log_section_error">Error Stacktrace</string>
<string name="log_section_log">Installation Log</string>
<string name="log_action_export">Export log</string>