From ce7f726ee892de58431267225b70ad47e5f6e1b9 Mon Sep 17 00:00:00 2001 From: rushii <33725716+rushiiMachine@users.noreply.github.com> Date: Fri, 15 Mar 2024 16:26:58 -0700 Subject: [PATCH] feat: new icon patcher (#71) --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 1 + .../main/assets/icons/ic_logo_foreground.png | Bin 6205 -> 0 bytes app/src/main/assets/icons/ic_logo_round.png | Bin 4628 -> 0 bytes app/src/main/assets/icons/ic_logo_square.png | Bin 4571 -> 0 bytes .../installer/steps/patch/ReplaceIconStep.kt | 101 +++++++-- .../manager/installer/util/ArscUtil.kt | 155 ++++++++++++++ .../manager/installer/util/AxmlUtil.kt | 201 ++++++++++++++++++ .../ui/screens/installopts/InstallOptions.kt | 54 ++++- .../installopts/InstallOptionsModel.kt | 4 +- .../com/aliucord/manager/util/Context.kt | 24 +++ .../kotlin/com/aliucord/manager/util/Utils.kt | 10 + .../res/drawable/ic_discord_monochrome.xml | 12 ++ gradle/libs.versions.toml | 6 +- 14 files changed, 543 insertions(+), 26 deletions(-) delete mode 100644 app/src/main/assets/icons/ic_logo_foreground.png delete mode 100644 app/src/main/assets/icons/ic_logo_round.png delete mode 100644 app/src/main/assets/icons/ic_logo_square.png create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/util/ArscUtil.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/installer/util/AxmlUtil.kt create mode 100644 app/src/main/res/drawable/ic_discord_monochrome.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b8940fb..c2a4d46 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -135,6 +135,7 @@ dependencies { implementation(libs.apksig) implementation(libs.axml) implementation(libs.bouncycastle) + implementation(libs.binaryResources) implementation(libs.coil) implementation(variantOf(libs.zip) { artifactType("aar") }) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 28aec09..9618ec9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -24,6 +24,7 @@ android:name=".ManagerApplication" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:largeHeap="true" android:requestLegacyExternalStorage="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true"> diff --git a/app/src/main/assets/icons/ic_logo_foreground.png b/app/src/main/assets/icons/ic_logo_foreground.png deleted file mode 100644 index 592fc38928161d9a8a46c4a99ea13fd8d530cae0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6205 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 z zeV?0E7iRKm>`;?`K=nocpmW`YE050IIXEzOBtcO!Q(^fI^5M&#UMBa(i&8UpdbywL zY;PWQ%HBiyXb#R&kPd-uy}T@nUv4=kD}A8Rns~2$+{d;@v;M@x%gCHNRwC{_KaQItlKl1H} z>~b;n{jQHT2iL!RR;>&~#1xTH_2x5D^-6u+J@}1}D;qXV2g@kPqXu(4bEtVAPjxA! zw0X~UI=Oe{)4;3ob<v@0T7`fCOm@ zFow-ZG=|MQ*M3rjv(Xa(BxOy`21_6Y5AIL#>B0mnd)}37-)h n3xf6aan1mzpfO|J)(OR3@!oz4U&}~v`2k+;es0B!gSY+@vs+yZ diff --git a/app/src/main/assets/icons/ic_logo_square.png b/app/src/main/assets/icons/ic_logo_square.png deleted file mode 100644 index fc0090fdcfe9a729921f00568c64c904917b7b51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4571 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 diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt index 1afe19e..e631205 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/patch/ReplaceIconStep.kt @@ -1,20 +1,34 @@ package com.aliucord.manager.installer.steps.patch import android.content.Context +import android.os.Build import androidx.compose.runtime.Stable import com.aliucord.manager.R import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.base.StepState +import com.aliucord.manager.installer.util.ArscUtil +import com.aliucord.manager.installer.util.ArscUtil.addColorResource +import com.aliucord.manager.installer.util.ArscUtil.addResource +import com.aliucord.manager.installer.util.ArscUtil.getMainArscChunk +import com.aliucord.manager.installer.util.ArscUtil.getPackageChunk +import com.aliucord.manager.installer.util.ArscUtil.getResourceFileName +import com.aliucord.manager.installer.util.AxmlUtil import com.aliucord.manager.ui.screens.installopts.InstallOptions +import com.aliucord.manager.ui.screens.installopts.InstallOptions.IconReplacement +import com.aliucord.manager.util.getResBytes import com.github.diamondminer88.zip.ZipWriter +import com.google.devrel.gmscore.tools.apk.arsc.BinaryResourceIdentifier +import com.google.devrel.gmscore.tools.apk.arsc.BinaryResourceValue import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import java.io.InputStream /** - * Replace icons + * Patch the mipmap-v26 launcher icons' background, foreground and monochrome attributes, + * and if below API 26, replace the traditional mipmap icons instead. + * All filenames are retrieved based on the resource definitions in `resources.arsc`, + * so that this works on pretty much almost any APK. */ @Stable class ReplaceIconStep(private val options: InstallOptions) : Step(), KoinComponent { @@ -24,32 +38,79 @@ class ReplaceIconStep(private val options: InstallOptions) : Step(), KoinCompone override val localizedName = R.string.install_step_patch_icon override suspend fun execute(container: StepRunner) { - if (!options.replaceIcon) { + if (Build.VERSION.SDK_INT < 26 || (!options.monochromeIcon && options.iconReplacement is IconReplacement.Original)) { state = StepState.Skipped return } val apk = container.getStep().patchedApk + val arsc = ArscUtil.readArsc(apk) + + val iconRscIds = AxmlUtil.readManifestIconInfo(apk) + val squareIconFile = arsc.getMainArscChunk().getResourceFileName(iconRscIds.squareIcon, "anydpi-v26") + val roundIconFile = arsc.getMainArscChunk().getResourceFileName(iconRscIds.roundIcon, "anydpi-v26") + + var foregroundIcon: BinaryResourceIdentifier? = null + var backgroundIcon: BinaryResourceIdentifier? = null + var monochromeIcon: BinaryResourceIdentifier? = null + + // Add the monochrome resource and add the resource file later + if (options.monochromeIcon) { + val filePathIdx = arsc.getMainArscChunk().stringPool + .addString("res/ic_aliucord_monochrome.xml") + + monochromeIcon = arsc.getPackageChunk().addResource( + typeName = "drawable", + resourceName = "ic_aliucord_monochrome", + configurations = { it.isDefault }, + valueType = BinaryResourceValue.Type.STRING, + valueData = filePathIdx, + ) + } + + // Add a new color resource to use + if (options.iconReplacement is IconReplacement.CustomColor) { + backgroundIcon = arsc.getPackageChunk() + .addColorResource("aliucord", options.iconReplacement.color) + } + + // Add a new mipmap resource and the file to be added later + if (options.iconReplacement is IconReplacement.CustomImage) { + val iconPathIdx = arsc.getMainArscChunk().stringPool + .addString("res/ic_foreground_replacement.png") + + foregroundIcon = arsc.getPackageChunk().addResource( + typeName = "mipmap", + resourceName = "ic_foreground_replacement", + configurations = { it.toString().endsWith("dpi") }, // Any mipmap config except anydpi-v26 + valueType = BinaryResourceValue.Type.STRING, + valueData = iconPathIdx, + ) + } + + for (rscFile in setOf(squareIconFile, roundIconFile)) { // setOf to not possibly patch same file twice + AxmlUtil.patchAdaptiveIcon( + apk = apk, + resourcePath = rscFile, + backgroundColor = backgroundIcon, + foregroundIcon = foregroundIcon, + monochromeIcon = monochromeIcon, + ) + } + + val newArscBytes = arsc.toByteArray(/* shrink = */ true) ZipWriter(apk, /* append = */ true).use { - val foregroundIcon = readAsset("icons/ic_logo_foreground.png") - val squareIcon = readAsset("icons/ic_logo_square.png") - - val replacements = mapOf( - arrayOf("MbV.png", "kbF.png", "_eu.png", "EtS.png") to foregroundIcon, - arrayOf("_h_.png", "9MB.png", "Dy7.png", "kC0.png", "oEH.png", "RG0.png", "ud_.png", "W_3.png") to squareIcon - ) - - for ((files, replacement) in replacements) { - for (file in files) { - val path = "res/$file" - it.deleteEntry(path) - it.writeEntry(path, replacement) - } + if (options.monochromeIcon) { + it.writeEntry("res/ic_aliucord_monochrome.xml", context.getResBytes(R.drawable.ic_discord_monochrome)) } + + if (options.iconReplacement is IconReplacement.CustomImage) { + it.writeEntry("res/ic_foreground_replacement.png", options.iconReplacement.imageBytes) + } + + it.deleteEntry("resources.arsc") + it.writeEntry("resources.arsc", newArscBytes) } } - - private fun readAsset(fileName: String): ByteArray = - context.assets.open(fileName).use(InputStream::readBytes) } diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/util/ArscUtil.kt b/app/src/main/kotlin/com/aliucord/manager/installer/util/ArscUtil.kt new file mode 100644 index 0000000..f5509df --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/util/ArscUtil.kt @@ -0,0 +1,155 @@ +package com.aliucord.manager.installer.util + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import com.github.diamondminer88.zip.ZipReader +import com.google.devrel.gmscore.tools.apk.arsc.* +import java.io.File + +object ArscUtil { + /** + * Read and parse `resources.arsc` from an APK. + */ + fun readArsc(apk: File): BinaryResourceFile { + val bytes = ZipReader(apk).use { it.openEntry("resources.arsc")?.read() } + ?: error("APK missing resources.arsc") + + return try { + BinaryResourceFile(bytes) + } catch (t: Throwable) { + throw Error("Failed to parse resources.arsc", t) + } + } + + /** + * Get the only top-level chunk in an arsc file. + */ + fun BinaryResourceFile.getMainArscChunk(): ResourceTableChunk { + if (this.chunks.size > 1) + error("More than 1 top level chunk in resources.arsc") + + return this.chunks.first() as? ResourceTableChunk + ?: error("Invalid top-level resources.arsc chunk") + } + + /** + * Get a singular package chunk in an arsc file. + */ + fun BinaryResourceFile.getPackageChunk(): PackageChunk { + return this.getMainArscChunk().packages.singleOrNull() + ?: error("resources.arsc must contain exactly 1 package chunk") + } + + /** + * Adds a new color resource to all configuration variants in an arsc package. + * + * @param name The new resource name. + * @param color The value of the new color resource. + * @return The resource ID of the newly added resource. + */ + fun PackageChunk.addColorResource( + name: String, + color: Color, + ): BinaryResourceIdentifier { + return this.addResource( + typeName = "color", + resourceName = name, + configurations = { true }, + valueType = BinaryResourceValue.Type.INT_COLOR_ARGB8, + valueData = color.toArgb(), + ) + } + + /** + * Adds a new color resource to the matching configuration variants in an arsc package. + * + * @param typeName The type of the resource (ex: `mipmap`, `drawable`, etc.) + * @param resourceName The new resource name. + * @param configurations A predicate whether to add the value into a matching type chunk. + * @param valueType The type of the resource value. + * @param valueData The raw data of the resource value. + * @return The resource ID of the newly added resource. + */ + fun PackageChunk.addResource( + typeName: String, + resourceName: String, + configurations: (BinaryResourceConfiguration) -> Boolean, + valueType: BinaryResourceValue.Type, + valueData: Int, + ): BinaryResourceIdentifier { + // Add a new resource entry to the "type spec chunk" and, + // a new resource entry to all matching "type chunks" + + val specChunk = this.getTypeSpecChunk(typeName) + val typeChunks = this.getTypeChunks(typeName) + + // Add a new string to the pool to be used as a key + val resourceNameIdx = this.keyStringPool.addString(resourceName, /* deduplicate = */ true) + + // Add a new resource entry to the type spec chunk + val resourceIdx = specChunk.addResource(/* flags = */ 0) + + for (typeChunk in typeChunks) { + // If no matching config, add a null entry and try next chunk + if (!configurations(typeChunk.configuration)) { + typeChunk.addEntry(null) + continue + } + + val entry = TypeChunk.Entry( + /* headerSize = */ 8, + /* flags = */ 0, + /* keyIndex = */ resourceNameIdx, + /* value = */ + BinaryResourceValue( + /* type = */ valueType, + /* data = */ valueData, + ), + /* values = */ null, // not a complex resource + /* parentEntry = */ 0, // not a complex resource + /* parent = */ typeChunk, + ) + + typeChunk.addEntry(entry) + } + + return BinaryResourceIdentifier.create( + /* packageId = */ this.id, + /* typeId = */ specChunk.id, + /* entryId = */ resourceIdx, + ) + } + + /** + * In an arsc file, for a specific resource in a configuration, get it's value. + * + * @param resourceId The target resource id. + * @param configurationName The target configuration variant of the resource. (ex: `anydpi-v26`, `xxhdpi`, `ldtrl-mpi`, etc.) + * @return The string value of the resource, which should be a file path inside the apk. + */ + fun ResourceTableChunk.getResourceFileName( + resourceId: BinaryResourceIdentifier, + configurationName: String, + ): String { + val packageChunk = this.packages.find { it.id == resourceId.packageId() } + ?: error("Unable to find target resource") + + val typeChunk = packageChunk.getTypeChunks(resourceId.typeId()) + .find { it.configuration.toString() == configurationName } + ?: error("Unable to find target resource") + + val entry = try { + typeChunk.getEntry(resourceId.entryId())!! + } catch (_: Throwable) { + error("Unable to find target resource") + } + + if (entry.isComplex || entry.value().type() != BinaryResourceValue.Type.STRING) + error("Target resource value type is not STRING") + + val valueIdx = entry.value().data() + val value = this.stringPool.getString(valueIdx) + + return value + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/util/AxmlUtil.kt b/app/src/main/kotlin/com/aliucord/manager/installer/util/AxmlUtil.kt new file mode 100644 index 0000000..204a27c --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/installer/util/AxmlUtil.kt @@ -0,0 +1,201 @@ +package com.aliucord.manager.installer.util + +import com.aliucord.manager.util.find +import com.github.diamondminer88.zip.ZipReader +import com.github.diamondminer88.zip.ZipWriter +import com.google.devrel.gmscore.tools.apk.arsc.* +import java.io.File + +object AxmlUtil { + /** + * Read and parse a specific axml resource inside an APK + * @param apk The source apk + * @param resourcePath The full path to the axml file inside the apk, which may be flattened. + */ + private fun readAxml(apk: File, resourcePath: String): BinaryResourceFile { + val bytes = ZipReader(apk).use { it.openEntry(resourcePath)?.read() } + ?: error("APK missing resource file at $resourcePath") + + return try { + BinaryResourceFile(bytes) + } catch (t: Throwable) { + throw Error("Failed to parse axml at $resourcePath", t) + } + } + + /** + * Get the only top-level chunk in an axml file. + */ + private fun BinaryResourceFile.getMainAxmlChunk(): XmlChunk { + if (this.chunks.size > 1) + error("More than 1 top level chunk in axml") + + return this.chunks.first() as? XmlChunk + ?: error("Invalid top-level axml chunk") + } + + /** + * Finds the first chunk with a matching [name] in a flattened chunk list. + * @receiver The top level XmlChunk ([getMainAxmlChunk]) + */ + private fun XmlChunk.getStartElementChunk(name: String): XmlStartElementChunk? { + val nameIdx = this.stringPool.indexOf(name) + + return this.chunks + .find { it is XmlStartElementChunk && it.nameIndex == nameIdx } + as? XmlStartElementChunk + } + + /** + * Finds the first attribute with a matching name (ignoring namespace) + * in a starting element chunk. + */ + private fun XmlStartElementChunk.getAttribute(name: String): XmlAttribute { + val nameIdx = (this.parent as XmlChunk).stringPool.indexOf(name) + + return this.attributes + .find { it.nameIndex() == nameIdx } + ?: error("Failed to find $name attribute in an axml chunk") + } + + /** + * Patches an axml file to change the `background`, `foreground`, and `monochrome` resource references. + * If any of the following are not null, then they will be patched. + * @param backgroundColor A color resource id to replace with. + * + * @param foregroundIcon A drawable resource id to replace with. + * @param monochromeIcon A drawable resource id to add or replace with. + */ + fun patchAdaptiveIcon( + apk: File, + resourcePath: String, + backgroundColor: BinaryResourceIdentifier? = null, + foregroundIcon: BinaryResourceIdentifier? = null, + monochromeIcon: BinaryResourceIdentifier? = null, + ) { + val xml = readAxml(apk, resourcePath) + val xmlChunk = xml.getMainAxmlChunk() + + // Patch the background color resource reference + if (backgroundColor != null) { + val chunk = xmlChunk.getStartElementChunk("background")!! + val attribute = chunk.getAttribute("drawable") + attribute.typedValue().setValue( + /* type = */ BinaryResourceValue.Type.REFERENCE, + /* data = */ backgroundColor.resourceId(), + ) + } + + // Patch the foreground drawable reference + if (foregroundIcon != null) { + val chunk = xmlChunk.getStartElementChunk("foreground")!! + val attribute = chunk.getAttribute("drawable") + attribute.typedValue().setValue( + /* type = */ BinaryResourceValue.Type.REFERENCE, + /* data = */ foregroundIcon.resourceId(), + ) + } + + // Add or replace the monochrome drawable reference + if (monochromeIcon != null) { + // already exists, patch existing chunk + val existingChunk = xmlChunk.getStartElementChunk("monochrome") + if (existingChunk != null) { + val attribute = existingChunk.getAttribute("drawable") + attribute.typedValue().setValue( + /* type = */ BinaryResourceValue.Type.REFERENCE, + /* data = */ monochromeIcon.resourceId(), + ) + } + // Add a new start & end chunk since they don't exist + // ` + else { + val iconEndChunkIdx = xmlChunk.chunks + .indexOfLast { it is XmlEndElementChunk && it.name == "adaptive-icon" } + + val namespaceIdx = xmlChunk.stringPool.indexOf("http://schemas.android.com/apk/res/android") + val drawableIdx = xmlChunk.stringPool.indexOf("drawable") + val monochromeIdx = xmlChunk.stringPool.addString("monochrome") + + val startChunk = XmlStartElementChunk( + /* namespaceIndex = */ -1, + /* nameIndex = */ monochromeIdx, + /* idIndex = */ -1, + /* classIndex = */ -1, + /* styleIndex = */ -1, + /* attributes = */ + listOf( + XmlAttribute( + /* namespaceIndex = */ namespaceIdx, + /* nameIndex = */ drawableIdx, + /* rawValueIndex = */ -1, + /* typedValue = */ + BinaryResourceValue( + /* type = */ BinaryResourceValue.Type.REFERENCE, + /* data = */ monochromeIcon.resourceId(), + ), + // This is wrong but it doesn't matter here as long as this attribute isn't stringified + /* parent = */ null, + ) + ), + /* parent = */ xmlChunk, + ) + val endChunk = XmlEndElementChunk( + /* namespaceIndex = */ namespaceIdx, + /* nameIndex = */ monochromeIdx, + /* parent = */ xmlChunk, + ) + + xmlChunk.addChunk(iconEndChunkIdx, startChunk) + xmlChunk.addChunk(iconEndChunkIdx + 1, endChunk) + } + } + + ZipWriter(apk, /* append = */ true).use { zip -> + zip.deleteEntry(resourcePath) + zip.writeEntry(resourcePath, xml.toByteArray(/* shrink = */ true)) + } + } + + /** + * From an APK, read the manifest's `icon` and `roundIcon` references to a resource. + * This is then used to get the filename of the resource from `resources.arsc`. + */ + fun readManifestIconInfo(apk: File): ManifestIconInfo { + val manifestBytes = ZipReader(apk).use { it.openEntry("AndroidManifest.xml")?.read() } + ?: error("APK missing manifest") + val manifest = BinaryResourceFile(manifestBytes) + val mainChunk = manifest.getMainAxmlChunk() + + // Prefetch string indexes to avoid parsing the entire string pool + val iconStringIdx = mainChunk.stringPool.indexOf("icon") + val roundIconStringIdx = mainChunk.stringPool.indexOf("roundIcon") + val applicationStringIdx = mainChunk.stringPool.indexOf("application") + + val applicationChunk = mainChunk.chunks + .find { it is XmlStartElementChunk && it.nameIndex == applicationStringIdx } as? XmlStartElementChunk + ?: error("Unable to find in manifest") + + val squareIcon = applicationChunk.attributes + .find { it.nameIndex() == iconStringIdx } + ?: error("Unable to find android:icon in manifest") + + val roundIcon = applicationChunk.attributes + .find { it.nameIndex() == roundIconStringIdx } + ?: error("Unable to find android:roundIcon in manifest") + + assert(squareIcon.typedValue().type() == BinaryResourceValue.Type.REFERENCE) + assert(roundIcon.typedValue().type() == BinaryResourceValue.Type.REFERENCE) + + return ManifestIconInfo( + // Resource IDs into resources.arsc + squareIcon = BinaryResourceIdentifier.create(squareIcon.typedValue().data()), + roundIcon = BinaryResourceIdentifier.create(roundIcon.typedValue().data()), + ) + } + + data class ManifestIconInfo( + val squareIcon: BinaryResourceIdentifier, + val roundIcon: BinaryResourceIdentifier, + ) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/installopts/InstallOptions.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/installopts/InstallOptions.kt index 16809fa..10ea91b 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/installopts/InstallOptions.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/installopts/InstallOptions.kt @@ -1,12 +1,60 @@ package com.aliucord.manager.ui.screens.installopts import androidx.compose.runtime.Immutable -import java.io.Serializable +import androidx.compose.ui.graphics.Color @Immutable data class InstallOptions( + /** + * The app name that's user-facing in launchers. + */ val appName: String, + + /** + * Changes the installation package name. + */ val packageName: String, + + /** + * Adding the debuggable APK flag. + */ val debuggable: Boolean, - val replaceIcon: Boolean, -) : Serializable + + /** + * Replacement of the user-facing launcher icon. + */ + val iconReplacement: IconReplacement, + + /** + * Adding a monochrome variant to the launcher icon. + * This is independent of [InstallOptions.iconReplacement] + */ + val monochromeIcon: Boolean, +) { + @Immutable + sealed interface IconReplacement { + /** + * Keeps the original icons that are present in the APK. + */ + data object Original : IconReplacement + + /** + * Replaces the foreground image of the icon entirely and sets the background to transparent. + * This does not affect the monochrome icon. + */ + data class CustomImage(val imageBytes: ByteArray) : IconReplacement + + /** + * Changes the background of the icon to a specific color without + * altering the foreground or monochrome variants. + */ + data class CustomColor(val color: Color) : IconReplacement + + companion object { + /** + * The default icon replacement option. + */ + val Aliucord = CustomColor(Color(0xFF00C853)) + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/installopts/InstallOptionsModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/installopts/InstallOptionsModel.kt index 31136bd..40fe4d5 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/installopts/InstallOptionsModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/installopts/InstallOptionsModel.kt @@ -86,7 +86,9 @@ class InstallOptionsModel( appName = appName, packageName = packageName, debuggable = debuggable, - replaceIcon = replaceIcon, // TODO: advanced icon patchers + // TODO: advanced icon options + iconReplacement = InstallOptions.IconReplacement.Aliucord, + monochromeIcon = true, ) } diff --git a/app/src/main/kotlin/com/aliucord/manager/util/Context.kt b/app/src/main/kotlin/com/aliucord/manager/util/Context.kt index 7e7f893..5af069e 100644 --- a/app/src/main/kotlin/com/aliucord/manager/util/Context.kt +++ b/app/src/main/kotlin/com/aliucord/manager/util/Context.kt @@ -7,11 +7,14 @@ import android.net.Uri import android.os.* import android.provider.Settings import android.util.Log +import android.util.TypedValue import android.widget.Toast +import androidx.annotation.AnyRes import androidx.annotation.StringRes import com.aliucord.manager.BuildConfig import com.aliucord.manager.R import java.io.File +import java.io.InputStream fun Context.copyToClipboard(text: String) { val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager @@ -86,3 +89,24 @@ fun Context.requestNoBatteryOptimizations() { startActivity(intent) } + +/** + * Get the raw bytes for a resource. + * @param id The resource identifier + * @return The resource's raw bytes as stored inside the APK + */ +fun Context.getResBytes(@AnyRes id: Int): ByteArray { + val tValue = TypedValue() + this.resources.getValue( + /* id = */ id, + /* outValue = */ tValue, + /* resolveRefs = */ true, + ) + + val resPath = tValue.string.toString() + + return this.javaClass.classLoader + ?.getResourceAsStream(resPath) + ?.use(InputStream::readBytes) + ?: error("Failed to get resource file $resPath from APK") +} diff --git a/app/src/main/kotlin/com/aliucord/manager/util/Utils.kt b/app/src/main/kotlin/com/aliucord/manager/util/Utils.kt index ba716ee..9acd3e0 100644 --- a/app/src/main/kotlin/com/aliucord/manager/util/Utils.kt +++ b/app/src/main/kotlin/com/aliucord/manager/util/Utils.kt @@ -1,5 +1,6 @@ package com.aliucord.manager.util +import androidx.collection.ObjectList import kotlin.math.pow import kotlin.math.truncate @@ -10,3 +11,12 @@ fun Double.toPrecision(decimals: Int): Double { val multiplier = 10.0.pow(decimals) return truncate(this * multiplier) / multiplier } + +inline fun ObjectList.find(block: (E) -> Boolean): E? { + forEach { value -> + if (block(value)) + return value + } + + return null +} diff --git a/app/src/main/res/drawable/ic_discord_monochrome.xml b/app/src/main/res/drawable/ic_discord_monochrome.xml new file mode 100644 index 0000000..fa30732 --- /dev/null +++ b/app/src/main/res/drawable/ic_discord_monochrome.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 077bfef..7cda3d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,12 +1,13 @@ [versions] accompanist = "0.32.0" -agp = "8.2.1" +agp = "8.2.2" androidx-activity = "1.8.2" androidx-core = "1.12.0" androidx-splashscreen = "1.0.1" androidx-lifecycle = "2.7.0" apksig = "7.4.2" axml = "1.0.1" +binary-resources = "1.0.0" bouncycastle = "1.70" coil = "2.5.0" compose = "1.5.4" @@ -19,7 +20,7 @@ kotlinx-immutable = "0.3.7" kotlinx-serialization = "1.5.0" ktor = "2.3.7" voyager = "1.0.0" -zip = "2.1.0" +zip = "2.1.1" [libraries] # Accompanist @@ -68,6 +69,7 @@ voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voy # Other apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" } axml = { module = "com.aliucord:axml", version.ref = "axml" } +binaryResources = { module = "com.aliucord:binary-resources", version.ref = "binary-resources" } bouncycastle = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bouncycastle" } coil = { module = "io.coil-kt:coil-compose", version.ref = "coil" } zip = { module = "io.github.diamondminer88:zip-android", version.ref = "zip" } # Only use the "aar" artifact