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.apksig)
implementation(libs.axml) implementation(libs.axml)
implementation(libs.bouncycastle) implementation(libs.bouncycastle)
implementation(libs.binaryResources)
implementation(libs.coil) implementation(libs.coil)
implementation(variantOf(libs.zip) { artifactType("aar") }) implementation(variantOf(libs.zip) { artifactType("aar") })
} }

View file

@ -24,6 +24,7 @@
android:name=".ManagerApplication" android:name=".ManagerApplication"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"> 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 package com.aliucord.manager.installer.steps.patch
import android.content.Context import android.content.Context
import android.os.Build
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.installer.steps.StepGroup
import com.aliucord.manager.installer.steps.StepRunner import com.aliucord.manager.installer.steps.StepRunner
import com.aliucord.manager.installer.steps.base.Step import com.aliucord.manager.installer.steps.base.Step
import com.aliucord.manager.installer.steps.base.StepState 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
import com.aliucord.manager.ui.screens.installopts.InstallOptions.IconReplacement
import com.aliucord.manager.util.getResBytes
import com.github.diamondminer88.zip.ZipWriter 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.KoinComponent
import org.koin.core.component.inject 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 @Stable
class ReplaceIconStep(private val options: InstallOptions) : Step(), KoinComponent { 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 val localizedName = R.string.install_step_patch_icon
override suspend fun execute(container: StepRunner) { 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 state = StepState.Skipped
return return
} }
val apk = container.getStep<CopyDependenciesStep>().patchedApk 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 { ZipWriter(apk, /* append = */ true).use {
val foregroundIcon = readAsset("icons/ic_logo_foreground.png") if (options.monochromeIcon) {
val squareIcon = readAsset("icons/ic_logo_square.png") it.writeEntry("res/ic_aliucord_monochrome.xml", context.getResBytes(R.drawable.ic_discord_monochrome))
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.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 package com.aliucord.manager.ui.screens.installopts
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import java.io.Serializable import androidx.compose.ui.graphics.Color
@Immutable @Immutable
data class InstallOptions( data class InstallOptions(
/**
* The app name that's user-facing in launchers.
*/
val appName: String, val appName: String,
/**
* Changes the installation package name.
*/
val packageName: String, val packageName: String,
/**
* Adding the debuggable APK flag.
*/
val debuggable: Boolean, 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, appName = appName,
packageName = packageName, packageName = packageName,
debuggable = debuggable, 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.os.*
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.util.TypedValue
import android.widget.Toast import android.widget.Toast
import androidx.annotation.AnyRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.aliucord.manager.BuildConfig import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R import com.aliucord.manager.R
import java.io.File import java.io.File
import java.io.InputStream
fun Context.copyToClipboard(text: String) { fun Context.copyToClipboard(text: String) {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
@ -86,3 +89,24 @@ fun Context.requestNoBatteryOptimizations() {
startActivity(intent) 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 package com.aliucord.manager.util
import androidx.collection.ObjectList
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.truncate import kotlin.math.truncate
@ -10,3 +11,12 @@ fun Double.toPrecision(decimals: Int): Double {
val multiplier = 10.0.pow(decimals) val multiplier = 10.0.pow(decimals)
return truncate(this * multiplier) / multiplier 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] [versions]
accompanist = "0.32.0" accompanist = "0.32.0"
agp = "8.2.1" agp = "8.2.2"
androidx-activity = "1.8.2" androidx-activity = "1.8.2"
androidx-core = "1.12.0" androidx-core = "1.12.0"
androidx-splashscreen = "1.0.1" androidx-splashscreen = "1.0.1"
androidx-lifecycle = "2.7.0" androidx-lifecycle = "2.7.0"
apksig = "7.4.2" apksig = "7.4.2"
axml = "1.0.1" axml = "1.0.1"
binary-resources = "1.0.0"
bouncycastle = "1.70" bouncycastle = "1.70"
coil = "2.5.0" coil = "2.5.0"
compose = "1.5.4" compose = "1.5.4"
@ -19,7 +20,7 @@ kotlinx-immutable = "0.3.7"
kotlinx-serialization = "1.5.0" kotlinx-serialization = "1.5.0"
ktor = "2.3.7" ktor = "2.3.7"
voyager = "1.0.0" voyager = "1.0.0"
zip = "2.1.0" zip = "2.1.1"
[libraries] [libraries]
# Accompanist # Accompanist
@ -68,6 +69,7 @@ voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voy
# Other # Other
apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" } apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" }
axml = { module = "com.aliucord:axml", version.ref = "axml" } 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" } bouncycastle = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bouncycastle" }
coil = { module = "io.coil-kt:coil-compose", version.ref = "coil" } 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 zip = { module = "io.github.diamondminer88:zip-android", version.ref = "zip" } # Only use the "aar" artifact