From 6032e9fd074c4153417b016f3e3b669ffbb5281a Mon Sep 17 00:00:00 2001 From: Moritz Date: Sun, 15 Feb 2026 17:10:41 +0100 Subject: [PATCH] Production cleanup: enable R8, add ProGuard rules, validate API input - Enable R8 minification and resource shrinking for release builds - Add ProGuard keep rules for Ktor, kotlinx.serialization, Room - Validate hour/minute range in POST /set endpoint - Guard wake lock release on server start failure - Remove unused template colors from colors.xml - Rewrite README with curl examples, security note, troubleshooting Co-Authored-By: Claude Opus 4.6 --- README.md | 91 +++++++++++++------ app/build.gradle.kts | 3 +- app/proguard-rules.pro | 42 +++++---- .../helios_alarm_clock/service/KtorService.kt | 15 ++- app/src/main/res/values/colors.xml | 7 +- 5 files changed, 103 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 26c697a..7bb2fcb 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,80 @@ # Helios Alarm Clock -An Android alarm clock app with a built-in HTTP server, designed to be controlled remotely from a Raspberry Pi or any device on the local network. +An Android alarm clock with an embedded HTTP server for remote control from a Raspberry Pi or any device on the local network. ## Features -- **HTTP API** — Embedded Ktor server (port 8080) for remote alarm management -- **In-app UI** — Set and remove alarms with a Material 3 time picker -- **Reliable alarms** — Uses `AlarmManager.setExactAndAllowWhileIdle()` to fire through Doze mode -- **Full-screen alarm** — Wakes the screen, plays the system alarm sound, and vibrates -- **DND bypass** — Alarm audio uses `USAGE_ALARM` to ring even in Do Not Disturb mode -- **Persistent server** — Foreground service with wake lock keeps the HTTP server alive -- **Boot survival** — Server and alarms reschedule automatically after reboot -- **Auto-cleanup** — Fired alarms are automatically deleted from the database +- **HTTP API** on port 8080 for remote alarm management +- **Material 3 UI** with time picker and alarm list +- **Reliable alarms** via `AlarmManager.setExactAndAllowWhileIdle()` (survives Doze) +- **Full-screen alarm** with system alarm sound and DND bypass +- **Persistent server** as a foreground service with wake lock +- **Boot survival** — server and alarms reschedule after reboot +- **Auto-cleanup** — fired alarms are deleted automatically ## HTTP API -All endpoints are served on port `8080`. +All endpoints listen on port `8080`. Replace `PHONE_IP` with the IP shown in the app. ### Set an alarm -``` -POST /set -Content-Type: application/json - -{"hour": 7, "minute": 30, "label": "Wake up"} +```bash +curl -X POST http://PHONE_IP:8080/set \ + -H "Content-Type: application/json" \ + -d '{"hour": 7, "minute": 30, "label": "Wake up"}' ``` -Returns `201 Created` with `{"id": ""}`. +Response: `201 Created` +```json +{"id": "550e8400-e29b-41d4-a716-446655440000"} +``` + +`hour` must be 0-23, `minute` must be 0-59. `label` is optional (defaults to empty). ### Remove an alarm -``` -POST /rm -Content-Type: application/json - -{"id": ""} +```bash +curl -X POST http://PHONE_IP:8080/rm \ + -H "Content-Type: application/json" \ + -d '{"id": "550e8400-e29b-41d4-a716-446655440000"}' ``` ### List alarms -``` -GET /list +```bash +curl http://PHONE_IP:8080/list ``` Returns a JSON array of all scheduled alarms. +## Security + +The HTTP server has **no authentication**. Anyone on the same network can set or remove alarms. Only run this on a trusted local network. + ## Tech Stack - Kotlin, Jetpack Compose, Material 3 -- MVVM architecture with Hilt dependency injection -- Room database for alarm persistence +- MVVM with Hilt dependency injection +- Room database for persistence - Ktor CIO embedded HTTP server -- AlarmManager with exact alarms -- Foreground service (connectedDevice type) for the HTTP server +- Foreground service (`connectedDevice` type) ## Requirements - Android 8.0+ (API 26) - Target SDK 36 (Android 16) -- Permissions: exact alarms, notifications, foreground service, wake lock, internet + +## Permissions + +| Permission | Purpose | +|---|---| +| `INTERNET` / `ACCESS_NETWORK_STATE` | HTTP server | +| `FOREGROUND_SERVICE_CONNECTED_DEVICE` | Keep server alive | +| `SCHEDULE_EXACT_ALARM` / `USE_EXACT_ALARM` | Precise alarm timing | +| `USE_FULL_SCREEN_INTENT` | Wake screen on alarm | +| `WAKE_LOCK` | Prevent CPU sleep | +| `RECEIVE_BOOT_COMPLETED` | Restart after reboot | +| `POST_NOTIFICATIONS` | Service + alarm notifications | ## Building @@ -66,4 +82,21 @@ Returns a JSON array of all scheduled alarms. ./gradlew assembleRelease ``` -The APK will be at `app/build/outputs/apk/release/app-release.apk`. +The APK is at `app/build/outputs/apk/release/app-release.apk`. + +## Installing via ADB + +```bash +adb install app/build/outputs/apk/release/app-release.apk +``` + +## Troubleshooting + +- **Server not reachable**: Ensure the phone and client are on the same Wi-Fi network. Check that battery optimization is disabled for the app. +- **Alarm doesn't fire**: Grant exact alarm permission in system settings. Disable battery optimization for the app. +- **Notification not showing**: Grant notification permission (Android 13+). +- **Server dies in background**: Some OEMs aggressively kill background services. Disable battery optimization and lock the app in recents. + +## License + +MIT diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 98c3830..47e155b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -34,7 +34,8 @@ android { } buildTypes { release { - isMinifyEnabled = false + isMinifyEnabled = true + isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 481bb43..01b4aba 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,21 +1,27 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html +# Ktor — keep CIO engine and routing +-keep class io.ktor.** { *; } +-keepclassmembers class io.ktor.** { *; } +-dontwarn io.ktor.** -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} +# kotlinx.serialization — keep @Serializable classes +-keepattributes *Annotation*, InnerClasses +-dontnote kotlinx.serialization.AnnotationsKt +-keepclassmembers @kotlinx.serialization.Serializable class ** { + *** Companion; +} +-keepclasseswithmembers class ** { + kotlinx.serialization.KSerializer serializer(...); +} +-keep,includedescriptorclasses class com.example.helios_alarm_clock.**$$serializer { *; } +-keepclassmembers class com.example.helios_alarm_clock.** { + *** Companion; +} +-keepclasseswithmembers class com.example.helios_alarm_clock.** { + kotlinx.serialization.KSerializer serializer(...); +} -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable +# Room — keep entities +-keep class com.example.helios_alarm_clock.data.AlarmEntity { *; } -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +# SLF4J (Ktor dependency) — suppress missing impl warnings +-dontwarn org.slf4j.** diff --git a/app/src/main/java/com/example/helios_alarm_clock/service/KtorService.kt b/app/src/main/java/com/example/helios_alarm_clock/service/KtorService.kt index 31ffc2b..5cebd75 100644 --- a/app/src/main/java/com/example/helios_alarm_clock/service/KtorService.kt +++ b/app/src/main/java/com/example/helios_alarm_clock/service/KtorService.kt @@ -54,7 +54,13 @@ class KtorService : Service() { super.onCreate() acquireWakeLock() startForeground(NOTIFICATION_ID, buildNotification()) - startServer() + try { + startServer() + } catch (e: Exception) { + releaseWakeLock() + stopSelf() + return + } rescheduleAlarms() } @@ -111,6 +117,13 @@ class KtorService : Service() { post("/set") { try { val req = call.receive() + if (req.hour !in 0..23 || req.minute !in 0..59) { + call.respond( + HttpStatusCode.BadRequest, + ErrorResponse("hour must be 0-23, minute must be 0-59") + ) + return@post + } val id = UUID.randomUUID().toString() val now = Calendar.getInstance() diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127..768b058 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,10 +1,5 @@ - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 #FF000000 #FFFFFFFF - \ No newline at end of file +