A native Android app that shows you the health data already stored on your phone by Health Connect — steps, sleep, heart rate, weight, blood pressure and every other type it supports.
Health Connect collects what your watch, scale and fitness apps write, but offers no good way to actually look at it. This is that missing viewer: a read-only browser for your own data.
It has no internet permission. Not a promise — a property of the build. Android prevents the app from opening any network connection, so your health data cannot leave your device even in principle. There is no account, no server, no analytics, no ads, no tracking.
A build check fails if INTERNET or ACCESS_NETWORK_STATE ever appears in the merged
manifest, including transitively from a dependency.
Read-only. Only READ_* permissions are requested, so the app cannot modify or delete
your records. Another check fails the build if a release ever requests write access.
Nothing is stored. Records live in memory only while displayed. Backups and device transfer are disabled. The one exception is an export you start yourself: it is written straight into the file you pick in the system’s save dialog, and the app keeps no copy. A report you view or print is built in memory and handed to the system’s print preview.
You choose what to share. Permissions are requested per data type, nothing preselected.
Totals are correct. When several apps write the same metric, their records overlap. Summing them double-counts, so all totals come from Health Connect’s own deduplicating aggregation, and the app tells you when more than one app contributed to a figure.
It says what it cannot know. Health Connect stores no link between a workout and the readings taken during it, so a session’s statistics are matched by time, and every screen showing them says so. Nothing is predicted and nothing is scored.
Free:
Pro, a one-time purchase with no subscription:
The purchase goes through Google Play Billing with its telemetry dependencies excluded, so it adds no network permission to the app either.
Requires JDK 17+, the Android SDK with API 37, and a device or emulator on API 26+.
./gradlew assembleDebug # build
./scripts/deploy.sh debug # build, install and launch
./gradlew check # tests plus the privacy build checks
./scripts/make-avd.sh # create the emulator this project tests against
The AVD it creates is 1080x2160 — exactly the 2:1 maximum aspect ratio Play accepts for phone screenshots, so store assets need no cropping — with a 16 GB data partition, because a full one makes installs fail while the previous build keeps running.
local.properties needs sdk.dir pointing at your Android SDK. No secrets are required to
build: release signing falls back to the debug key when .env is absent.
./gradlew check treats compiler and lint warnings as errors, with no baseline. That
includes lint’s notices about newer dependency versions, so it can fail on a tree nobody
touched until the versions in gradle/libs.versions.toml are raised.
Debug builds install beside the release as de.steppicrew.healthconnectview.debug and add
what the release must never have: write permissions for a data seeder, probe activities for
checking aggregation on a device, and an intent extra that opens any screen directly. The
release source set has an empty twin of that last one, and a build check
(verifyNoDebugBackdoor) confirms it.
cp .env.example .env # then fill it in
./scripts/make-keystore.sh # creates the signing key (run this yourself)
./scripts/check-keystore.sh # verifies the path, password and alias in .env
./scripts/deploy.sh bundle # signed .aab for the Play Store
check-keystore.sh is worth running whenever signing misbehaves: it reports which of the
three — file, password, alias — is wrong, instead of letting the build fail deep inside R8.
Add --prompt to test a password interactively without writing it to .env. A release
build refuses to start unless these checks pass, so it can never silently fall back to the
debug key.
.env holds the keystore path and passwords and is gitignored. make-keystore.sh reads the
path and alias from it but leaves passwords to keytool’s own prompt, so no script ever
handles the secret.
Publishing to Play is scripted as well:
./scripts/release.sh --dry-run # everything except the upload
./scripts/release.sh # build, then publish the bundle and changed listing assets
./scripts/release-status.sh # what is actually live on each track
release.sh refuses a dirty tree or a version already published, and stops unless
app/src/main/play/release-notes/<lang>/entries/<versionCode>.txt exists for every store
language. The store listing lives beside them in app/src/main/play/listings/.
Under app/src/main/java/de/steppicrew/healthconnectview/:
| Path | What it holds |
|---|---|
registry/ |
Every record type as data: name, category, units, extractors, permission, how its tile and chart behave |
health/ |
Health Connect access; reads and aggregation are separate entry points. Goals, streaks and sessions are worked out here |
dashboard/ |
The dashboard’s configuration: tiles, their sizes, goals and colours, and its backup file |
ui/ |
Compose screens: dashboard, type detail, sessions, cycle, compare, insights, settings |
export/ |
CSV, PDF and GPX, written only into a file the user chose |
billing/ |
Entitlement gate for the Pro features |
settings/ |
Non-health UI state in DataStore |
Elsewhere:
| Path | What it holds |
|---|---|
app/src/debug/ |
Seeder, probe activities and the navigation backdoor; never in a release |
app/src/main/play/ |
Store listing texts and images, and per-version release notes, for every store language |
scripts/ |
Building, signing, releasing, store assets, the privacy page |
docs/ROADMAP.md |
What was built and why, with the measurements behind the decisions |
docs/FEATURE-IDEAS.md |
Ideas not yet planned |
PRIVACY.md |
The privacy policy |
CLAUDE.md |
Working notes: the Health Connect behaviours that were expensive to find |
The registry is the heart of it: one generic RecordTypeSpec holding lambdas, keyed by
KClass, so a single code path serves all 41 types instead of 41 branches per screen.
Adding a type is a registry entry, not a UI change.
Permission strings are always resolved through HealthPermission.getReadPermission() rather
than derived from class names, because the mapping is not one-to-one — SleepSessionRecord
needs READ_SLEEP, and Steps and StepsCadence share a single permission. A unit test checks
every resolved string against the platform constants, since a permission the platform does
not define is never granted and looks exactly like missing data.
Numbers shown as totals never come from summing records. read() is for display,
dailyTotals() is the only source of figures, and the two are separate repository entry
points to keep that boundary visible.
The app ships in seventeen languages: English, German, Arabic, Chinese (simplified and
traditional), Dutch, French, Hindi, Indonesian, Italian, Japanese, Korean, Polish,
Portuguese (Brazil), Russian, Spanish and Turkish. A new string goes into every
res/values-*/strings.xml in the same commit; a unit test fails a language that was
complete and drops a key. Corrections from native speakers are welcome.
MIT. See LICENSE.