{"id":46109709,"url":"https://github.com/reygnn/kolibri-launcher","last_synced_at":"2026-05-07T07:02:39.136Z","repository":{"id":318964633,"uuid":"1034650971","full_name":"reygnn/Kolibri-Launcher","owner":"reygnn","description":"Minimalist 'Android 16 only' launcher focused on simplicity and speed. Built with modern architecture (MVVM, Clean Architecture, Kotlin Coroutines).","archived":false,"fork":false,"pushed_at":"2026-05-06T05:25:20.000Z","size":4667,"stargazers_count":3,"open_issues_count":7,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-06T05:29:43.246Z","etag":null,"topics":["android","android-16","android-launcher","f-droid","foss","kotlin","launcher","minimalist","minsdk-36","mvvm","open-source"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/reygnn.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":"AUDIT.md","citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-08-08T18:39:07.000Z","updated_at":"2026-05-06T05:25:24.000Z","dependencies_parsed_at":"2026-01-05T16:06:33.869Z","dependency_job_id":null,"html_url":"https://github.com/reygnn/Kolibri-Launcher","commit_stats":null,"previous_names":["reygnn/kolibri-launcher"],"tags_count":18,"template":false,"template_full_name":null,"purl":"pkg:github/reygnn/Kolibri-Launcher","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reygnn%2FKolibri-Launcher","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reygnn%2FKolibri-Launcher/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reygnn%2FKolibri-Launcher/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reygnn%2FKolibri-Launcher/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/reygnn","download_url":"https://codeload.github.com/reygnn/Kolibri-Launcher/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/reygnn%2FKolibri-Launcher/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32726686,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-07T02:14:30.463Z","status":"ssl_error","status_checked_at":"2026-05-07T02:14:29.405Z","response_time":62,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","android-16","android-launcher","f-droid","foss","kotlin","launcher","minimalist","minsdk-36","mvvm","open-source"],"created_at":"2026-03-01T22:03:12.292Z","updated_at":"2026-05-07T07:02:39.130Z","avatar_url":"https://github.com/reygnn.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Kolibri Launcher\n\n[![Android CI](https://github.com/reygnn/Kolibri-Launcher/actions/workflows/android.yml/badge.svg)](https://github.com/reygnn/Kolibri-Launcher/actions/workflows/android.yml)\n[![Coverage](https://codecov.io/gh/reygnn/Kolibri-Launcher/branch/main/graph/badge.svg)](https://codecov.io/gh/reygnn/Kolibri-Launcher)\n[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)\n[![API](https://img.shields.io/badge/API-36-brightgreen.svg?style=flat-square)](https://source.android.com/docs/setup/about/build-numbers)\n\nA minimalist Android home-screen launcher. Opinionated, single-platform, no clutter.\n\n## What it is\n\nKolibri replaces your home screen with a stripped-down view: time, date,\nbattery, and a hand-picked list of favourite apps. Everything else lives\nin a swipe-up app drawer with instant search. No widgets. No icon grid.\nNo \"discover\" tabs.\n\nIt's also a private playground for trying current Android architecture\nideas — Hilt, Coroutines/Flow, Jetpack Navigation, Clean-Architecture\nmodule split, Robolectric + Espresso, the works. The codebase is\ndeliberately over-engineered relative to its size; if you're poking\naround for examples of how a given pattern looks in 2026, you'll\nprobably find one.\n\n## Requirements\n\n- **Android 16** (API 36). `minSdk = compileSdk = targetSdk = 36`.\n  No backwards-compat shims. If your device is older, this launcher\n  won't install.\n- **JDK 21** to build. Lower JDKs fail at the Robolectric / SDK-36\n  test layer.\n\n## Privacy \u0026 Crash Reporting\n\n- **ACRA**, opt-in. Disabled by default; the consent dialog appears\n  on first launch and the choice is remembered.\n- **Self-hosted backend.** Reports go to a private server. No third-\n  party telemetry, no analytics SDKs, no ads.\n- **Anonymous payload.** Stack trace + device model + Android version.\n  No user identifiers, no app contents.\n- **Per-type rate-limited.** Same exception class within a 24 h window\n  fires once. Stops a recurring crash from flooding the backend.\n- **Toggle anytime** in Settings → Crash Reports.\n- **Post-mortem ANR detection** uses Android's\n  `ApplicationExitInfo` API (introduced in API 30, available natively\n  here because of the `minSdk = 36` floor) plus a self-defense\n  `RecoveryWatchdog` that kills the process if the main looper hangs\n  for 8 s. No third-party ANR library.\n\n## Screenshots\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"metadata/en-US/images/phoneScreenshots/1.png\" width=\"250\" alt=\"Home screen\" /\u003e\n  \u003cimg src=\"metadata/en-US/images/phoneScreenshots/2.png\" width=\"250\" alt=\"App drawer\" /\u003e\n  \u003cimg src=\"metadata/en-US/images/phoneScreenshots/3.png\" width=\"250\" alt=\"Settings\" /\u003e\n\u003c/p\u003e\n\n## Features\n\n### Home \u0026 navigation\n\n- Minimalist home: time, date, battery, calendar event, alarm — all\n  optional via Settings.\n- Swipe-up to the app drawer; swipe-down inside the drawer to dismiss.\n- Long-press any app for system shortcuts (LauncherApps API).\n- Double-tap to lock screen (optional, requires accessibility service).\n- Two configurable swipe-from-edge actions (left / right).\n\n### App drawer\n\n- Real-time search with debounced filter.\n- Sort alphabetically or by time-weighted usage (decay constant\n  configurable via the `USAGE_DECAY_LAMBDA`).\n- Optional auto-launch when the search filter narrows to a single\n  result.\n- Hide apps you never want to see.\n- Custom display names per app (rename \"Slack\" to \"🐛\", etc.).\n\n### Wallpaper\n\n- Single image or multi-layer composition with per-layer scale,\n  translation, and blend modes.\n- Dynamic text colours: foreground text adapts to wallpaper luminance\n  (`smart_contrast` mode) so dark text never lands on a dark\n  wallpaper region.\n\n### Backup \u0026 Restore\n\n- Full settings export / import via SAF (file picker).\n- ZIP format with embedded wallpaper images; legacy JSON-only format\n  still loads on import.\n- Per-section import options: import only favourites, or only\n  themes, etc.\n\n### Reliability\n\n- Crash-safety pattern across the codebase — every async boundary\n  has its own coroutine exception handler, every external API call\n  is in a `try/catch` that classifies the failure (see the\n  four-category frame in `CLAUDE.md` Rule 11).\n- Multi-layer global crash handler — OOM recovery, ACRA spam guard,\n  process-restart paths.\n\n## Architecture \u0026 Tech Stack\n\nThree Gradle modules since the 2026-05 split:\n\n```\n:app     Android Application — UI (Activities, Fragments), main\n          (Hilt entry, ACRA init, AnrReporter, RecoveryWatchdog),\n          and per-feature packages under `ui/`.\n:data    Android Library — repository implementations, DataStore,\n          ContentResolver-touching code, package-update receiver.\n          Depends on :domain.\n:domain  Pure-Kotlin JVM module (no Android SDK). Repository\n          interfaces, ~50 fine-grained use cases, pure-Kotlin\n          domain models, dispatcher qualifiers, KolibriLog\n          indirection (so :domain doesn't import Timber directly).\n```\n\nModule dependency direction is one-way: `:app → :data → :domain`.\nCycles eliminated in the cycle-elimination branches (see TODO.md\n§9.2).\n\n### Stack\n\n- **100 % Kotlin 2.2.x**, no Java sources.\n- **MVVM + Clean Architecture.** Activities/Fragments hold no\n  business logic — every state path goes through a `ViewModel` which\n  composes use cases.\n- **Coroutines + Flow.** UI state is `StateFlow`, one-time events\n  are `SharedFlow`. The `WhileSubscribed` cold-path bug class is\n  audited and fixed (see `BackupDataAssembler.performImport` for the\n  canonical pattern).\n- **Hilt** for DI. Test variants use `HiltTestApplication` so the\n  production `KolibriLauncherApp` doesn't leak into Robolectric runs.\n- **Jetpack DataStore Preferences** is the *only* persistent store\n  (two documented exceptions for crash-handler bootstrap state, see\n  `CLAUDE.md` Rule 5).\n- **Material 3** + AndroidX. View Binding, no Compose.\n- **Navigation Component** for fragment transitions inside the\n  HOME activity.\n- **ACRA 5.11.4** for crash reporting (self-hosted, opt-in).\n\n### Testing\n\nThe project is *aggressively* tested by personal-project standards.\nThree layers:\n\n- **JVM unit tests** (~2200 in total across `:domain`, `:data`,\n  `:app`) — JUnit 4, MockK, Turbine, kotlinx-coroutines-test.\n  No Mockito (fully migrated). The `:domain` module is pure-Kotlin\n  so its 310 tests run in ~5 s without Robolectric bootstrap.\n- **Robolectric tests** — only for code that legitimately touches\n  Android types (`android.net.Uri`, real `ParcelFileDescriptor`,\n  Activity-host fragment smoke tests). Project default `Application`\n  is `android.app.Application`, NOT `KolibriLauncherApp`, to keep\n  background services out of test runs (see TODO.md §6 for the\n  OOM-incident memo).\n- **Instrumented tests** (16 on a real AVD / device) — limited to\n  things JVM and Robolectric structurally cannot reach: real\n  ContentResolver descriptors, real RoleManager state,\n  LauncherApps permission gates, real BroadcastReceiver `goAsync`,\n  real RecyclerView measure pass, swipe-down gesture\n  dispatch chains. AndroidX Test Orchestrator (`clearPackageData`)\n  resets state between tests.\n\nPer-repository contract-test pattern: each repository interface has\nan abstract `XyzRepositoryContract` plus a `FakeXyzRepositoryContractTest`\n+ `XyzRepositoryImplContractTest` — if fake and impl drift, the\ncontract catches it at JVM speed. Three real fake-vs-impl bugs were\ncaught this way.\n\nDetailed conventions live in\n[`app/src/test/CLAUDE.md`](app/src/test/CLAUDE.md) and\n[`TESTING_CONVENTIONS.kt`](app/src/test/java/com/github/reygnn/kolibri_launcher/TESTING_CONVENTIONS.kt);\ninstrumented-test specifics in\n[`INSTRUMENTED_TESTING_NOTES.kt`](app/src/androidTest/java/com/github/reygnn/kolibri_launcher/INSTRUMENTED_TESTING_NOTES.kt).\n\n## Building\n\n```bash\ngit clone https://github.com/reygnn/Kolibri-Launcher.git\ncd Kolibri-Launcher\n./gradlew assembleDebug         # debug APK\n./gradlew testDebugUnitTest     # JVM unit tests\n./gradlew connectedDebugAndroidTest   # instrumented tests (needs AVD/device)\n./gradlew jacocoTestReport      # coverage\n./gradlew assembleRelease       # release APK + ProGuard mapping upload to ACRA\n```\n\nGradle is bundled via the wrapper. JDK 21 is required (Robolectric +\nSDK 36 dependency).\n\nFor ACRA's release-build pipeline, two property files are expected at\nthe repo root and intentionally git-ignored:\n\n- `keystore.properties` — release signing config (skipped if absent;\n  CI uses a debug-signed APK).\n- `secrets.properties` — ACRA backend URL + basic-auth credentials\n  (skipped if absent; reports just won't be uploaded).\n\n## Project documentation\n\n- [`CLAUDE.md`](CLAUDE.md) — project conventions and the 13 hard\n  architectural rules. Read this first before any code change.\n- [`TODO.md`](TODO.md) — living roadmap, audit snapshot, and the\n  quarterly-recheck process for pinned dependencies.\n- [`KNOWN_ISSUES.md`](KNOWN_ISSUES.md) — StrictMode violations\n  caused by the Android framework or OEM modifications that cannot\n  be fixed in app code.\n\n## Contributing\n\nSolo-maintained, but issues and pull requests are welcome. If you're\nfixing a bug, please include a failing test first (the project\ntreats tests as the source of truth for behaviour). Architectural\nchanges that contradict `CLAUDE.md` need an issue first to discuss\nthe rule trade-off.\n\n## License\n\nGNU General Public License v3.0 — see [`LICENSE`](LICENSE).\n\nThis program is distributed in the hope that it will be useful, but\nWITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freygnn%2Fkolibri-launcher","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Freygnn%2Fkolibri-launcher","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Freygnn%2Fkolibri-launcher/lists"}