feat(patching): add detailed logging to each step
This commit is contained in:
parent
d64a1767fc
commit
96a463349c
25 changed files with 220 additions and 48 deletions
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue