fuzzyworld

haunt / commit

Add app icon, adaptive icon support, and generateIcons Gradle task

35fda751ff28a683816c7d6a84e6476066c101fd · Pavel Safronov · 2026-05-13 14:00:14 -0700 · parents 5dd0d6717ebc

Add app icon, adaptive icon support, and generateIcons Gradle task

- location.png (512×512 RGBA) is the canonical source icon
- Legacy mipmap PNGs generated at mdpi/hdpi/xhdpi/xxhdpi/xxxhdpi with
  #ADD8E6 background flattened in for pre-API-26 launchers
- Adaptive icon (API 26+): mipmap-anydpi-v26/ic_launcher.xml with
  @color/ic_launcher_background (#ADD8E6) + @drawable/ic_launcher_foreground
- Foreground PNG kept transparent, centered in 432×432 safe-zone canvas
- gradle :app:generateIcons regenerates all sizes from location.png;
  uses ProcessBuilder+magick, incremental via Gradle input/output tracking
- AndroidManifest: android:icon and android:roundIcon point to @mipmap/ic_launcher
- README: document generateIcons task, orientation mode, haunt mode;
  update development status table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
 README.md                                          |  37 +++++++++++---
 app/build.gradle.kts                               |  56 +++++++++++++++++++++
 app/src/main/AndroidManifest.xml                   |   2 +
 .../main/res/drawable/ic_launcher_foreground.png   | Bin 0 -> 9987 bytes
 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml |   5 ++
 app/src/main/res/mipmap-hdpi/ic_launcher.png       | Bin 0 -> 5785 bytes
 app/src/main/res/mipmap-mdpi/ic_launcher.png       | Bin 0 -> 3598 bytes
 app/src/main/res/mipmap-xhdpi/ic_launcher.png      | Bin 0 -> 8132 bytes
 app/src/main/res/mipmap-xxhdpi/ic_launcher.png     | Bin 0 -> 12492 bytes
 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png    | Bin 0 -> 16882 bytes
 app/src/main/res/values/colors.xml                 |   4 ++
 location.png                                       | Bin 0 -> 18550 bytes
 12 files changed, 96 insertions(+), 8 deletions(-)

diff --git a/README.md b/README.md
index 1480b8b..3550459 100644
--- a/README.md
+++ b/README.md
@@ -38,18 +38,39 @@ Run the app in debug mode — it lists every RTT-capable AP with its BSSID. Walk
 
 Requires Android Studio Ladybug or later. On first open, let Gradle sync finish and generate the wrapper JAR, then run on a physical device (RTT does not work in the emulator).
 
+## App icon
+
+The launcher icon is generated from `location.png` in the repo root. After replacing that file, regenerate all mipmap sizes and the adaptive-icon foreground with:
+
+```bash
+gradle :app:generateIcons
+```
+
+To change the background color, edit the `bgColor` variable at the top of the `generateIcons` task in `app/build.gradle.kts`. The task is incremental — Gradle skips it if `location.png` hasn't changed.
+
+## Orientation mode
+
+The map has a built-in compass mode. Tap **⊙ Orient** in the toolbar to enable it. The map then rotates to keep the phone's forward direction at the top, and a red **N** indicator appears at the edge showing where north is.
+
+**Calibration:** face your known north, hold the phone pointing that way, then drag the **N** circle to the top of the screen. The offset is saved and persists across restarts.
+
+## Haunt mode
+
+Tap **Haunt** to enable haunt mode. While active, when you walk close to a registered Hue light (within ~1 m), that light blinks on and off slowly. When you move away or turn haunt mode off, the light returns to the state it was in before blinking started.
+
 ## Development status
 
 | Layer | Status | Description |
-|---|---|---|
+| --- | --- | --- |
 | 1 | ✅ | Project scaffold, permissions, RTT availability check |
-| 2 | ✅ | RTT ranging, debug map, add/rename/delete points |
-| 3 | — | CalibrationStore — JSON zone fingerprints |
-| 4 | — | ZoneMatcher — nearest-neighbour in RTT space |
-| 5 | — | Calibration UI — walk-and-tap flow |
-| 6 | — | HueBridgeClient — Retrofit, toggle light by ID |
-| 7 | — | Live mode + debounce logic |
-| 8 | — | Tuning N (readings averaged) and K (debounce count) |
+| 2 | ✅ | RTT ranging, debug map, add/rename/delete points, history trail |
+| 3 | ✅ | Hue Bridge discovery, pairing, light/group listing and blinking |
+| 4 | ✅ | Haunt mode — proximity detection, blink loop, state restoration |
+| 5 | ✅ | Compass orientation mode — live map rotation, N indicator, calibration |
+| 6 | ✅ | LAN device scanner — ARP + mDNS, pin network devices to map |
+| 7 | — | ZoneMatcher — automatic light switching without haunt button |
+| 8 | — | Background service — keep ranging alive when app is backgrounded |
+| 9 | — | Tuning — average N readings, debounce K zone changes |
 
 ## Package
 
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index da73a31..0d5b7f4 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -35,6 +35,62 @@ android {
     }
 }
 
+// ---------------------------------------------------------------------------
+// generateIcons task
+//
+// Reads location.png from the project root and writes all mipmap PNGs plus
+// the adaptive-icon foreground. Run with:
+//   ./gradlew :app:generateIcons
+//
+// The source file and background color are the only things you'd ever change.
+// ---------------------------------------------------------------------------
+tasks.register("generateIcons") {
+    val src = rootProject.file("location.png")
+    val resDir = file("src/main/res")
+    val bgColor = "#ADD8E6"
+
+    inputs.file(src)
+    outputs.dir(resDir)
+
+    doLast {
+        if (!src.exists()) error("Source icon not found: ${src.absolutePath}")
+
+        fun magick(vararg args: String) {
+            val cmd = listOf("magick") + args.toList()
+            val proc = ProcessBuilder(cmd).redirectErrorStream(true).start()
+            val out = proc.inputStream.bufferedReader().readText()
+            val exit = proc.waitFor()
+            if (exit != 0) error("magick failed (exit $exit):\n$out")
+        }
+
+        data class Density(val dir: String, val size: Int)
+        val densities = listOf(
+            Density("mipmap-mdpi",    48),
+            Density("mipmap-hdpi",    72),
+            Density("mipmap-xhdpi",   96),
+            Density("mipmap-xxhdpi",  144),
+            Density("mipmap-xxxhdpi", 192),
+        )
+
+        // Legacy flat icons — background baked in
+        for ((dir, size) in densities) {
+            val out = resDir.resolve("$dir/ic_launcher.png")
+            out.parentFile.mkdirs()
+            magick(src.absolutePath, "-background", bgColor, "-flatten",
+                "-resize", "${size}x${size}", out.absolutePath)
+            println("  ✓ $dir/ic_launcher.png (${size}×${size})")
+        }
+
+        // Adaptive foreground — transparent, centered in 432×432 safe-zone canvas
+        val fg = resDir.resolve("drawable/ic_launcher_foreground.png")
+        fg.parentFile.mkdirs()
+        magick(src.absolutePath, "-background", "none",
+            "-resize", "264x264", "-gravity", "center", "-extent", "432x432",
+            fg.absolutePath)
+        println("  ✓ drawable/ic_launcher_foreground.png (432×432)")
+    }
+}
+
 dependencies {
     implementation(libs.androidx.appcompat)
     implementation(libs.androidx.activity.ktx)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 579bd77..d7b7ed9 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -12,6 +12,8 @@
 
     <application
         android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:roundIcon="@mipmap/ic_launcher"
         android:label="@string/app_name"
         android:supportsRtl="true"
         android:theme="@style/Theme.Haunt"
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.png b/app/src/main/res/drawable/ic_launcher_foreground.png
new file mode 100644
index 0000000..52dd70c
Binary files /dev/null and b/app/src/main/res/drawable/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..a8a8fa5
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..bdedd88
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..7556fb6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..54e3437
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..73af283
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..181136e
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..d232ad9
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="ic_launcher_background">#ADD8E6</color>
+</resources>
diff --git a/location.png b/location.png
new file mode 100644
index 0000000..1638259
Binary files /dev/null and b/location.png differ