Skip to content

Commit f22aa3f

Browse files
authored
fix: Create adaptive icons for all resolutions (#171)
1 parent 73316e3 commit f22aa3f

2 files changed

Lines changed: 124 additions & 90 deletions

File tree

app/src/main/java/app/revanced/manager/ui/screen/shared/AdaptiveIconCreatorDialog.kt

Lines changed: 118 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,17 @@ private object AdaptiveIconConfig {
5858
const val BACKGROUND_FILE_NAME = "morphe_adaptive_background_custom.png"
5959
const val FOREGROUND_FILE_NAME = "morphe_adaptive_foreground_custom.png"
6060

61-
// Density folders and sizes
62-
val DENSITY_CONFIGS = mapOf(
63-
160 to ("mipmap-mdpi" to 108),
64-
240 to ("mipmap-hdpi" to 162),
65-
320 to ("mipmap-xhdpi" to 216),
66-
480 to ("mipmap-xxhdpi" to 324),
67-
Int.MAX_VALUE to ("mipmap-xxxhdpi" to 432)
61+
// Density folders and sizes - all densities will be generated
62+
val DENSITY_CONFIGS = listOf(
63+
DensityConfig("mipmap-mdpi", 108),
64+
DensityConfig("mipmap-hdpi", 162),
65+
DensityConfig("mipmap-xhdpi", 216),
66+
DensityConfig("mipmap-xxhdpi", 324),
67+
DensityConfig("mipmap-xxxhdpi", 432)
6868
)
6969

70+
data class DensityConfig(val folderName: String, val size: Int)
71+
7072
// Transform constraints
7173
const val MIN_SCALE = 0.5f
7274
const val MAX_SCALE = 3.0f
@@ -154,7 +156,7 @@ fun AdaptiveIconCreatorDialog(
154156
uri?.let {
155157
scope.launch(Dispatchers.IO) {
156158
try {
157-
val success = createAdaptiveIcon(
159+
val success = createAdaptiveIcons(
158160
context = context,
159161
baseUri = it,
160162
foregroundBitmap = foregroundBitmap!!,
@@ -571,11 +573,11 @@ private fun SafeZoneLegendItem(
571573
}
572574

573575
/**
574-
* Create adaptive icon files in proper structure
576+
* Create adaptive icon files for all densities in proper structure
575577
* Returns the path to morphe_icons folder or null if failed
576578
*/
577579
@SuppressLint("UseKtx")
578-
private suspend fun createAdaptiveIcon(
580+
private suspend fun createAdaptiveIcons(
579581
context: Context,
580582
baseUri: Uri,
581583
foregroundBitmap: Bitmap,
@@ -585,111 +587,137 @@ private suspend fun createAdaptiveIcon(
585587
offsetY: Float
586588
): String? = withContext(Dispatchers.IO) {
587589
try {
588-
// Get current device density
589-
val displayMetrics = context.resources.displayMetrics
590-
val density = displayMetrics.densityDpi
591-
592-
// Determine which folder to create based on density
593-
val (folderName, targetSize) = AdaptiveIconConfig.DENSITY_CONFIGS
594-
.entries
595-
.firstOrNull { density <= it.key }
596-
?.value
597-
?: AdaptiveIconConfig.DENSITY_CONFIGS[Int.MAX_VALUE]!!
598-
599590
// Convert URI to File path using existing utility
600591
val basePath = baseUri.toFilePath()
601592
val baseDir = File(basePath)
602593

603-
// Create directory structure: morphe_branding/morphe_icons/mipmap-xxx
594+
// Create directory structure: morphe_branding/morphe_icons
604595
val brandingDir = File(baseDir, AdaptiveIconConfig.BRANDING_FOLDER_NAME)
605596
if (!brandingDir.exists()) brandingDir.mkdirs()
606597

598+
// Create .nomedia file to prevent icons from appearing in gallery
599+
val nomediaFile = File(brandingDir, ".nomedia")
600+
if (!nomediaFile.exists()) {
601+
nomediaFile.createNewFile()
602+
}
603+
607604
val iconsDir = File(brandingDir, AdaptiveIconConfig.ICONS_FOLDER_NAME)
608605
if (!iconsDir.exists()) iconsDir.mkdirs()
609606

610-
val mipmapDir = File(iconsDir, folderName)
611-
if (!mipmapDir.exists()) mipmapDir.mkdirs()
612-
613-
// Create background bitmap (solid color)
614-
val backgroundBitmap = createBitmap(targetSize, targetSize)
615-
val canvas = Canvas(backgroundBitmap)
616-
val rgb = parseColorToRgb(backgroundColor)
617-
val paint = Paint().apply {
618-
color = android.graphics.Color.rgb(
619-
(rgb.first * 255).toInt(),
620-
(rgb.second * 255).toInt(),
621-
(rgb.third * 255).toInt()
607+
// Get preview density for offset calculations
608+
val previewDensity = context.resources.displayMetrics.density
609+
610+
// Create icons for all densities
611+
AdaptiveIconConfig.DENSITY_CONFIGS.forEach { densityConfig ->
612+
createIconsForDensity(
613+
iconsDir = iconsDir,
614+
densityConfig = densityConfig,
615+
foregroundBitmap = foregroundBitmap,
616+
backgroundColor = backgroundColor,
617+
scale = scale,
618+
offsetX = offsetX,
619+
offsetY = offsetY,
620+
previewDensity = previewDensity
622621
)
623622
}
624-
canvas.drawRect(0f, 0f, targetSize.toFloat(), targetSize.toFloat(), paint)
625-
626-
// Create foreground bitmap with scaling and offset
627-
val foregroundScaled = createBitmap(targetSize, targetSize)
628-
val foregroundCanvas = Canvas(foregroundScaled)
629623

630-
// Get density to convert preview offsets to target offsets
631-
val previewDensity = context.resources.displayMetrics.density
624+
// Return path to 'morphe_icons' folder
625+
iconsDir.absolutePath
626+
} catch (e: Exception) {
627+
e.printStackTrace()
628+
null
629+
}
630+
}
632631

633-
// Preview canvas size in pixels
634-
val previewCanvasSize = targetSize * previewDensity
632+
/**
633+
* Create icon files for a specific density
634+
*/
635+
private fun createIconsForDensity(
636+
iconsDir: File,
637+
densityConfig: AdaptiveIconConfig.DensityConfig,
638+
foregroundBitmap: Bitmap,
639+
backgroundColor: String,
640+
scale: Float,
641+
offsetX: Float,
642+
offsetY: Float,
643+
previewDensity: Float
644+
) {
645+
val targetSize = densityConfig.size
646+
647+
// Create mipmap directory
648+
val mipmapDir = File(iconsDir, densityConfig.folderName)
649+
if (!mipmapDir.exists()) mipmapDir.mkdirs()
650+
651+
// Create background bitmap (solid color)
652+
val backgroundBitmap = createBitmap(targetSize, targetSize)
653+
val canvas = Canvas(backgroundBitmap)
654+
val rgb = parseColorToRgb(backgroundColor)
655+
val paint = Paint().apply {
656+
color = android.graphics.Color.rgb(
657+
(rgb.first * 255).toInt(),
658+
(rgb.second * 255).toInt(),
659+
(rgb.third * 255).toInt()
660+
)
661+
}
662+
canvas.drawRect(0f, 0f, targetSize.toFloat(), targetSize.toFloat(), paint)
635663

636-
// Calculate base size by fitting image to canvas (same logic as preview)
637-
val imageAspect = foregroundBitmap.width.toFloat() / foregroundBitmap.height.toFloat()
638-
val canvasAspect = previewCanvasSize / previewCanvasSize // 1.0 for square
664+
// Create foreground bitmap with scaling and offset
665+
val foregroundScaled = createBitmap(targetSize, targetSize)
666+
val foregroundCanvas = Canvas(foregroundScaled)
639667

640-
val (baseWidth, baseHeight) = if (imageAspect > canvasAspect) {
641-
// Image is wider - fit to width
642-
previewCanvasSize to (previewCanvasSize / imageAspect)
643-
} else {
644-
// Image is taller - fit to height
645-
(previewCanvasSize * imageAspect) to previewCanvasSize
646-
}
668+
// Preview canvas size in pixels
669+
val previewCanvasSize = AdaptiveIconConfig.PREVIEW_SIZE.value * previewDensity
647670

648-
// Apply user scale to the fitted size
649-
val scaledWidth = baseWidth * scale
650-
val scaledHeight = baseHeight * scale
671+
// Calculate base size by fitting image to canvas (same logic as preview)
672+
val imageAspect = foregroundBitmap.width.toFloat() / foregroundBitmap.height.toFloat()
673+
val canvasAspect = 1.0f // Square canvas
651674

652-
// Convert to target bitmap coordinates
653-
val targetScaledWidth = scaledWidth / previewDensity
654-
val targetScaledHeight = scaledHeight / previewDensity
675+
val (baseWidth, baseHeight) = if (imageAspect > canvasAspect) {
676+
// Image is wider - fit to width
677+
previewCanvasSize to (previewCanvasSize / imageAspect)
678+
} else {
679+
// Image is taller - fit to height
680+
(previewCanvasSize * imageAspect) to previewCanvasSize
681+
}
655682

656-
// Convert offsets from preview canvas pixels to target bitmap pixels
657-
val targetOffsetX = offsetX / previewDensity
658-
val targetOffsetY = offsetY / previewDensity
683+
// Apply user scale to the fitted size
684+
val scaledWidth = baseWidth * scale
685+
val scaledHeight = baseHeight * scale
659686

660-
val left = (targetSize - targetScaledWidth) / 2 + targetOffsetX
661-
val top = (targetSize - targetScaledHeight) / 2 + targetOffsetY
687+
// Convert to target bitmap coordinates
688+
val targetScaledWidth = scaledWidth * (targetSize / previewCanvasSize)
689+
val targetScaledHeight = scaledHeight * (targetSize / previewCanvasSize)
662690

663-
// Create Paint with anti-aliasing and bicubic filtering for high-quality scaling
664-
val bitmapPaint = Paint().apply {
665-
isAntiAlias = true
666-
isFilterBitmap = true
667-
isDither = true
668-
}
691+
// Convert offsets from preview canvas pixels to target bitmap pixels
692+
val targetOffsetX = offsetX * (targetSize / previewCanvasSize)
693+
val targetOffsetY = offsetY * (targetSize / previewCanvasSize)
669694

670-
val destRect = RectF(left, top, left + targetScaledWidth, top + targetScaledHeight)
671-
foregroundCanvas.drawBitmap(foregroundBitmap, null, destRect, bitmapPaint)
695+
val left = (targetSize - targetScaledWidth) / 2 + targetOffsetX
696+
val top = (targetSize - targetScaledHeight) / 2 + targetOffsetY
672697

673-
// Save background
674-
val backgroundFile = File(mipmapDir, AdaptiveIconConfig.BACKGROUND_FILE_NAME)
675-
FileOutputStream(backgroundFile).use { out ->
676-
backgroundBitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
677-
}
698+
// Create Paint with anti-aliasing and bicubic filtering for high-quality scaling
699+
val bitmapPaint = Paint().apply {
700+
isAntiAlias = true
701+
isFilterBitmap = true
702+
isDither = true
703+
}
678704

679-
// Save foreground
680-
val foregroundFile = File(mipmapDir, AdaptiveIconConfig.FOREGROUND_FILE_NAME)
681-
FileOutputStream(foregroundFile).use { out ->
682-
foregroundScaled.compress(Bitmap.CompressFormat.PNG, 100, out)
683-
}
705+
val destRect = RectF(left, top, left + targetScaledWidth, top + targetScaledHeight)
706+
foregroundCanvas.drawBitmap(foregroundBitmap, null, destRect, bitmapPaint)
684707

685-
// Clean up
686-
backgroundBitmap.recycle()
687-
foregroundScaled.recycle()
708+
// Save background
709+
val backgroundFile = File(mipmapDir, AdaptiveIconConfig.BACKGROUND_FILE_NAME)
710+
FileOutputStream(backgroundFile).use { out ->
711+
backgroundBitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
712+
}
688713

689-
// Return path to 'morphe_icons' folder
690-
iconsDir.absolutePath
691-
} catch (e: Exception) {
692-
e.printStackTrace()
693-
null
714+
// Save foreground
715+
val foregroundFile = File(mipmapDir, AdaptiveIconConfig.FOREGROUND_FILE_NAME)
716+
FileOutputStream(foregroundFile).use { out ->
717+
foregroundScaled.compress(Bitmap.CompressFormat.PNG, 100, out)
694718
}
719+
720+
// Clean up
721+
backgroundBitmap.recycle()
722+
foregroundScaled.recycle()
695723
}

app/src/main/java/app/revanced/manager/ui/screen/shared/HeaderCreatorDialog.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,12 @@ private suspend fun createHeaderFiles(
581581
val brandingDir = File(baseDir, HeaderConfig.BRANDING_FOLDER_NAME)
582582
if (!brandingDir.exists()) brandingDir.mkdirs()
583583

584+
// Create .nomedia file to prevent icons from appearing in gallery
585+
val nomediaFile = File(brandingDir, ".nomedia")
586+
if (!nomediaFile.exists()) {
587+
nomediaFile.createNewFile()
588+
}
589+
584590
val headerDir = File(brandingDir, HeaderConfig.HEADER_FOLDER_NAME)
585591
if (!headerDir.exists()) headerDir.mkdirs()
586592

0 commit comments

Comments
 (0)