MaksForge

Kozica

Android app

A hiking map of the Polish mountains that works with no signal at all.

Building

Kozica never opens a network connection; live information is a hand-off to another app

Context

A hiking app is tempted by the network from every side: online map tiles, mountain weather, the TOPR avalanche bulletin, trail closures, hut opening hours, sharing your position with someone at home, cloud sync of hikes, crash reports. Each would be useful somewhere, and most of them are useful exactly where a phone has no signal: on a ridge, in a valley, in the Tatras’ dead zones.

The map data does not need the network: Play delivers it as asset packs (ADR-0007). Nor does the user’s position: GPS is a receiver, and the assistance that speeds up a first fix (A-GPS, Wi-Fi and cell positioning) is fetched by Android and Google Play services, not by the app, and needs no permission of ours.

Decision

The merged manifest of every build declares no INTERNET permission, and no ACCESS_NETWORK_STATE or ACCESS_WIFI_STATE either. scripts/aab-permissions.py forbids all three. A dependency that merges one has it removed with tools:node="remove" and must work without it; a library that cannot is not used. Three are known:

  • MapLibre (android-sdk 13.7.1) merges all three, and the fine and coarse location pair that phase 2’s ask uses, the moment it becomes a dependency.
  • The Play Billing Library (9.1.0), for a later tip, depends on Google’s datatransport. Besides INTERNET and ACCESS_NETWORK_STATE, that merges a JobInfoSchedulerService and an AlarmManagerSchedulerBroadcastReceiver that schedule the library’s own usage logging. Stripping the permissions alone would ship an SDK that still tries to upload on a schedule, so both components are removed too, and the tip ships only after a purchase is confirmed on a phone without them.
  • Play Asset Delivery (asset-delivery 2.3.0) brings WorkManager, which merges ACCESS_NETWORK_STATE. The main manifest’s removal covers it. Read through Play on API 30 and 36 on 2026-10-20 (phase-2b.md § 1, verdict 1): the Play Store extracts a pack through the library’s own services, no ExtractionWorker runs, and nothing asks for network state.

Live information is a hand-off. Where the user needs something only the network has, the app opens another app with an intent and claims nothing about the result:

  • mountain rescue: the dialler with 985;
  • the official Ratunek app of GOPR and TOPR, for calling for help with a position;
  • the avalanche bulletin: TOPR’s page in the browser;
  • mountain weather: IMGW’s page in the browser.

They arrive together, before the first stranger installs the app (PLAN.md, phase 3).

Alternatives

Fetch a narrow set of live data — the avalanche bulletin, the weather. It is the most useful online feature for safety. But neither source has a public API, so the app would scrape pages that change without notice, and a bulletin cached yesterday and shown as today’s is worse than none. The browser shows the page as its authors keep it, with its own date on it.

Share the hiker’s position with someone at home. It is a real safety feature, and the most sensitive data an app can hold. It would need a server, an account and a privacy story of its own. Ratunek already lets rescuers locate a caller.

Crash reporting of our own. It is the honest loss. Play Console’s Android vitals reports crashes and ANRs with stack traces, but only from devices whose owners opted into sharing usage and diagnostics; anything else arrives when someone mails it, through ui/support/. At this size, vitals, the mail hand-off and readings from a real phone carry it.

Cloud backup and sync. Backup stays on the device and in Android’s own backup (ADR-0005), which needs nothing from the app’s manifest.

Host the map data ourselves. Covered and rejected in ADR-0007.

Consequences

  • The privacy policy and Play’s Data safety answers stay one sentence: nothing the user enters leaves the phone. Adding any network feature reopens both, and this ADR.
  • MapLibre needs MapLibre.setConnected(false) before the first map is created, or it asks the system for network state it no longer has permission to read and throws.
  • A hand-off’s target can move: a URL changes, Ratunek is renamed. It is fixed in an ordinary release, and the browser or the Play Store shows the user that something is wrong rather than the app.
  • No crash reporting of our own. Android vitals is the only source, and it sees only opted-in devices; a fault on any other phone is found by its owner, or by a reading.

Amendment — 2026-10-22: the hand-offs, per country

The Decision’s four hand-offs are Poland’s. Since phase 2b’s amendment of ADR-0007 the Regions cross into Slovakia and Czechia, where 985 and Ratunek are the wrong first call, so phase 3 builds them for each of the three countries, in a fixed order, under 112 (phase-3.md § 2): the rescue number in the dialler, the rescue app or its Play page, the avalanche bulletin and the mountain weather in the browser. No country is chosen by the user’s position: a fix can be missing, stale or beside the border, and a wrong guess hands a hiker the wrong number. Nothing is dialled by the app; ACTION_DIAL types the number and the user places the call, so no CALL_PHONE is asked for.

Hi, I'm Maksymilian.

MaksForge is where I build the small tools I wish existed. Each one solves a single problem.