Sunrise, sunset & daylight tracker for multiple locations. Animated sun/moon arcs, time-of-day gradients, yearly daylight chart. Kotlin + Jetpack Compose + Material 3.
  • Kotlin 85.5%
  • HTML 14%
  • Python 0.5%
Find a file
2026-07-29 22:16:36 +02:00
.claude/commands Remove worktree dirs from tracking and update gitignore 2026-04-11 17:53:21 +02:00
.github/workflows Sync privacy and store copy with what v1.5 actually does 2026-07-23 17:22:13 +02:00
.idea Fix merge integration issues: duplicate fields, import paths, JUnit version 2026-04-11 17:53:11 +02:00
app Fix moon phase emoji mapping 2026-07-29 22:15:46 +02:00
build/reports/problems Stop revoking Pro from paying users who switch Google accounts 2026-07-23 17:26:31 +02:00
docs S1: tighten privacy narrative to match kept-on backup behavior 2026-07-24 12:35:13 +02:00
google-play docs(store): tell the truth about Crashlytics in all three store documents 2026-07-27 19:23:32 +02:00
gradle merge: 1.7.0 — dark sky, time travel, widget redesign, Crashlytics 2026-07-28 12:36:17 +02:00
Plans merge: 1.7.0 — dark sky, time travel, widget redesign, Crashlytics 2026-07-28 12:36:17 +02:00
screenshots Add screenshots to README 2026-01-27 16:54:20 +01:00
.gitattributes
.gitignore chore(firebase): register the app against Firebase project sunzones-13084 2026-07-26 21:45:39 +02:00
AGENTS.md Unpin the JDK path in build docs and refresh stale agent guidance 2026-07-24 21:49:50 +02:00
AUDIT_REPORT.md S1: tighten privacy narrative to match kept-on backup behavior 2026-07-24 12:35:13 +02:00
AUDIT_REPORT_ROUND4.md Round 4 audit: reports, agent changelog, widget receiver label 2026-07-24 21:59:28 +02:00
AUDIT_REPORT_ROUND5.md docs: round-5 audit reports 2026-07-27 20:01:33 +02:00
build.gradle.kts feat(crashlytics): crash reporting that cannot carry a place or a coordinate 2026-07-27 02:43:00 +02:00
CHANGELOG.md merge: 1.7.0 — dark sky, time travel, widget redesign, Crashlytics 2026-07-28 12:36:17 +02:00
CHANGELOG_AGENT.md docs: round-5 audit reports 2026-07-27 20:01:33 +02:00
CLAUDE.md merge: 1.7.0 — dark sky, time travel, widget redesign, Crashlytics 2026-07-28 12:36:17 +02:00
gradle.properties
gradlew
gradlew.bat
ISA.md Add SunZones Pro monetization: freemium + one-time premium_lifetime purchase 2026-07-19 12:44:34 +02:00
LICENSE
README.md chore(release): 1.7.0 2026-07-27 03:16:10 +02:00
settings.gradle.kts
UX_RECOMMENDATIONS.md Audit round 3: reports (findings, D1 fix, residual list, S1 decision) 2026-07-24 12:31:41 +02:00
UX_RECOMMENDATIONS_ROUND4.md Announce page position to screen readers and explain a failed widget add 2026-07-24 22:03:37 +02:00
UX_RECOMMENDATIONS_ROUND5.md docs: round-5 audit reports 2026-07-27 20:01:33 +02:00

SunZones

Track sunrise, sunset and daylight across multiple locations around the world. A minimal, single-screen Android app with time-aware gradient backgrounds, animated sun/moon arcs, and a yearly daylight chart.

Screenshots

Warsaw at sunset Adeje at midday Warsaw at night

Features

  • Multi-location tracking -- swipe between saved locations to see real-time sun and moon data; tap page indicator to manage cities
  • Sunrise & sunset times with day/night length and countdown to next sunrise
  • Animated sun/moon arc -- a visual progress indicator showing where the sun (or moon) is in its path across the sky
  • Moon phase & illumination -- current lunar phase emoji and illumination percentage
  • Local time, labelled -- each card and city-list row says whose clock it is showing ("19:00 local time · 7 h ahead of you") and flags when the date over there is already different
  • Time-of-day gradients -- card backgrounds shift dynamically through sunrise, morning, midday, afternoon, sunset, twilight and night palettes
  • Resizable home-screen widget -- three designs from a 2x1 strip to a 4x4 block, each with a full-width sun/moon arc as its horizon line
  • Timezone-aware -- each location stores its own IANA timezone, so times are always correct regardless of where your device is
  • Search or GPS -- add locations by city name search or use your current position
  • Sunrise/sunset reminders -- optional local notifications, scheduled entirely on-device
  • 20 languages -- English, Polish, Chinese, Hindi, Spanish, French, Arabic, Bengali, Portuguese, Russian, Urdu, Indonesian, German, Japanese, Nigerian Pidgin, Marathi, Telugu, Turkish, Tamil, Vietnamese

