feat: patch icons on Android <8 (no adaptive icons)
This commit is contained in:
parent
5edc39f289
commit
7c2a422094
7 changed files with 150 additions and 27 deletions
|
|
@ -27,7 +27,7 @@ class KotlinPatchRunner(options: PatchOptions) : StepRunner() {
|
|||
|
||||
// Patch
|
||||
SmaliPatchStep(),
|
||||
ReplaceIconStep(options),
|
||||
PatchIconsStep(options),
|
||||
PatchManifestStep(options),
|
||||
ReorganizeDexStep(),
|
||||
AddAliuhookLibsStep(),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,17 @@
|
|||
package com.aliucord.manager.patcher.steps.patch
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.InsetDrawable
|
||||
import android.os.Build
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.applyCanvas
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.patcher.StepRunner
|
||||
import com.aliucord.manager.patcher.steps.StepGroup
|
||||
|
|
@ -15,15 +24,17 @@ import com.aliucord.manager.patcher.util.ArscUtil.addResource
|
|||
import com.aliucord.manager.patcher.util.ArscUtil.getMainArscChunk
|
||||
import com.aliucord.manager.patcher.util.ArscUtil.getPackageChunk
|
||||
import com.aliucord.manager.patcher.util.ArscUtil.getResourceFileName
|
||||
import com.aliucord.manager.patcher.util.ArscUtil.getResourceFileNames
|
||||
import com.aliucord.manager.patcher.util.AxmlUtil
|
||||
import com.aliucord.manager.ui.screens.patchopts.PatchOptions
|
||||
import com.aliucord.manager.ui.screens.patchopts.PatchOptions.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 com.google.devrel.gmscore.tools.apk.arsc.*
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Patch the mipmap-v26 launcher icons' background, foreground and monochrome attributes,
|
||||
|
|
@ -32,21 +43,23 @@ import org.koin.core.component.inject
|
|||
* so that this works on pretty much almost any APK.
|
||||
*/
|
||||
@Stable
|
||||
class ReplaceIconStep(private val options: PatchOptions) : Step(), KoinComponent {
|
||||
class PatchIconsStep(private val options: PatchOptions) : Step(), KoinComponent {
|
||||
private val context: Context by inject()
|
||||
|
||||
override val group = StepGroup.Patch
|
||||
override val localizedName = R.string.patch_step_patch_icon
|
||||
|
||||
// Adaptive icons are available starting with Android 8
|
||||
private val isAdaptiveIconsAvailable = Build.VERSION.SDK_INT >= 28
|
||||
|
||||
// Monochrome icons are always patched starting with Android 12
|
||||
private val isMonochromeIconsAvailable = Build.VERSION.SDK_INT >= 31
|
||||
|
||||
override suspend fun execute(container: StepRunner) {
|
||||
val isAdaptiveIconsAvailable = Build.VERSION.SDK_INT >= 28
|
||||
val isMonochromeIconsAvailable = Build.VERSION.SDK_INT >= 31
|
||||
container.log("isAdaptiveIconsAvailable: $isAdaptiveIconsAvailable, isMonochromeIconsAvailable: $isMonochromeIconsAvailable")
|
||||
|
||||
// Adaptive icons are available starting with Android 8
|
||||
// Monochrome icons are always patched starting with Android 12
|
||||
// Skip if adaptive icons are unavailable or { not patching main icon AND monochrome icons are unavailable }
|
||||
if (!isAdaptiveIconsAvailable || (!isMonochromeIconsAvailable && options.iconReplacement is IconReplacement.Original)) {
|
||||
// Skip if not patching main icon AND monochrome icons are unavailable
|
||||
if (!isMonochromeIconsAvailable && options.iconReplacement is IconReplacement.Original) {
|
||||
container.log("No patching necessary, skipping step")
|
||||
state = StepState.Skipped
|
||||
return
|
||||
|
|
@ -56,6 +69,26 @@ class ReplaceIconStep(private val options: PatchOptions) : Step(), KoinComponent
|
|||
val apk = container.getStep<CopyDependenciesStep>().patchedApk
|
||||
val arsc = ArscUtil.readArsc(apk)
|
||||
|
||||
if (isAdaptiveIconsAvailable) {
|
||||
patchAdaptiveIcons(
|
||||
container = container,
|
||||
apk = apk,
|
||||
arsc = arsc,
|
||||
)
|
||||
} else {
|
||||
patchRawIcons(
|
||||
container = container,
|
||||
apk = apk,
|
||||
arsc = arsc,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun patchAdaptiveIcons(
|
||||
container: StepRunner,
|
||||
apk: File,
|
||||
arsc: BinaryResourceFile,
|
||||
) {
|
||||
container.log("Parsing AndroidManifest.xml and obtaining adaptive square/round icon file paths")
|
||||
val iconRscIds = AxmlUtil.readManifestIconInfo(apk)
|
||||
val squareIconFile = arsc.getMainArscChunk().getResourceFileName(iconRscIds.squareIcon, "anydpi-v26")
|
||||
|
|
@ -130,8 +163,70 @@ class ReplaceIconStep(private val options: PatchOptions) : Step(), KoinComponent
|
|||
|
||||
container.log("Writing resources unaligned compressed")
|
||||
it.deleteEntry("resources.arsc")
|
||||
// This doesn't need to be aligned and uncompressed here, since it that is done during AlignmentSte
|
||||
// This doesn't need to be aligned and uncompressed here, since it that is done during AlignmentStep
|
||||
it.writeEntry("resources.arsc", arsc.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
private fun patchRawIcons(
|
||||
container: StepRunner,
|
||||
apk: File,
|
||||
arsc: BinaryResourceFile,
|
||||
) {
|
||||
container.log("Parsing AndroidManifest.xml and obtaining all square/round launcher icon file paths")
|
||||
val iconRscIds = AxmlUtil.readManifestIconInfo(apk)
|
||||
val squareIconFiles = arsc.getMainArscChunk().getResourceFileNames(iconRscIds.squareIcon)
|
||||
val roundIconFiles = arsc.getMainArscChunk().getResourceFileNames(iconRscIds.roundIcon)
|
||||
val allIconFiles = squareIconFiles + roundIconFiles
|
||||
|
||||
val backgroundColor = when (options.iconReplacement) {
|
||||
IconReplacement.Original -> IconReplacement.Aliucord.color
|
||||
is IconReplacement.CustomColor -> options.iconReplacement.color
|
||||
is IconReplacement.CustomImage -> error("Cannot patch custom images below Android 8 (Adaptive Icons are required)")
|
||||
}
|
||||
|
||||
container.log("Generating static launcher icon")
|
||||
val icon = createDiscordLauncherIcon(backgroundColor)
|
||||
val iconBytes = ByteArrayOutputStream().use {
|
||||
icon.compress(Bitmap.CompressFormat.PNG, 0, it)
|
||||
icon.recycle()
|
||||
it.toByteArray()
|
||||
}
|
||||
|
||||
container.log("Writing patched icons back to apk")
|
||||
ZipWriter(apk, /* append = */ true).use {
|
||||
it.deleteEntries(allIconFiles)
|
||||
|
||||
for (path in allIconFiles)
|
||||
it.writeEntry(path, iconBytes)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the Discord icon over a custom colored background and rounds it
|
||||
* to create a launcher icon like Discord's. This is used for devices that
|
||||
* do not support adaptive icons.
|
||||
*/
|
||||
private fun createDiscordLauncherIcon(backgroundColor: Color, size: Int = 192): Bitmap {
|
||||
val paint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
setColor(backgroundColor.toArgb())
|
||||
}
|
||||
|
||||
val vectorDrawable = ContextCompat.getDrawable(context, R.drawable.ic_discord)!!
|
||||
val drawable = InsetDrawable(vectorDrawable, (size * .17f).toInt())
|
||||
|
||||
val icon = createBitmap(size, size).applyCanvas {
|
||||
drawRect(Rect(0, 0, width, height), paint)
|
||||
drawable.setBounds(0, 0, width, height)
|
||||
drawable.draw(this)
|
||||
}
|
||||
val iconRound = RoundedBitmapDrawableFactory.create(context.resources, icon).apply {
|
||||
isCircular = true
|
||||
setAntiAlias(true)
|
||||
}.toBitmap()
|
||||
|
||||
icon.recycle()
|
||||
return iconRound
|
||||
}
|
||||
}
|
||||
|
|
@ -125,7 +125,7 @@ object ArscUtil {
|
|||
*
|
||||
* @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.
|
||||
* @return The string value of the resource, which should be a file path into the apk.
|
||||
*/
|
||||
fun ResourceTableChunk.getResourceFileName(
|
||||
resourceId: BinaryResourceIdentifier,
|
||||
|
|
@ -152,4 +152,31 @@ object ArscUtil {
|
|||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* In an arsc file, for a specific resource in all configurations, get all values.
|
||||
*
|
||||
* @param resourceId The target resource id.
|
||||
* @return The string value of all configuration values of the resource, which should be file paths into the apk.
|
||||
*/
|
||||
fun ResourceTableChunk.getResourceFileNames(resourceId: BinaryResourceIdentifier): List<String> {
|
||||
val packageChunk = this.packages.find { it.id == resourceId.packageId() }
|
||||
?: error("Unable to find target resource")
|
||||
|
||||
val typeChunks = packageChunk.getTypeChunks(resourceId.typeId())
|
||||
val entries = typeChunks.map { typeChunk ->
|
||||
val entry = typeChunk.getEntry(resourceId.entryId())
|
||||
?: error("Unable to find target resource in type chunk " + typeChunk.configuration)
|
||||
|
||||
if (entry.isComplex || entry.value().type() != BinaryResourceValue.Type.STRING)
|
||||
error("Target resource value type in type chunk ${typeChunk.configuration} is not STRING")
|
||||
|
||||
entry
|
||||
}
|
||||
|
||||
return entries.map { entry ->
|
||||
val valueIdx = entry.value().data()
|
||||
this.stringPool.getString(valueIdx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.aliucord.manager.ui.screens.iconopts
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
|
|
@ -74,6 +75,8 @@ fun IconOptionsScreenContent(
|
|||
selectedImage: () -> ByteArray?,
|
||||
setSelectedImage: (Uri) -> Unit,
|
||||
) {
|
||||
val isAdaptiveIconsAvailable = Build.VERSION.SDK_INT >= 28
|
||||
|
||||
Scaffold(
|
||||
topBar = { IconOptionsAppBar() },
|
||||
) { paddingValues ->
|
||||
|
|
@ -107,12 +110,14 @@ fun IconOptionsScreenContent(
|
|||
selected = mode == IconOptionsMode.CustomColor,
|
||||
onClick = remember { { setMode(IconOptionsMode.CustomColor) } },
|
||||
)
|
||||
RadioSelectorItem(
|
||||
name = stringResource(R.string.iconopts_variant_title_image),
|
||||
description = stringResource(R.string.iconopts_variant_desc_image),
|
||||
selected = mode == IconOptionsMode.CustomImage,
|
||||
onClick = remember { { setMode(IconOptionsMode.CustomImage) } },
|
||||
)
|
||||
if (isAdaptiveIconsAvailable) {
|
||||
RadioSelectorItem(
|
||||
name = stringResource(R.string.iconopts_variant_title_image),
|
||||
description = stringResource(R.string.iconopts_variant_desc_image),
|
||||
selected = mode == IconOptionsMode.CustomImage,
|
||||
onClick = remember { { setMode(IconOptionsMode.CustomImage) } },
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(Modifier.padding(vertical = 16.dp))
|
||||
|
||||
|
|
|
|||
|
|
@ -142,7 +142,6 @@ fun PatchOptionsScreenContent(
|
|||
|
||||
TextDivider(text = stringResource(R.string.patchopts_divider_basic))
|
||||
|
||||
// TODO: hide when below to Android 8
|
||||
IconPatchOption(
|
||||
icon = painterResource(R.drawable.ic_app_shortcut),
|
||||
name = stringResource(R.string.patchopts_icon_title),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="19dp"
|
||||
android:viewportWidth="71"
|
||||
android:viewportHeight="55">
|
||||
<group>
|
||||
<clip-path android:pathData="M0,0h71v55h-71z" />
|
||||
android:height="24dp"
|
||||
android:viewportWidth="256"
|
||||
android:viewportHeight="256">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M60.1045,4.8978C55.5792,2.8214 50.7265,1.2916 45.6527,0.4154C45.5603,0.3985 45.468,0.4408 45.4204,0.5253C44.7963,1.6353 44.105,3.0834 43.6209,4.2216C38.1637,3.4046 32.7345,3.4046 27.3892,4.2216C26.905,3.0581 26.1886,1.6353 25.5617,0.5253C25.5141,0.4436 25.4218,0.4013 25.3294,0.4154C20.2584,1.2888 15.4057,2.8186 10.8776,4.8978C10.8384,4.9147 10.8048,4.9429 10.7825,4.9795C1.578,18.7309 -0.9436,32.1443 0.2934,45.3914C0.299,45.4562 0.3354,45.5182 0.3858,45.5576C6.4587,50.0174 12.3413,52.7249 18.1147,54.5195C18.2071,54.5477 18.305,54.5139 18.3638,54.4378C19.7295,52.5728 20.9469,50.6063 21.9907,48.5383C22.0523,48.4172 21.9935,48.2735 21.8676,48.2256C19.9366,47.4931 18.0979,46.6 16.3292,45.5858C16.1893,45.5041 16.1781,45.304 16.3068,45.2082C16.679,44.9293 17.0513,44.6391 17.4067,44.3461C17.471,44.2926 17.5606,44.2813 17.6362,44.3151C29.2558,49.6202 41.8354,49.6202 53.3179,44.3151C53.3935,44.2785 53.4831,44.2898 53.5502,44.3433C53.9057,44.6363 54.2779,44.9293 54.6529,45.2082C54.7816,45.304 54.7732,45.5041 54.6333,45.5858C52.8646,46.6197 51.0259,47.4931 49.0921,48.2228C48.9662,48.2707 48.9102,48.4172 48.9718,48.5383C50.038,50.6034 51.2554,52.5699 52.5959,54.435C52.6519,54.5139 52.7526,54.5477 52.845,54.5195C58.6464,52.7249 64.529,50.0174 70.6019,45.5576C70.6551,45.5182 70.6887,45.459 70.6943,45.3942C72.1747,30.0791 68.2147,16.7757 60.1968,4.9823C60.1772,4.9429 60.1437,4.9147 60.1045,4.8978ZM23.7259,37.3253C20.2276,37.3253 17.3451,34.1136 17.3451,30.1693C17.3451,26.225 20.1717,23.0133 23.7259,23.0133C27.308,23.0133 30.1626,26.2532 30.1066,30.1693C30.1066,34.1136 27.28,37.3253 23.7259,37.3253ZM47.3178,37.3253C43.8196,37.3253 40.9371,34.1136 40.9371,30.1693C40.9371,26.225 43.7636,23.0133 47.3178,23.0133C50.9,23.0133 53.7545,26.2532 53.6986,30.1693C53.6986,34.1136 50.9,37.3253 47.3178,37.3253Z" />
|
||||
</group>
|
||||
android:pathData="M216.86,45.1C200.29,37.34 182.57,31.71 164.04,28.5C161.77,32.61 159.11,38.15 157.28,42.55C137.58,39.58 118.07,39.58 98.74,42.55C96.91,38.15 94.19,32.61 91.9,28.5C73.35,31.71 55.61,37.36 39.04,45.14C5.62,95.65 -3.44,144.9 1.09,193.46C23.26,210.01 44.74,220.07 65.86,226.65C71.08,219.47 75.73,211.84 79.74,203.8C72.1,200.9 64.79,197.32 57.89,193.17C59.72,191.81 61.51,190.39 63.24,188.93C105.37,208.63 151.13,208.63 192.75,188.93C194.51,190.39 196.3,191.81 198.11,193.17C191.18,197.34 183.85,200.92 176.22,203.82C180.23,211.84 184.86,219.49 190.1,226.67C211.24,220.09 232.74,210.03 254.91,193.46C260.23,137.17 245.83,88.37 216.86,45.1ZM85.47,163.59C72.83,163.59 62.46,151.79 62.46,137.41C62.46,123.04 72.61,111.21 85.47,111.21C98.34,111.21 108.71,123.02 108.49,137.41C108.51,151.79 98.34,163.59 85.47,163.59ZM170.53,163.59C157.88,163.59 147.51,151.79 147.51,137.41C147.51,123.04 157.66,111.21 170.53,111.21C183.39,111.21 193.76,123.02 193.54,137.41C193.54,151.79 183.39,163.59 170.53,163.59Z" />
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<group>
|
||||
<clip-path android:pathData="M31.56,36.63l44.88,0l0,34.77l-44.88,0z" />
|
||||
<path
|
||||
android:fillColor="#5865f2"
|
||||
android:fillColor="#FFFFFFFF"
|
||||
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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue