feat: new icon patcher (#71)

This commit is contained in:
rushii 2024-03-15 16:26:58 -07:00 • committed by GitHub
parent 4b820e594e
commit ce7f726ee8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 543 additions and 26 deletions

View file

@ -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") })
}

View file

@ -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">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -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<CopyDependenciesStep>().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)
}

View file

@ -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
}
}

View file

@ -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 <adaptive-icon> 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 <background> with.
*
* @param foregroundIcon A drawable resource id to replace <foreground> with.
* @param monochromeIcon A drawable resource id to add or replace <monochrome> 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) {
// <monochrome> 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
// `<monochrome android:drawable="@drawable/xyz"></monochrome>
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 <application> 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,
)
}

View file

@ -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))
}
}
}

View file

@ -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,
)
}

View file

@ -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")
}

View file

@ -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 <E> ObjectList<E>.find(block: (E) -> Boolean): E? {
forEach { value ->
if (block(value))
return value
}
return null
}

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group>
<clip-path android:pathData="M31.56,36.63l44.88,0l0,34.77l-44.88,0z" />
<path
android:fillColor="#5865f2"
android:pathData="M69.55,39.73C66.69,38.42 63.63,37.45 60.42,36.9 60.36,36.89 60.3,36.91 60.27,36.97 59.88,37.67 59.44,38.58 59.13,39.3 55.68,38.79 52.25,38.79 48.87,39.3 48.57,38.57 48.11,37.67 47.72,36.97 47.69,36.91 47.63,36.89 47.57,36.9 44.36,37.45 41.3,38.42 38.43,39.73 38.41,39.74 38.39,39.76 38.37,39.78 32.56,48.48 30.96,56.95 31.74,65.33c0,0.04 0.03,0.08 0.06,0.11 3.84,2.82 7.56,4.53 11.21,5.67 0.06,0.02 0.12,-0 0.16,-0.05 0.86,-1.18 1.63,-2.42 2.29,-3.73 0.04,-0.08 0,-0.17 -0.08,-0.2 -1.22,-0.46 -2.38,-1.03 -3.5,-1.67 -0.09,-0.05 -0.1,-0.18 -0.01,-0.24 0.24,-0.18 0.47,-0.36 0.7,-0.54 0.04,-0.03 0.1,-0.04 0.15,-0.02 7.35,3.35 15.3,3.35 22.56,0 0.05,-0.02 0.1,-0.02 0.15,0.02 0.22,0.19 0.46,0.37 0.7,0.55 0.08,0.06 0.08,0.19 -0.01,0.24 -1.12,0.65 -2.28,1.21 -3.5,1.67 -0.08,0.03 -0.11,0.12 -0.08,0.2 0.67,1.31 1.44,2.55 2.29,3.73 0.04,0.05 0.1,0.07 0.16,0.05 3.67,-1.13 7.39,-2.85 11.23,-5.67 0.03,-0.02 0.05,-0.06 0.06,-0.1C77.18,55.65 74.68,47.24 69.61,39.78 69.6,39.76 69.58,39.74 69.55,39.73ZM46.56,60.23c-2.21,0 -4.03,-2.03 -4.03,-4.52 0,-2.49 1.79,-4.52 4.03,-4.52 2.26,0 4.07,2.05 4.03,4.52 0,2.49 -1.79,4.52 -4.03,4.52zM61.47,60.23c-2.21,0 -4.03,-2.03 -4.03,-4.52 0,-2.49 1.79,-4.52 4.03,-4.52 2.26,0 4.07,2.05 4.03,4.52 0,2.49 -1.77,4.52 -4.03,4.52z" />
</group>
</vector>

View file

@ -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