SunZones Pro

A single one-time purchase (premium_lifetime, Google Play Billing -- no subscription, no ads, no account). The free tier holds three locations and every core sun feature above.

  • Unlimited locations
  • Twilight & light -- golden hour, blue hour and the full civil/nautical/astronomical twilight timeline
  • Night & Moon -- moonrise and moonset, the astronomical-night window, how much of it is moonless, the next new moon, and whether the Milky Way core clears the horizon after dark
  • Yearly daylight chart -- month-by-month day length, tap a month to jump to it
  • Time travel -- step the date back and forward, or pick any day between 1900 and 2100, and every reading on the card follows
  • Golden hour reminders

Privacy

No account, no backend, no analytics SDK, no ads. Sun and moon maths run entirely on-device; saved places never leave it.

One exception, stated up front: crash reports are sent to Google's Firebase Crashlytics, on by default, and can be turned off in Settings -> Diagnostics. Coordinates, saved place names and device identifiers are stripped from the message, breadcrumbs and custom keys before anything is sent; the stack trace and Crashlytics' own device/installation metadata are what remain.

The published privacy policy, the Data Safety form and the store listing still describe the pre-1.7.0 state and are being rewritten -- see the "Store-facing documents Crashlytics makes false" section of Plans/ROADMAP.md.

Architecture

Clean MVVM, single-activity Jetpack Compose app with Hilt dependency injection.

Room DB (Flow)
  -> LocationDao -> LocationRepository -> GetLocationsUseCase(date, includeAstro)
       -> CalculateSunTimesUseCase (+ CalculateAstroNightUseCase) -> SunLocation
            -> MainViewModel (StateFlow) -> Compose UI
                                         -> WidgetRenderData -> Glance widget
Layer Responsibility
data/local Room entity, DAO, database
data/repository Thin DAO wrapper, current-location replacement logic
data/preferences DataStore: notification settings, crash-reporting opt-out
domain/usecase Sun/moon calculations via commons-suncalc, astronomical night and Milky Way season, yearly daylight aggregation
domain/model SunLocation (computed fields), AstroNight, ArcState (the one day/night decision), MonthDaylight
domain/util Timezone-id hardening, administrative-suffix stripping for geocoded names
diagnostics Crash-report scrubbing (pure JVM) + the Crashlytics seam behind it
billing EntitlementRepository, Play Billing data source, monetization config/killswitch
ui/main HorizontalPager of SunCard composables, date bar, 60-second auto-refresh; SelectedDatePolicy validates the restored/intent-borne date
ui/widget Glance widget; layout breakpoints, element gating and bitmap arcs decided in pure functions outside the Glance tree
ui/addlocation Geocoder search + GPS, timezone resolution via Android ICU
di Hilt modules providing Room, DAO, Geocoder, FusedLocationProviderClient, billing and diagnostics

Tech stack

Language Kotlin
UI Jetpack Compose, Material 3
DI Hilt
Database Room (schema exported, migrations tested)
Preferences DataStore
Widget Jetpack Glance 1.1.1
Background work WorkManager
Billing Google Play Billing 9.1.0
Crash reporting Firebase Crashlytics (opt-out; no Firebase Analytics)
Sun/moon math commons-suncalc 3.11
Location Google Play Services FusedLocationProviderClient
Geocoding Android Geocoder
Timezone lookup android.icu.util.TimeZone (built-in)
Tests JUnit 5, MockK -- 391 JVM unit tests
Min SDK 26 (Android 8.0)
Target SDK 36 (Android 16)

Build

Requires Java 17 and the Android SDK.

A google-services.json for the Firebase project must be present at app/google-services.json -- it is committed, since this repository is private.

export JAVA_HOME="/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home"
./gradlew assembleDebug

Full gate:

./gradlew lint testDebugUnitTest assembleDebug

assembleRelease now uploads the R8 mapping file to Firebase, so a release build needs network access. Debug builds upload nothing.

License

MIT -- Pawel Orzech