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;
|
static <1>$$serializer INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Keep class names of patching steps since they're used via reflection
|
||||||
|
-keepnames com.aliucord.manager.patcher.steps.**
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
# Uncomment this to preserve the line number information for
|
||||||
# debugging stack traces.
|
# debugging stack traces.
|
||||||
-keepattributes SourceFile,LineNumberTable
|
-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import java.util.Locale
|
||||||
*/
|
*/
|
||||||
class InstallLogManager(
|
class InstallLogManager(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
|
private val prefs: PreferencesManager,
|
||||||
private val json: Json,
|
private val json: Json,
|
||||||
) {
|
) {
|
||||||
val logsDir = application.filesDir.resolve("install-logs").apply { mkdir() }
|
val logsDir = application.filesDir.resolve("install-logs").apply { mkdir() }
|
||||||
|
|
@ -51,6 +52,7 @@ class InstallLogManager(
|
||||||
id: String,
|
id: String,
|
||||||
installDate: Instant,
|
installDate: Instant,
|
||||||
options: PatchOptions,
|
options: PatchOptions,
|
||||||
|
log: String,
|
||||||
error: Throwable?,
|
error: Throwable?,
|
||||||
) {
|
) {
|
||||||
val path = logsDir.resolve("$id.json")
|
val path = logsDir.resolve("$id.json")
|
||||||
|
|
@ -60,7 +62,7 @@ class InstallLogManager(
|
||||||
installDate = installDate,
|
installDate = installDate,
|
||||||
installOptions = options,
|
installOptions = options,
|
||||||
environmentInfo = getEnvironmentInfo(),
|
environmentInfo = getEnvironmentInfo(),
|
||||||
installationLog = "", // TODO: install log
|
installationLog = log,
|
||||||
errorStacktrace = error?.let { Log.getStackTraceString(it).trimEnd() },
|
errorStacktrace = error?.let { Log.getStackTraceString(it).trimEnd() },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -83,12 +85,13 @@ class InstallLogManager(
|
||||||
return """
|
return """
|
||||||
Aliucord Manager v${BuildConfig.VERSION_NAME}
|
Aliucord Manager v${BuildConfig.VERSION_NAME}
|
||||||
Built from commit ${BuildConfig.GIT_COMMIT} on ${BuildConfig.GIT_BRANCH} $gitChanges
|
Built from commit ${BuildConfig.GIT_COMMIT} on ${BuildConfig.GIT_BRANCH} $gitChanges
|
||||||
|
Developer mode: ${if (prefs.devMode) "On" else "Off"}
|
||||||
|
|
||||||
Android API: ${Build.VERSION.SDK_INT}
|
Android API: ${Build.VERSION.SDK_INT}
|
||||||
Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()}
|
Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()}
|
||||||
ROM: Android ${Build.VERSION.RELEASE} (Patch ${Build.VERSION.SECURITY_PATCH})
|
ROM: Android ${Build.VERSION.RELEASE} (Patch ${Build.VERSION.SECURITY_PATCH})
|
||||||
Device: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE})
|
Device: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE})
|
||||||
Emulator: $IS_PROBABLY_EMULATOR (guess)
|
Emulator: ${if (IS_PROBABLY_EMULATOR) "Yes" else "No"} (guess)
|
||||||
Play Protect: $playProtect
|
Play Protect: $playProtect
|
||||||
SOC: $soc
|
SOC: $soc
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@ class AliucordGithubService(
|
||||||
) {
|
) {
|
||||||
suspend fun getDataJson(): ApiResponse<BuildInfo> {
|
suspend fun getDataJson(): ApiResponse<BuildInfo> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
http.request {
|
http.request { url(DATA_JSON_URL) }
|
||||||
url("https://raw.githubusercontent.com/$ORG/$MAIN_REPO/builds/data.json")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,6 +23,7 @@ class AliucordGithubService(
|
||||||
const val MAIN_REPO = "Aliucord"
|
const val MAIN_REPO = "Aliucord"
|
||||||
const val MANAGER_REPO = "Manager"
|
const val MANAGER_REPO = "Manager"
|
||||||
|
|
||||||
|
const val DATA_JSON_URL = "https://raw.githubusercontent.com/$ORG/$MAIN_REPO/builds/data.json"
|
||||||
const val PATCHED_APKS_INFO_URL = "https://github.com/$ORG/$MANAGER_REPO/blob/main/INFO.md#obtaining-patched-apks"
|
const val PATCHED_APKS_INFO_URL = "https://github.com/$ORG/$MANAGER_REPO/blob/main/INFO.md#obtaining-patched-apks"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package com.aliucord.manager.patcher
|
package com.aliucord.manager.patcher
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
|
import com.aliucord.manager.BuildConfig
|
||||||
import com.aliucord.manager.R
|
import com.aliucord.manager.R
|
||||||
import com.aliucord.manager.manager.PreferencesManager
|
import com.aliucord.manager.manager.PreferencesManager
|
||||||
import com.aliucord.manager.patcher.steps.StepGroup
|
import com.aliucord.manager.patcher.steps.StepGroup
|
||||||
|
|
@ -30,6 +32,8 @@ abstract class StepRunner : KoinComponent {
|
||||||
private val context: Context by inject()
|
private val context: Context by inject()
|
||||||
private val preferences: PreferencesManager by inject()
|
private val preferences: PreferencesManager by inject()
|
||||||
|
|
||||||
|
private val logEntries: MutableList<String> = mutableListOf()
|
||||||
|
|
||||||
abstract val steps: ImmutableList<Step>
|
abstract val steps: ImmutableList<Step>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -50,14 +54,36 @@ abstract class StepRunner : KoinComponent {
|
||||||
return step
|
return step
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a log entry without any associated log level.
|
||||||
|
*/
|
||||||
|
fun log(text: String) {
|
||||||
|
logEntries += text
|
||||||
|
Log.i(BuildConfig.TAG, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines all the log entries into a single formatted log.
|
||||||
|
*/
|
||||||
|
fun getLog(): String = logEntries.joinToString(separator = "\n")
|
||||||
|
|
||||||
suspend fun executeAll(): Throwable? {
|
suspend fun executeAll(): Throwable? {
|
||||||
|
log("Starting step runner")
|
||||||
|
log("Registered steps: " + steps.joinToString { it.javaClass.simpleName })
|
||||||
|
|
||||||
for (step in steps) {
|
for (step in steps) {
|
||||||
|
val stepName = step.javaClass.simpleName
|
||||||
|
|
||||||
|
log("Running step: $stepName")
|
||||||
val error = step.executeCatching(this@StepRunner)
|
val error = step.executeCatching(this@StepRunner)
|
||||||
|
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
|
log("Failed on step: $stepName after ${step.getDuration()}ms")
|
||||||
showErrorNotification()
|
showErrorNotification()
|
||||||
|
|
||||||
// If this is a patch step and it failed, then clear download cache just in case
|
// If this is a patch step and it failed, then clear download cache just in case
|
||||||
if (step.group == StepGroup.Patch && !preferences.devMode) {
|
if (step.group == StepGroup.Patch && !preferences.devMode) {
|
||||||
|
log("Deleting download cache")
|
||||||
for (downloadStep in steps.asSequence().filterIsInstance<DownloadStep>()) {
|
for (downloadStep in steps.asSequence().filterIsInstance<DownloadStep>()) {
|
||||||
downloadStep.targetFile.delete()
|
downloadStep.targetFile.delete()
|
||||||
}
|
}
|
||||||
|
|
@ -71,8 +97,12 @@ abstract class StepRunner : KoinComponent {
|
||||||
if (!preferences.devMode && duration < MINIMUM_STEP_DELAY) {
|
if (!preferences.devMode && duration < MINIMUM_STEP_DELAY) {
|
||||||
delay(MINIMUM_STEP_DELAY - duration)
|
delay(MINIMUM_STEP_DELAY - duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log("Completed step: $stepName in ${duration}ms")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log("Successfully finished all steps in ${steps.sumOf { it.getDuration() }}ms")
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.aliucord.manager.patcher.steps.base
|
package com.aliucord.manager.patcher.steps.base
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
|
@ -10,6 +11,7 @@ import com.aliucord.manager.manager.download.KtorDownloadManager
|
||||||
import com.aliucord.manager.patcher.StepRunner
|
import com.aliucord.manager.patcher.StepRunner
|
||||||
import com.aliucord.manager.patcher.steps.StepGroup
|
import com.aliucord.manager.patcher.steps.StepGroup
|
||||||
import com.aliucord.manager.util.showToast
|
import com.aliucord.manager.util.showToast
|
||||||
|
import com.aliucord.manager.util.toPrecision
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
|
@ -48,24 +50,43 @@ abstract class DownloadStep : Step(), KoinComponent {
|
||||||
override val group = StepGroup.Download
|
override val group = StepGroup.Download
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
if (targetFile.exists()) {
|
container.log("Checking if file cached: ${targetFile.absolutePath}")
|
||||||
if (targetFile.length() > 0) {
|
|
||||||
state = StepState.Skipped
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
targetFile.delete()
|
if (targetFile.exists()) {
|
||||||
|
container.log("File exists, verifying...")
|
||||||
|
|
||||||
|
try {
|
||||||
|
verify()
|
||||||
|
state = StepState.Skipped
|
||||||
|
container.log("File verified, skipping download")
|
||||||
|
return
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
targetFile.delete()
|
||||||
|
container.log("Verification error: " + Log.getStackTraceString(t))
|
||||||
|
container.log("File failed verification, deleting and redownloading")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
container.log("Downloading file at url: $targetUrl")
|
||||||
|
var lastLogProgress = 0f
|
||||||
val result = downloader.download(targetUrl, targetFile) { newProgress ->
|
val result = downloader.download(targetUrl, targetFile) { newProgress ->
|
||||||
progress = newProgress ?: -1f
|
progress = newProgress ?: -1f
|
||||||
|
|
||||||
|
if (newProgress != null && newProgress > lastLogProgress + 0.1f) {
|
||||||
|
container.log("Download progress: ${(newProgress * 100.0).toPrecision(0)}% after ${getDuration()}ms")
|
||||||
|
lastLogProgress = newProgress
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (result) {
|
when (result) {
|
||||||
is IDownloadManager.Result.Cancelled ->
|
is IDownloadManager.Result.Cancelled -> {
|
||||||
state = StepState.Error
|
state = StepState.Error
|
||||||
|
container.log("Download cancelled!")
|
||||||
|
}
|
||||||
|
|
||||||
is IDownloadManager.Result.Success -> {
|
is IDownloadManager.Result.Success -> {
|
||||||
|
container.log("Successfully downloaded file, verifying...")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
verify()
|
verify()
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
|
|
@ -73,9 +94,12 @@ abstract class DownloadStep : Step(), KoinComponent {
|
||||||
context.showToast(R.string.installer_dl_verify_fail)
|
context.showToast(R.string.installer_dl_verify_fail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
container.log("Failed to verify file, deleting...")
|
||||||
targetFile.delete()
|
targetFile.delete()
|
||||||
throw t
|
throw t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
container.log("Verified downloaded file")
|
||||||
}
|
}
|
||||||
|
|
||||||
is IDownloadManager.Result.Error -> {
|
is IDownloadManager.Result.Error -> {
|
||||||
|
|
@ -86,6 +110,7 @@ abstract class DownloadStep : Step(), KoinComponent {
|
||||||
Toast.makeText(context, toastText, Toast.LENGTH_LONG).show()
|
Toast.makeText(context, toastText, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
container.log("Failed to download file")
|
||||||
throw Error("Failed to download: $result")
|
throw Error("Failed to download: $result")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,13 @@ class CopyDependenciesStep : Step(), KoinComponent {
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
val dir = paths.patchingWorkingDir()
|
val dir = paths.patchingWorkingDir()
|
||||||
|
|
||||||
|
container.log("Clearing patched directory")
|
||||||
if (!dir.deleteRecursively())
|
if (!dir.deleteRecursively())
|
||||||
throw Error("Failed to clear existing patched dir")
|
throw Error("Failed to clear existing patched dir")
|
||||||
|
|
||||||
val srcApk = container.getStep<DownloadDiscordStep>().targetFile
|
val srcApk = container.getStep<DownloadDiscordStep>().targetFile
|
||||||
|
|
||||||
|
container.log("Copying patched apk from ${srcApk.absolutePath} to ${patchedApk.absolutePath}")
|
||||||
srcApk.copyTo(patchedApk)
|
srcApk.copyTo(patchedApk)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,9 @@ class DownloadAliuhookStep : DownloadStep(), IDexProvider, KoinComponent {
|
||||||
override val targetFile get() = paths.cachedAliuhookAAR(targetVersion)
|
override val targetFile get() = paths.cachedAliuhookAAR(targetVersion)
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
|
container.log("Obtaining latest aliuhook version")
|
||||||
targetVersion = maven.getAliuhookVersion().getOrThrow()
|
targetVersion = maven.getAliuhookVersion().getOrThrow()
|
||||||
|
container.log("Fetched aliuhook version: $targetVersion")
|
||||||
|
|
||||||
super.execute(container)
|
super.execute(container)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,12 @@ class DownloadInjectorStep : DownloadStep(), IDexProvider, KoinComponent {
|
||||||
get() = paths.cachedInjectorDex(targetVersion, custom = isCustomVersion)
|
get() = paths.cachedInjectorDex(targetVersion, custom = isCustomVersion)
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
var customVersions = mutableStateListOf<SemVer>()
|
val customVersions = mutableStateListOf<SemVer>()
|
||||||
.apply { addAll(paths.customInjectorDexs()) }
|
.apply { addAll(paths.customInjectorDexs()) }
|
||||||
|
|
||||||
// Prompt to select or manage custom versions instead of downloading
|
// Prompt to select or manage custom versions instead of downloading
|
||||||
if (customVersions.isNotEmpty()) {
|
if (customVersions.isNotEmpty()) {
|
||||||
|
container.log("Custom versions present, waiting for selection: ${customVersions.joinToString()}")
|
||||||
val selectedVersion = overlays.startComposableForResult { callback ->
|
val selectedVersion = overlays.startComposableForResult { callback ->
|
||||||
CustomComponentVersionPicker(
|
CustomComponentVersionPicker(
|
||||||
componentTitle = "Injector",
|
componentTitle = "Injector",
|
||||||
|
|
@ -63,16 +64,21 @@ class DownloadInjectorStep : DownloadStep(), IDexProvider, KoinComponent {
|
||||||
onCancel = { callback(null) },
|
onCancel = { callback(null) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedVersion != null) {
|
if (selectedVersion != null) {
|
||||||
isCustomVersion = true
|
isCustomVersion = true
|
||||||
targetVersion = selectedVersion
|
targetVersion = selectedVersion
|
||||||
state = StepState.Skipped
|
state = StepState.Skipped
|
||||||
|
container.log("Using local custom injector version $selectedVersion")
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
container.log("Selection dialog cancelled, continuing as normal")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
targetVersion = container.getStep<FetchInfoStep>()
|
targetVersion = container.getStep<FetchInfoStep>()
|
||||||
.data.injectorVersion
|
.data.injectorVersion
|
||||||
|
container.log("Downloading injector version $targetVersion")
|
||||||
|
|
||||||
super.execute(container)
|
super.execute(container)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,12 @@ class DownloadPatchesStep : DownloadStep(), KoinComponent {
|
||||||
override val targetFile get() = paths.cachedSmaliPatches(targetVersion, isCustomVersion)
|
override val targetFile get() = paths.cachedSmaliPatches(targetVersion, isCustomVersion)
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
var customVersions = mutableStateListOf<SemVer>()
|
val customVersions = mutableStateListOf<SemVer>()
|
||||||
.apply { addAll(paths.customSmaliPatches()) }
|
.apply { addAll(paths.customSmaliPatches()) }
|
||||||
|
|
||||||
// Prompt to select or manage custom versions instead of downloading
|
// Prompt to select or manage custom versions instead of downloading
|
||||||
if (customVersions.isNotEmpty()) {
|
if (customVersions.isNotEmpty()) {
|
||||||
|
container.log("Custom versions present, waiting for selection: ${customVersions.joinToString()}")
|
||||||
val selectedVersion = overlays.startComposableForResult { callback ->
|
val selectedVersion = overlays.startComposableForResult { callback ->
|
||||||
CustomComponentVersionPicker(
|
CustomComponentVersionPicker(
|
||||||
componentTitle = "Smali Patches",
|
componentTitle = "Smali Patches",
|
||||||
|
|
@ -61,16 +62,21 @@ class DownloadPatchesStep : DownloadStep(), KoinComponent {
|
||||||
onCancel = { callback(null) },
|
onCancel = { callback(null) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedVersion != null) {
|
if (selectedVersion != null) {
|
||||||
isCustomVersion = true
|
isCustomVersion = true
|
||||||
targetVersion = selectedVersion
|
targetVersion = selectedVersion
|
||||||
state = StepState.Skipped
|
state = StepState.Skipped
|
||||||
|
container.log("Using local custom patches version $selectedVersion")
|
||||||
return
|
return
|
||||||
|
} else {
|
||||||
|
container.log("Selection dialog cancelled, continuing as normal")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
targetVersion = container.getStep<FetchInfoStep>()
|
targetVersion = container.getStep<FetchInfoStep>()
|
||||||
.data.patchesVersion
|
.data.patchesVersion
|
||||||
|
container.log("Downloading patches version $targetVersion")
|
||||||
|
|
||||||
super.execute(container)
|
super.execute(container)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,11 @@ class AlignmentStep : Step(), KoinComponent {
|
||||||
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
||||||
|
|
||||||
var resourcesArscBytes: ByteArray? = null
|
var resourcesArscBytes: ByteArray? = null
|
||||||
var nativeLibPaths: List<String>? = null
|
|
||||||
var dexCount: Int = -1
|
var dexCount: Int = -1
|
||||||
|
|
||||||
// Align resources.arsc due to targeting API 30 for silent install
|
// Align resources.arsc due to targeting API 30 for silent install
|
||||||
if (Build.VERSION.SDK_INT >= 30) {
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
|
container.log("Extracting resources.arsc to be aligned later")
|
||||||
resourcesArscBytes = ZipReader(apk)
|
resourcesArscBytes = ZipReader(apk)
|
||||||
.use { it.openEntry("resources.arsc")?.read() }
|
.use { it.openEntry("resources.arsc")?.read() }
|
||||||
?: throw IllegalArgumentException("APK is missing resources.arsc")
|
?: throw IllegalArgumentException("APK is missing resources.arsc")
|
||||||
|
|
@ -37,6 +37,7 @@ class AlignmentStep : Step(), KoinComponent {
|
||||||
|
|
||||||
// Align dex files due to using useEmbeddedDex (ref. ManifestPatcher)
|
// Align dex files due to using useEmbeddedDex (ref. ManifestPatcher)
|
||||||
if (Build.VERSION.SDK_INT >= 29) {
|
if (Build.VERSION.SDK_INT >= 29) {
|
||||||
|
container.log("Extracting all dex files to be aligned later")
|
||||||
ZipReader(apk).use { zip ->
|
ZipReader(apk).use { zip ->
|
||||||
// Count the amount of dex files currently in the apk
|
// Count the amount of dex files currently in the apk
|
||||||
dexCount = zip.entryNames.count { it.endsWith(".dex") }
|
dexCount = zip.entryNames.count { it.endsWith(".dex") }
|
||||||
|
|
@ -48,42 +49,54 @@ class AlignmentStep : Step(), KoinComponent {
|
||||||
file.writeBytes(bytes)
|
file.writeBytes(bytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
container.log("Extracted $dexCount dex files")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Align native libs due to using extractNativeLibs
|
// Align native libs due to using extractNativeLibs
|
||||||
nativeLibPaths = ZipReader(apk).use { zip ->
|
container.log("Extracting native libraries to be aligned later")
|
||||||
|
val nativeLibPaths = ZipReader(apk).use { zip ->
|
||||||
val libPaths = zip.entryNames.filter { it.endsWith(".so") }
|
val libPaths = zip.entryNames.filter { it.endsWith(".so") }
|
||||||
|
|
||||||
// Extract to disk temporarily
|
// Extract to disk temporarily
|
||||||
for ((idx, path) in libPaths.withIndex()) {
|
for ((idx, path) in libPaths.withIndex()) {
|
||||||
// Ignore lib architectures that don't match this device
|
// Ignore lib architectures that don't match this device
|
||||||
if (!path.startsWith("lib/$currentDeviceArch"))
|
if (!path.startsWith("lib/$currentDeviceArch")) {
|
||||||
|
container.log("Skipping native lib $path due to incompatible architecture")
|
||||||
continue
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Index is just used as a placeholder id to cache on disk
|
// Index is just used as a placeholder id to cache on disk
|
||||||
val bytes = zip.openEntry(path)!!.read()
|
val bytes = zip.openEntry(path)!!.read()
|
||||||
val file = paths.patchingWorkingDir().resolve("$idx.so")
|
val file = paths.patchingWorkingDir().resolve("$idx.so")
|
||||||
file.writeBytes(bytes)
|
file.writeBytes(bytes)
|
||||||
|
container.log("Extracted native lib $file")
|
||||||
}
|
}
|
||||||
|
|
||||||
libPaths
|
libPaths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
container.log("Writing entries back aligned")
|
||||||
ZipWriter(apk, /* append = */ true).use { zip ->
|
ZipWriter(apk, /* append = */ true).use { zip ->
|
||||||
// Delete all the unaligned files from APK
|
// Delete all the unaligned files from APK
|
||||||
|
container.log("Deleting resources.arsc")
|
||||||
if (resourcesArscBytes != null)
|
if (resourcesArscBytes != null)
|
||||||
zip.deleteEntry("resources.arsc")
|
zip.deleteEntry("resources.arsc")
|
||||||
|
|
||||||
|
container.log("Deleting $dexCount dex files")
|
||||||
for (i in 0..<dexCount)
|
for (i in 0..<dexCount)
|
||||||
zip.deleteEntry(getDexName(i))
|
zip.deleteEntry(getDexName(i))
|
||||||
|
|
||||||
|
container.log("Deleting native libraries: $nativeLibPaths")
|
||||||
for (path in nativeLibPaths)
|
for (path in nativeLibPaths)
|
||||||
zip.deleteEntry(path)
|
zip.deleteEntry(path)
|
||||||
|
|
||||||
// Write all the files back aligned this time
|
// Write all the files back aligned this time
|
||||||
if (resourcesArscBytes != null)
|
if (resourcesArscBytes != null) {
|
||||||
|
container.log("Writing resources.arsc uncompressed aligned to 4 bytes")
|
||||||
zip.writeEntry("resources.arsc", resourcesArscBytes, ZipCompression.NONE, 4)
|
zip.writeEntry("resources.arsc", resourcesArscBytes, ZipCompression.NONE, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
container.log("Writing dex files uncompressed aligned to 4 bytes")
|
||||||
for (idx in 0..<dexCount) {
|
for (idx in 0..<dexCount) {
|
||||||
val file = paths.patchingWorkingDir().resolve(getDexName(idx))
|
val file = paths.patchingWorkingDir().resolve(getDexName(idx))
|
||||||
val bytes = file.readBytes()
|
val bytes = file.readBytes()
|
||||||
|
|
@ -98,6 +111,8 @@ class AlignmentStep : Step(), KoinComponent {
|
||||||
|
|
||||||
val file = paths.patchingWorkingDir().resolve("$idx.so")
|
val file = paths.patchingWorkingDir().resolve("$idx.so")
|
||||||
val bytes = file.readBytes()
|
val bytes = file.readBytes()
|
||||||
|
|
||||||
|
container.log("Writing $path uncompressed aligned to 16KiB")
|
||||||
zip.writeEntry(path, bytes, ZipCompression.NONE, 16384)
|
zip.writeEntry(path, bytes, ZipCompression.NONE, 16384)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,10 @@ class CleanupStep : Step(), KoinComponent {
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
if (prefs.keepPatchedApks) {
|
if (prefs.keepPatchedApks) {
|
||||||
|
container.log("keepPatchedApks enabled, skipping...")
|
||||||
state = StepState.Skipped
|
state = StepState.Skipped
|
||||||
} else {
|
} else {
|
||||||
|
container.log("Deleting patching working dir")
|
||||||
if (!paths.patchingWorkingDir().deleteRecursively())
|
if (!paths.patchingWorkingDir().deleteRecursively())
|
||||||
throw IllegalStateException("Failed to delete patching working dir")
|
throw IllegalStateException("Failed to delete patching working dir")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ class InstallStep(private val options: PatchOptions) : Step(), KoinComponent {
|
||||||
title = R.string.notif_install_ready_title,
|
title = R.string.notif_install_ready_title,
|
||||||
description = R.string.notif_install_ready_desc,
|
description = R.string.notif_install_ready_desc,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
container.log("Waiting until manager is resumed to continue installation")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait until app resumed
|
// Wait until app resumed
|
||||||
|
|
@ -53,24 +55,32 @@ class InstallStep(private val options: PatchOptions) : Step(), KoinComponent {
|
||||||
|
|
||||||
// Show [PlayProtectDialog] and wait until it gets dismissed
|
// Show [PlayProtectDialog] and wait until it gets dismissed
|
||||||
if (!context.isPackageInstalled(options.packageName) && context.isPlayProtectEnabled() == true) {
|
if (!context.isPackageInstalled(options.packageName) && context.isPlayProtectEnabled() == true) {
|
||||||
|
container.log("Showing play protect warning dialog")
|
||||||
overlays.startComposableForResult { callback ->
|
overlays.startComposableForResult { callback ->
|
||||||
PlayProtectDialog(onDismiss = { callback(Unit) })
|
PlayProtectDialog(onDismiss = { callback(Unit) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
container.log("Installing ${apk.absolutePath}, silent: ${!prefs.devMode}")
|
||||||
val result = installers.getActiveInstaller().waitInstall(
|
val result = installers.getActiveInstaller().waitInstall(
|
||||||
apks = listOf(apk),
|
apks = listOf(apk),
|
||||||
silent = !prefs.devMode,
|
silent = !prefs.devMode,
|
||||||
)
|
)
|
||||||
|
|
||||||
when (result) {
|
when (result) {
|
||||||
is InstallerResult.Error -> throw Error("Failed to install APKs: ${result.getDebugReason()}")
|
is InstallerResult.Error -> {
|
||||||
|
container.log("Installation failed")
|
||||||
|
throw Error("Failed to install APKs: ${result.getDebugReason()}")
|
||||||
|
}
|
||||||
|
|
||||||
is InstallerResult.Cancelled -> {
|
is InstallerResult.Cancelled -> {
|
||||||
// The install screen is automatically closed immediately once cleanup finishes
|
// The install screen is automatically closed immediately once cleanup finishes
|
||||||
state = StepState.Skipped
|
state = StepState.Skipped
|
||||||
|
container.log("Installation was cancelled by user")
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
InstallerResult.Success ->
|
||||||
|
container.log("Installation successful")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ class SigningStep : Step(), KoinComponent {
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
||||||
|
|
||||||
|
container.log("Signing apk at ${apk.absolutePath}")
|
||||||
Signer.signApk(apk)
|
Signer.signApk(apk)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,14 @@ class AddAliuhookLibsStep : Step(), KoinComponent {
|
||||||
ZipWriter(apk, /* append = */ true).use { patchedApk ->
|
ZipWriter(apk, /* append = */ true).use { patchedApk ->
|
||||||
ZipReader(aliuhook).use { aliuhook ->
|
ZipReader(aliuhook).use { aliuhook ->
|
||||||
for (libFile in arrayOf("libaliuhook.so", "libc++_shared.so", "liblsplant.so")) {
|
for (libFile in arrayOf("libaliuhook.so", "libc++_shared.so", "liblsplant.so")) {
|
||||||
val bytes = aliuhook.openEntry("jni/$currentDeviceArch/$libFile")?.read()
|
container.log("Reading aliuhook lib $libFile with arch $currentDeviceArch")
|
||||||
|
|
||||||
|
val apkLibPath = "lib/$currentDeviceArch/$libFile"
|
||||||
|
val libBytes = aliuhook.openEntry("jni/$currentDeviceArch/$libFile")?.read()
|
||||||
?: throw IllegalStateException("Failed to read $libFile from aliuhook aar")
|
?: throw IllegalStateException("Failed to read $libFile from aliuhook aar")
|
||||||
|
|
||||||
patchedApk.writeEntry("lib/$currentDeviceArch/$libFile", bytes, ZipCompression.NONE)
|
container.log("Writing to $apkLibPath in APK unaligned uncompressed")
|
||||||
|
patchedApk.writeEntry(apkLibPath, libBytes, ZipCompression.NONE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,12 @@ class PatchManifestStep(private val options: PatchOptions) : Step() {
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
||||||
|
|
||||||
|
container.log("Reading manifest from apk")
|
||||||
val manifest = ZipReader(apk)
|
val manifest = ZipReader(apk)
|
||||||
.use { zip -> zip.openEntry("AndroidManifest.xml")?.read() }
|
.use { zip -> zip.openEntry("AndroidManifest.xml")?.read() }
|
||||||
?: throw IllegalArgumentException("No manifest found in APK")
|
?: throw IllegalArgumentException("No manifest found in APK")
|
||||||
|
|
||||||
|
container.log("Patching manifest")
|
||||||
val patchedManifest = ManifestPatcher.patchManifest(
|
val patchedManifest = ManifestPatcher.patchManifest(
|
||||||
manifestBytes = manifest,
|
manifestBytes = manifest,
|
||||||
packageName = options.packageName,
|
packageName = options.packageName,
|
||||||
|
|
@ -31,6 +33,7 @@ class PatchManifestStep(private val options: PatchOptions) : Step() {
|
||||||
debuggable = options.debuggable,
|
debuggable = options.debuggable,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
container.log("Writing patched manifest to apk unaligned compressed")
|
||||||
ZipWriter(apk, /* append = */ true).use {
|
ZipWriter(apk, /* append = */ true).use {
|
||||||
it.deleteEntry("AndroidManifest.xml")
|
it.deleteEntry("AndroidManifest.xml")
|
||||||
it.writeEntry("AndroidManifest.xml", patchedManifest)
|
it.writeEntry("AndroidManifest.xml", patchedManifest)
|
||||||
|
|
|
||||||
|
|
@ -26,17 +26,24 @@ class ReorganizeDexStep : Step(), KoinComponent {
|
||||||
.filter { it.dexPriority > 0 }
|
.filter { it.dexPriority > 0 }
|
||||||
.sumOf { it.dexCount }
|
.sumOf { it.dexCount }
|
||||||
|
|
||||||
|
container.log("dexProviders: " + dexProviders.joinToString { it.javaClass.simpleName })
|
||||||
|
container.log("priorityDexCount: $priorityDexCount")
|
||||||
|
|
||||||
var dexCount = 0
|
var dexCount = 0
|
||||||
|
|
||||||
ZipReader(apk).use { zip ->
|
ZipReader(apk).use { zip ->
|
||||||
// Count the amount of dex files currently in the apk
|
// Count the amount of dex files currently in the apk
|
||||||
dexCount = zip.entryNames.count { it.endsWith(".dex") }
|
dexCount = zip.entryNames.count { it.endsWith(".dex") }
|
||||||
|
container.log("Existing dex files in apk: $dexCount")
|
||||||
|
|
||||||
// Copy all the dex files that need to be moved out of the apk
|
// Copy all the dex files that need to be moved out of the apk,
|
||||||
|
// to ensure there's space for our higher priority dex files
|
||||||
|
container.log("Copying dex files out of apk to be moved to a lesser priority")
|
||||||
for (idx in 0..<priorityDexCount) {
|
for (idx in 0..<priorityDexCount) {
|
||||||
// Not enough dex files to move
|
// Not enough dex files to move
|
||||||
if (idx + 1 > dexCount) break
|
if (idx + 1 > dexCount) break
|
||||||
|
|
||||||
|
container.log("Extracting ${getDexName(idx)} from apk")
|
||||||
val bytes = zip.openEntry(getDexName(idx))!!.read()
|
val bytes = zip.openEntry(getDexName(idx))!!.read()
|
||||||
val file = paths.patchingWorkingDir().resolve(getDexName(idx))
|
val file = paths.patchingWorkingDir().resolve(getDexName(idx))
|
||||||
file.writeBytes(bytes)
|
file.writeBytes(bytes)
|
||||||
|
|
@ -44,11 +51,13 @@ class ReorganizeDexStep : Step(), KoinComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
ZipWriter(apk, /* append = */ true).use { zip ->
|
ZipWriter(apk, /* append = */ true).use { zip ->
|
||||||
|
container.log("Deleting dex files to be replaced with a higher priorty dex file")
|
||||||
// Delete all the old dex files from the apk
|
// Delete all the old dex files from the apk
|
||||||
for (idx in 0..<priorityDexCount) {
|
for (idx in 0..<priorityDexCount) {
|
||||||
// Not enough dex files to move
|
// Not enough dex files to move
|
||||||
if (idx + 1 > dexCount) break
|
if (idx + 1 > dexCount) break
|
||||||
|
|
||||||
|
container.log("Deleting ${getDexName(idx)} from apk")
|
||||||
zip.deleteEntry(getDexName(idx))
|
zip.deleteEntry(getDexName(idx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,7 +66,13 @@ class ReorganizeDexStep : Step(), KoinComponent {
|
||||||
for (dexProvider in dexProviders) {
|
for (dexProvider in dexProviders) {
|
||||||
if (dexProvider.dexPriority <= 0) continue
|
if (dexProvider.dexPriority <= 0) continue
|
||||||
|
|
||||||
|
container.log(
|
||||||
|
"Writing custom high priority dex files from step: " +
|
||||||
|
"${dexProvider.javaClass.simpleName} with priority of ${dexProvider.dexPriority}"
|
||||||
|
)
|
||||||
|
|
||||||
for (dexBytes in dexProvider.getDexFiles()) {
|
for (dexBytes in dexProvider.getDexFiles()) {
|
||||||
|
container.log("Writing dex file ${getDexName(idx)} unaligned uncompressed")
|
||||||
zip.writeEntry(getDexName(idx++), dexBytes, ZipCompression.NONE)
|
zip.writeEntry(getDexName(idx++), dexBytes, ZipCompression.NONE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,6 +82,8 @@ class ReorganizeDexStep : Step(), KoinComponent {
|
||||||
// Not enough dex files to move
|
// Not enough dex files to move
|
||||||
if (idx + 1 > dexCount) break
|
if (idx + 1 > dexCount) break
|
||||||
|
|
||||||
|
container.log("Moving old low priority dex file back into apk unaligned uncompressed: " + getDexName(dexCount + idx))
|
||||||
|
|
||||||
val file = paths.patchingWorkingDir().resolve(getDexName(idx))
|
val file = paths.patchingWorkingDir().resolve(getDexName(idx))
|
||||||
val bytes = file.readBytes()
|
val bytes = file.readBytes()
|
||||||
zip.writeEntry(getDexName(dexCount + idx), bytes, ZipCompression.NONE)
|
zip.writeEntry(getDexName(dexCount + idx), bytes, ZipCompression.NONE)
|
||||||
|
|
@ -78,7 +95,13 @@ class ReorganizeDexStep : Step(), KoinComponent {
|
||||||
for (dexProvider in dexProviders) {
|
for (dexProvider in dexProviders) {
|
||||||
if (dexProvider.dexPriority > 0) continue
|
if (dexProvider.dexPriority > 0) continue
|
||||||
|
|
||||||
|
container.log(
|
||||||
|
"Writing remaining low priority dex files into apk from step:" +
|
||||||
|
"${dexProvider.javaClass.simpleName} with priority of ${dexProvider.dexPriority}"
|
||||||
|
)
|
||||||
|
|
||||||
for (dexBytes in dexProvider.getDexFiles()) {
|
for (dexBytes in dexProvider.getDexFiles()) {
|
||||||
|
container.log("Writing dex file ${getDexName(idx)} unaligned uncompressed")
|
||||||
zip.writeEntry(getDexName(dexCount++), dexBytes, ZipCompression.NONE)
|
zip.writeEntry(getDexName(dexCount++), dexBytes, ZipCompression.NONE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,18 +41,22 @@ class ReplaceIconStep(private val options: PatchOptions) : Step(), KoinComponent
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
val isAdaptiveIconsAvailable = Build.VERSION.SDK_INT >= 28
|
val isAdaptiveIconsAvailable = Build.VERSION.SDK_INT >= 28
|
||||||
val isMonochromeIconsAvailable = Build.VERSION.SDK_INT >= 31
|
val isMonochromeIconsAvailable = Build.VERSION.SDK_INT >= 31
|
||||||
|
container.log("isAdaptiveIconsAvailable: $isAdaptiveIconsAvailable, isMonochromeIconsAvailable: $isMonochromeIconsAvailable")
|
||||||
|
|
||||||
// Adaptive icons are available starting with Android 8
|
// Adaptive icons are available starting with Android 8
|
||||||
// Monochrome icons are always patched starting with Android 12
|
// Monochrome icons are always patched starting with Android 12
|
||||||
// Skip if adaptive icons are unavailable or { not patching main icon AND monochrome icons are unavailable }
|
// Skip if adaptive icons are unavailable or { not patching main icon AND monochrome icons are unavailable }
|
||||||
if (!isAdaptiveIconsAvailable || (!isMonochromeIconsAvailable && options.iconReplacement is IconReplacement.Original)) {
|
if (!isAdaptiveIconsAvailable || (!isMonochromeIconsAvailable && options.iconReplacement is IconReplacement.Original)) {
|
||||||
|
container.log("No patching necessary, skipping step")
|
||||||
state = StepState.Skipped
|
state = StepState.Skipped
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
container.log("Parsing resources.arsc")
|
||||||
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
||||||
val arsc = ArscUtil.readArsc(apk)
|
val arsc = ArscUtil.readArsc(apk)
|
||||||
|
|
||||||
|
container.log("Parsing AndroidManifest.xml and obtaining adaptive square/round icon file paths")
|
||||||
val iconRscIds = AxmlUtil.readManifestIconInfo(apk)
|
val iconRscIds = AxmlUtil.readManifestIconInfo(apk)
|
||||||
val squareIconFile = arsc.getMainArscChunk().getResourceFileName(iconRscIds.squareIcon, "anydpi-v26")
|
val squareIconFile = arsc.getMainArscChunk().getResourceFileName(iconRscIds.squareIcon, "anydpi-v26")
|
||||||
val roundIconFile = arsc.getMainArscChunk().getResourceFileName(iconRscIds.roundIcon, "anydpi-v26")
|
val roundIconFile = arsc.getMainArscChunk().getResourceFileName(iconRscIds.roundIcon, "anydpi-v26")
|
||||||
|
|
@ -63,6 +67,8 @@ class ReplaceIconStep(private val options: PatchOptions) : Step(), KoinComponent
|
||||||
|
|
||||||
// Add the monochrome resource and add the resource file later
|
// Add the monochrome resource and add the resource file later
|
||||||
if (isMonochromeIconsAvailable) {
|
if (isMonochromeIconsAvailable) {
|
||||||
|
container.log("Adding monochrome icon resource to arsc")
|
||||||
|
|
||||||
val filePathIdx = arsc.getMainArscChunk().stringPool
|
val filePathIdx = arsc.getMainArscChunk().stringPool
|
||||||
.addString("res/ic_aliucord_monochrome.xml")
|
.addString("res/ic_aliucord_monochrome.xml")
|
||||||
|
|
||||||
|
|
@ -77,12 +83,16 @@ class ReplaceIconStep(private val options: PatchOptions) : Step(), KoinComponent
|
||||||
|
|
||||||
// Add a new color resource to use
|
// Add a new color resource to use
|
||||||
if (options.iconReplacement is IconReplacement.CustomColor) {
|
if (options.iconReplacement is IconReplacement.CustomColor) {
|
||||||
|
container.log("Adding icon color resource to arsc")
|
||||||
|
|
||||||
backgroundIcon = arsc.getPackageChunk()
|
backgroundIcon = arsc.getPackageChunk()
|
||||||
.addColorResource("aliucord", options.iconReplacement.color)
|
.addColorResource("icon_background_replacement", options.iconReplacement.color)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a new mipmap resource and the file to be added later
|
// Add a new mipmap resource and the file to be added later
|
||||||
if (options.iconReplacement is IconReplacement.CustomImage) {
|
if (options.iconReplacement is IconReplacement.CustomImage) {
|
||||||
|
container.log("Adding custom icon foreground to arsc")
|
||||||
|
|
||||||
val iconPathIdx = arsc.getMainArscChunk().stringPool
|
val iconPathIdx = arsc.getMainArscChunk().stringPool
|
||||||
.addString("res/ic_foreground_replacement.png")
|
.addString("res/ic_foreground_replacement.png")
|
||||||
|
|
||||||
|
|
@ -96,6 +106,7 @@ class ReplaceIconStep(private val options: PatchOptions) : Step(), KoinComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
for (rscFile in setOf(squareIconFile, roundIconFile)) { // setOf to not possibly patch same file twice
|
for (rscFile in setOf(squareIconFile, roundIconFile)) { // setOf to not possibly patch same file twice
|
||||||
|
container.log("Patching and writing adaptive icon AXML at $rscFile")
|
||||||
AxmlUtil.patchAdaptiveIcon(
|
AxmlUtil.patchAdaptiveIcon(
|
||||||
apk = apk,
|
apk = apk,
|
||||||
resourcePath = rscFile,
|
resourcePath = rscFile,
|
||||||
|
|
@ -105,16 +116,21 @@ class ReplaceIconStep(private val options: PatchOptions) : Step(), KoinComponent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
container.log("Writing other patched files back to apk")
|
||||||
ZipWriter(apk, /* append = */ true).use {
|
ZipWriter(apk, /* append = */ true).use {
|
||||||
if (isMonochromeIconsAvailable) {
|
if (isMonochromeIconsAvailable) {
|
||||||
|
container.log("Writing monochrome icon AXML to apk")
|
||||||
it.writeEntry("res/ic_aliucord_monochrome.xml", context.getResBytes(R.drawable.ic_discord_monochrome))
|
it.writeEntry("res/ic_aliucord_monochrome.xml", context.getResBytes(R.drawable.ic_discord_monochrome))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.iconReplacement is IconReplacement.CustomImage) {
|
if (options.iconReplacement is IconReplacement.CustomImage) {
|
||||||
|
container.log("Writing custom icon foreground to apk")
|
||||||
it.writeEntry("res/ic_foreground_replacement.png", options.iconReplacement.imageBytes)
|
it.writeEntry("res/ic_foreground_replacement.png", options.iconReplacement.imageBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
container.log("Writing resources unaligned compressed")
|
||||||
it.deleteEntry("resources.arsc")
|
it.deleteEntry("resources.arsc")
|
||||||
|
// This doesn't need to be aligned and uncompressed here, since it that is done during AlignmentSte
|
||||||
it.writeEntry("resources.arsc", arsc.toByteArray())
|
it.writeEntry("resources.arsc", arsc.toByteArray())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,20 +12,20 @@ import com.aliucord.manager.ui.screens.patchopts.PatchOptions
|
||||||
import com.aliucord.manager.util.IS_CUSTOM_BUILD
|
import com.aliucord.manager.util.IS_CUSTOM_BUILD
|
||||||
import com.github.diamondminer88.zip.ZipWriter
|
import com.github.diamondminer88.zip.ZipWriter
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store the install options and additional data inside the APK for future use,
|
* Store the install options and additional data inside the APK for future use,
|
||||||
* for example checking what library versions were used, or performing "updates" while
|
* for example checking what library versions were used, or performing "updates" while
|
||||||
* maintaining the same install options as what was used upon first install.
|
* maintaining the same install options as what was used upon first install.
|
||||||
*/
|
*/
|
||||||
class SaveMetadataStep(private val options: PatchOptions) : Step() {
|
class SaveMetadataStep(private val options: PatchOptions) : Step(), KoinComponent {
|
||||||
|
private val json: Json by inject()
|
||||||
|
|
||||||
override val group = StepGroup.Patch
|
override val group = StepGroup.Patch
|
||||||
override val localizedName = R.string.patch_step_save_metadata
|
override val localizedName = R.string.patch_step_save_metadata
|
||||||
|
|
||||||
private val json = Json {
|
|
||||||
prettyPrint = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
||||||
val aliuhook = container.getStep<DownloadAliuhookStep>()
|
val aliuhook = container.getStep<DownloadAliuhookStep>()
|
||||||
|
|
@ -41,6 +41,7 @@ class SaveMetadataStep(private val options: PatchOptions) : Step() {
|
||||||
patchesVersion = patches.targetVersion,
|
patchesVersion = patches.targetVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
container.log("Writing serialized install metadata to APK")
|
||||||
ZipWriter(apk, /* append = */ true).use {
|
ZipWriter(apk, /* append = */ true).use {
|
||||||
it.writeEntry("aliucord.json", json.encodeToString<InstallMetadata>(metadata))
|
it.writeEntry("aliucord.json", json.encodeToString<InstallMetadata>(metadata))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,10 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
|
||||||
val patches = mutableListOf<LoadedPatch>()
|
val patches = mutableListOf<LoadedPatch>()
|
||||||
|
|
||||||
// Load and parse all the patches from the smali patch archive
|
// Load and parse all the patches from the smali patch archive
|
||||||
|
container.log("Loading patches from smali patch archive: ${patchesZip.absolutePath}")
|
||||||
ZipReader(patchesZip).use { zip ->
|
ZipReader(patchesZip).use { zip ->
|
||||||
for (patchFile in zip.entryNames) {
|
for (patchFile in zip.entryNames) {
|
||||||
|
container.log("Parsing patch file $patchFile")
|
||||||
if (!patchFile.endsWith(".patch")) continue
|
if (!patchFile.endsWith(".patch")) continue
|
||||||
|
|
||||||
val lines = zip.openEntry(patchFile)!!.read()
|
val lines = zip.openEntry(patchFile)!!.read()
|
||||||
|
|
@ -55,6 +57,7 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
|
||||||
patch = UnifiedDiffUtils.parseUnifiedDiff(lines),
|
patch = UnifiedDiffUtils.parseUnifiedDiff(lines),
|
||||||
)
|
)
|
||||||
patches.add(patch)
|
patches.add(patch)
|
||||||
|
container.log("Loaded patch file for class ${patch.fullClassName}")
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
throw Error("Failed to parse patch file $patchFile", t)
|
throw Error("Failed to parse patch file $patchFile", t)
|
||||||
}
|
}
|
||||||
|
|
@ -62,9 +65,11 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disassemble all the classes we have patches for from all the dex files
|
// Disassemble all the classes we have patches for from all the dex files
|
||||||
|
container.log("Disassembling target classes in APK")
|
||||||
ZipReader(apk).use { zip ->
|
ZipReader(apk).use { zip ->
|
||||||
for (file in zip.entryNames) {
|
for (file in zip.entryNames) {
|
||||||
if (!file.endsWith(".dex")) continue
|
if (!file.endsWith(".dex")) continue
|
||||||
|
container.log("Disassembling dex $file")
|
||||||
|
|
||||||
val dexFile = try {
|
val dexFile = try {
|
||||||
DexBackedDexFile(
|
DexBackedDexFile(
|
||||||
|
|
@ -88,11 +93,15 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(result) { "Failed to disassemble dex $file (unknown reason)" }
|
assert(result) { "Failed to disassemble dex $file (unknown reason)" }
|
||||||
|
container.log("Disassembled dex file for potential target classes")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply all the patches to the smali files
|
// Apply all the patches to the smali files
|
||||||
|
container.log("Applying smali patches to disassembled files")
|
||||||
for ((fullClassName, patch) in patches) {
|
for ((fullClassName, patch) in patches) {
|
||||||
|
container.log("Applying patch to class $fullClassName")
|
||||||
|
|
||||||
val smaliFile = smaliDir.resolve("$fullClassName.smali")
|
val smaliFile = smaliDir.resolve("$fullClassName.smali")
|
||||||
if (!smaliFile.exists()) {
|
if (!smaliFile.exists()) {
|
||||||
throw FileNotFoundException("Target smali file $fullClassName.smali not found for patching!")
|
throw FileNotFoundException("Target smali file $fullClassName.smali not found for patching!")
|
||||||
|
|
@ -110,6 +119,7 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assemble the patched classes back into a single dex
|
// Assemble the patched classes back into a single dex
|
||||||
|
container.log("Reassembling patches smali classes into new dex")
|
||||||
smaliDir.mkdir()
|
smaliDir.mkdir()
|
||||||
Smali.assemble(
|
Smali.assemble(
|
||||||
SmaliOptions().apply {
|
SmaliOptions().apply {
|
||||||
|
|
|
||||||
|
|
@ -29,21 +29,28 @@ class DowngradeCheckStep(private val options: PatchOptions) : Step(), KoinCompon
|
||||||
override val localizedName = R.string.patch_step_downgrade_check
|
override val localizedName = R.string.patch_step_downgrade_check
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
|
container.log("Fetching version of package ${options.packageName}")
|
||||||
val (_, currentVersion) = try {
|
val (_, currentVersion) = try {
|
||||||
context.getPackageVersion(options.packageName)
|
context.getPackageVersion(options.packageName)
|
||||||
}
|
}
|
||||||
// Package is not installed
|
// Package is not installed
|
||||||
catch (_: NameNotFoundException) {
|
catch (_: NameNotFoundException) {
|
||||||
state = StepState.Skipped
|
state = StepState.Skipped
|
||||||
|
container.log("Package not uninstalled, skipping check")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
container.log("Version of installed Discord app: $currentVersion")
|
||||||
|
|
||||||
val targetVersion = container
|
val targetVersion = container
|
||||||
.getStep<FetchInfoStep>()
|
.getStep<FetchInfoStep>()
|
||||||
.data.discordVersionCode.toIntOrNull()
|
.data.discordVersionCode.toIntOrNull()
|
||||||
?: throw IllegalArgumentException("Invalid remote target Discord version")
|
?: throw IllegalArgumentException("Invalid remote target Discord version")
|
||||||
|
|
||||||
|
container.log("Target discord version: $targetVersion")
|
||||||
|
|
||||||
if (currentVersion > targetVersion) {
|
if (currentVersion > targetVersion) {
|
||||||
|
container.log("Current installed version is greater than target, forcing uninstallation")
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
context.showToast(R.string.installer_uninstall_new)
|
context.showToast(R.string.installer_uninstall_new)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ class FetchInfoStep : Step(), KoinComponent {
|
||||||
private set
|
private set
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
|
container.log("Fetching ${AliucordGithubService.DATA_JSON_URL}")
|
||||||
data = github.getDataJson().getOrThrow()
|
data = github.getDataJson().getOrThrow()
|
||||||
|
container.log("Fetched build data: $data")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ fun LogScreenContent(
|
||||||
item("ENVIRONMENT_INFO") {
|
item("ENVIRONMENT_INFO") {
|
||||||
PatchOption(
|
PatchOption(
|
||||||
name = stringResource(R.string.log_section_env_info),
|
name = stringResource(R.string.log_section_env_info),
|
||||||
description = stringResource(R.string.log_section_env_info_desc),
|
description = null,
|
||||||
) {
|
) {
|
||||||
SelectableTextArea(
|
SelectableTextArea(
|
||||||
text = data.environmentInfo,
|
text = data.environmentInfo,
|
||||||
|
|
@ -109,17 +109,17 @@ fun LogScreenContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// item("LOG") {
|
item("LOG") {
|
||||||
// PatchOption(
|
PatchOption(
|
||||||
// name = stringResource(R.string.log_section_log),
|
name = stringResource(R.string.log_section_log),
|
||||||
// description = null,
|
description = null,
|
||||||
// ) {
|
) {
|
||||||
// SelectableTextArea(
|
SelectableTextArea(
|
||||||
// text = data.installationLog,
|
text = data.installationLog,
|
||||||
// modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
// )
|
)
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,7 @@ class PatchingScreen(private val data: PatchOptions) : Screen, Parcelable {
|
||||||
MainActionButton(
|
MainActionButton(
|
||||||
text = stringResource(R.string.action_retry_install),
|
text = stringResource(R.string.action_retry_install),
|
||||||
icon = painterResource(R.drawable.ic_refresh),
|
icon = painterResource(R.drawable.ic_refresh),
|
||||||
onClick = model::launchApp,
|
onClick = model::install,
|
||||||
)
|
)
|
||||||
|
|
||||||
MainActionButton(
|
MainActionButton(
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ class PatchingScreenModel(
|
||||||
private set
|
private set
|
||||||
|
|
||||||
init {
|
init {
|
||||||
restart()
|
install()
|
||||||
|
|
||||||
// Rotate fun facts every so often
|
// Rotate fun facts every so often
|
||||||
screenModelScope.launch {
|
screenModelScope.launch {
|
||||||
|
|
@ -81,7 +81,7 @@ class PatchingScreenModel(
|
||||||
application.showToast(R.string.action_cleared_cache)
|
application.showToast(R.string.action_cleared_cache)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restart() {
|
fun install() = screenModelScope.launch {
|
||||||
runnerJob?.cancel("Manual cancellation")
|
runnerJob?.cancel("Manual cancellation")
|
||||||
steps = null
|
steps = null
|
||||||
|
|
||||||
|
|
@ -105,6 +105,7 @@ class PatchingScreenModel(
|
||||||
id = installId!!,
|
id = installId!!,
|
||||||
installDate = startTime!!,
|
installDate = startTime!!,
|
||||||
options = options,
|
options = options,
|
||||||
|
log = "- Failed to initialize patch runner",
|
||||||
error = error,
|
error = error,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -124,9 +125,9 @@ class PatchingScreenModel(
|
||||||
.mapValues { it.value.toUnsafeImmutable() }
|
.mapValues { it.value.toUnsafeImmutable() }
|
||||||
.toUnsafeImmutable()
|
.toUnsafeImmutable()
|
||||||
|
|
||||||
// Intentionally delay to show the state change of the first step in UI when it runs
|
// Intentionally delay to show the state change of the first step when it runs in the UI.
|
||||||
// without it, on a fast internet it just immediately shows as "Success"
|
// Without this, on a fast internet connection the step just immediately shows as "Success".
|
||||||
delay(600)
|
delay(400)
|
||||||
|
|
||||||
// Execute all the steps and catch any errors
|
// Execute all the steps and catch any errors
|
||||||
when (val error = runner.executeAll()) {
|
when (val error = runner.executeAll()) {
|
||||||
|
|
@ -143,6 +144,7 @@ class PatchingScreenModel(
|
||||||
id = installId!!,
|
id = installId!!,
|
||||||
installDate = startTime!!,
|
installDate = startTime!!,
|
||||||
options = options,
|
options = options,
|
||||||
|
log = runner.getLog(),
|
||||||
error = null,
|
error = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -155,6 +157,7 @@ class PatchingScreenModel(
|
||||||
id = installId!!,
|
id = installId!!,
|
||||||
installDate = startTime!!,
|
installDate = startTime!!,
|
||||||
options = options,
|
options = options,
|
||||||
|
log = runner.getLog(),
|
||||||
error = error,
|
error = error,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,6 @@
|
||||||
<string name="log_title">Log</string>
|
<string name="log_title">Log</string>
|
||||||
<string name="log_section_install_info">Installation Info</string>
|
<string name="log_section_install_info">Installation Info</string>
|
||||||
<string name="log_section_env_info">Environment Info</string>
|
<string name="log_section_env_info">Environment Info</string>
|
||||||
<string name="log_section_env_info_desc">Information about your device that may be crucial for debugging.</string>
|
|
||||||
<string name="log_section_error">Error Stacktrace</string>
|
<string name="log_section_error">Error Stacktrace</string>
|
||||||
<string name="log_section_log">Installation Log</string>
|
<string name="log_section_log">Installation Log</string>
|
||||||
<string name="log_action_export">Export log</string>
|
<string name="log_action_export">Export log</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue