Files
deck_in_a_dash/android/scripts/gen_launcher_icon.py
T
ml 3bc1263294
build / build (push) Successful in 4m8s
Add a standalone Android app with the same daily reading and config page
android/ links engine/src/ and interpreter/src/ via JNI/CMake (real
astronomy.c, not the watch's size-constrained substitute), rendering
through Jetpack Compose with a burger-menu config page mirroring
WatchConfig and a WorkManager-driven daily notification. Independent
of the watch app - neither requires the other.

build-image now bundles the Android SDK/NDK alongside the Pebble SDK
(VERSION bumped to 2), and .gitea/workflows/build.yml/Makefile/
run-image.sh build and publish the APK the same way as the CLI
tarball and .pbw.
2026-07-18 22:41:51 +02:00

80 lines
3.2 KiB
Python

#!/usr/bin/env python3
"""Generates the Android launcher icon from ../../res/logo.png.
Produces:
- mipmap-<density>/ic_launcher_foreground.png - the logo centered,
scaled to fit the adaptive-icon safe zone (66% of a 108dp canvas),
transparent background - paired at runtime with
@color/ic_launcher_background (see mipmap-anydpi-v26/ic_launcher.xml,
committed - a static reference, not generated) for API 26+.
- mipmap-<density>/ic_launcher.png / ic_launcher_round.png - the same
logo flattened onto ic_launcher_background, for API <26 devices that
can't use the adaptive-icon XML at all.
Generated output, not committed (see the repo's root .gitignore's
/android/app/src/main/res/mipmap-* entries) - regenerate by running this
script directly or via Gradle's generateLauncherIcon task (wired into
app's preBuild, see ../app/build.gradle.kts). Requires Pillow, already
part of build-image's toolchain.
"""
import os
import sys
from PIL import Image
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
LOGO_PATH = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", "res", "logo.png"))
RES_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "app", "src", "main", "res"))
BACKGROUND_RGBA = (27, 27, 47, 255) # matches @color/ic_launcher_background
# (density qualifier, adaptive-icon canvas px, legacy launcher px)
DENSITIES = [
("mdpi", 108, 48),
("hdpi", 162, 72),
("xhdpi", 216, 96),
("xxhdpi", 324, 144),
("xxxhdpi", 432, 192),
]
# Adaptive icons render only the inner ~66% "safe zone" reliably across
# launchers that apply their own mask shape.
SAFE_ZONE_RATIO = 0.66
def resized_logo(logo, target_px):
ratio = target_px / max(logo.width, logo.height)
size = (round(logo.width * ratio), round(logo.height * ratio))
return logo.resize(size, Image.LANCZOS)
def main():
logo = Image.open(LOGO_PATH).convert("RGBA")
for density, canvas_px, legacy_px in DENSITIES:
mipmap_dir = os.path.join(RES_DIR, f"mipmap-{density}")
os.makedirs(mipmap_dir, exist_ok=True)
# Adaptive-icon foreground: transparent canvas, logo scaled to the
# safe zone and centered.
foreground = Image.new("RGBA", (canvas_px, canvas_px), (0, 0, 0, 0))
fg_logo = resized_logo(logo, round(canvas_px * SAFE_ZONE_RATIO))
offset = ((canvas_px - fg_logo.width) // 2, (canvas_px - fg_logo.height) // 2)
foreground.paste(fg_logo, offset, fg_logo)
foreground.save(os.path.join(mipmap_dir, "ic_launcher_foreground.png"))
# Legacy (<API26) flattened fallback, same for round and square -
# launchers on those versions apply their own circular crop.
legacy = Image.new("RGBA", (legacy_px, legacy_px), BACKGROUND_RGBA)
legacy_logo = resized_logo(logo, round(legacy_px * SAFE_ZONE_RATIO))
legacy_offset = ((legacy_px - legacy_logo.width) // 2, (legacy_px - legacy_logo.height) // 2)
legacy.paste(legacy_logo, legacy_offset, legacy_logo)
legacy.convert("RGB").save(os.path.join(mipmap_dir, "ic_launcher.png"))
legacy.convert("RGB").save(os.path.join(mipmap_dir, "ic_launcher_round.png"))
print(f"wrote launcher icons for {len(DENSITIES)} densities to {RES_DIR}", file=sys.stderr)
if __name__ == "__main__":
main()