diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index c96e796..15bead7 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -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 diff --git a/app/src/main/kotlin/com/aliucord/manager/manager/InstallLogManager.kt b/app/src/main/kotlin/com/aliucord/manager/manager/InstallLogManager.kt index 813f230..1855564 100644 --- a/app/src/main/kotlin/com/aliucord/manager/manager/InstallLogManager.kt +++ b/app/src/main/kotlin/com/aliucord/manager/manager/InstallLogManager.kt @@ -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() diff --git a/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordGithubService.kt b/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordGithubService.kt index 84ee4b6..a29775c 100644 --- a/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordGithubService.kt +++ b/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordGithubService.kt @@ -12,9 +12,7 @@ class AliucordGithubService( ) { suspend fun getDataJson(): ApiResponse { 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" } } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/StepRunner.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/StepRunner.kt index ed13d38..014627b 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/StepRunner.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/StepRunner.kt @@ -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 = mutableListOf() + abstract val steps: ImmutableList /** @@ -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.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 } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadStep.kt index c0225e4..e458fd0 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/base/DownloadStep.kt @@ -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") } } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/CopyDependenciesStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/CopyDependenciesStep.kt index 2493645..50523f2 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/CopyDependenciesStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/CopyDependenciesStep.kt @@ -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().targetFile + container.log("Copying patched apk from ${srcApk.absolutePath} to ${patchedApk.absolutePath}") srcApk.copyTo(patchedApk) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadAliuhookStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadAliuhookStep.kt index 142e496..87e94a1 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadAliuhookStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadAliuhookStep.kt @@ -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) } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadInjectorStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadInjectorStep.kt index b3872dd..3c41fc4 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadInjectorStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadInjectorStep.kt @@ -38,11 +38,12 @@ class DownloadInjectorStep : DownloadStep(), IDexProvider, KoinComponent { get() = paths.cachedInjectorDex(targetVersion, custom = isCustomVersion) override suspend fun execute(container: StepRunner) { - var customVersions = mutableStateListOf() + val customVersions = mutableStateListOf() .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() .data.injectorVersion + container.log("Downloading injector version $targetVersion") super.execute(container) } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadPatchesStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadPatchesStep.kt index b09d538..a5889e3 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadPatchesStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/DownloadPatchesStep.kt @@ -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() + val customVersions = mutableStateListOf() .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() .data.patchesVersion + container.log("Downloading patches version $targetVersion") super.execute(container) } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/install/AlignmentStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/install/AlignmentStep.kt index ebdaaa8..ad4eed5 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/install/AlignmentStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/install/AlignmentStep.kt @@ -25,11 +25,11 @@ class AlignmentStep : Step(), KoinComponent { val apk = container.getStep().patchedApk var resourcesArscBytes: ByteArray? = null - var nativeLibPaths: List? = 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.. 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") } } } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/install/SigningStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/install/SigningStep.kt index 834d468..493dbe8 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/install/SigningStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/install/SigningStep.kt @@ -18,6 +18,7 @@ class SigningStep : Step(), KoinComponent { override suspend fun execute(container: StepRunner) { val apk = container.getStep().patchedApk + container.log("Signing apk at ${apk.absolutePath}") Signer.signApk(apk) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/AddAliuhookLibsStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/AddAliuhookLibsStep.kt index 9b0636a..ea7ab59 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/AddAliuhookLibsStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/AddAliuhookLibsStep.kt @@ -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) } } } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/PatchManifestStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/PatchManifestStep.kt index 57afd95..af45e17 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/PatchManifestStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/PatchManifestStep.kt @@ -20,10 +20,12 @@ class PatchManifestStep(private val options: PatchOptions) : Step() { override suspend fun execute(container: StepRunner) { val apk = container.getStep().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) diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/ReorganizeDexStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/ReorganizeDexStep.kt index e4ba288..f1e397d 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/ReorganizeDexStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/ReorganizeDexStep.kt @@ -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.. 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.. 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) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/ReplaceIconStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/ReplaceIconStep.kt index 5f5e09b..b512f75 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/ReplaceIconStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/ReplaceIconStep.kt @@ -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().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()) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SaveMetadataStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SaveMetadataStep.kt index f5df94b..345097a 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SaveMetadataStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SaveMetadataStep.kt @@ -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().patchedApk val aliuhook = container.getStep() @@ -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(metadata)) } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SmaliPatchStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SmaliPatchStep.kt index 99c0bce..8135e62 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SmaliPatchStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/SmaliPatchStep.kt @@ -39,8 +39,10 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent { val patches = mutableListOf() // 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 { diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/DowngradeCheckStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/DowngradeCheckStep.kt index d88c63d..b988935 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/DowngradeCheckStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/DowngradeCheckStep.kt @@ -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() .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) } diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/FetchInfoStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/FetchInfoStep.kt index 886461e..c1cf998 100644 --- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/FetchInfoStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/prepare/FetchInfoStep.kt @@ -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") } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/log/LogScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/log/LogScreen.kt index 6b4c3f7..61b79c0 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/log/LogScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/log/LogScreen.kt @@ -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(), + ) + } + } } } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreen.kt index 843cc28..0432346 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreen.kt @@ -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( diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreenModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreenModel.kt index 6f97ab4..c3df323 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreenModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/patching/PatchingScreenModel.kt @@ -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, ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 61bb47f..9327a41 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -188,7 +188,6 @@ Log Installation Info Environment Info - Information about your device that may be crucial for debugging. Error Stacktrace Installation Log Export log