Open Source / ALEAPP #1784

Reading the Android Health Connect store

Health Connect is where Android keeps the steps, heart rate, sleep and exercise that fitness apps write. It is a rich store and ALEAPP could not read it. Thirteen artifacts now do. This post says plainly which of them we could check against real data, and which only against a synthetic database.

  • Daniel Addai, SectraTEK Lab
  • Merged
  • Merged 25 October 2026
  • CC BY 4.0

Summary

This contribution adds healthConnect.py, thirteen artifacts for the Android framework Health Connect store: healthconnect.db under system_ce/<user>/healthconnect on Android 14 and later, and the health-permissions-first-grant-times.xml file beside it. Three artifacts cover the registered apps, the store's preferences and the first time each package was granted health permissions. Ten read the records and logs: activity, measurements, heart rate samples, sleep sessions and stages, exercise sessions and route points, and the access, read-access and change logs.

When the pull request was filed, only the first three had rows on real data: every record table was empty on all four Android 16 images, so the ten record artifacts had been checked against the AOSP schema and a synthetic database. The artifact notes in the merged change later add two research stores from other Android versions, on which the record artifacts matched the table row counts. Both statements are listed in the sources table.

  • 13New artifacts
  • 3 of 13Had rows on the four Android 16 images
  • +2,746Lines added in 31 files
  • 13New synthetic test cases

The issue

Health Connect is where Android keeps the steps, heart rate, sleep and exercise that fitness apps write, and where other apps can read them with permission. For an examiner it can answer which health apps were registered, what they recorded and which apps read the data. ALEAPP could not read it.

Without a reader, an examiner who wanted those answers opened a SQLite database with many tables by hand. The store has no single table. It has one table per record type, series and session tables joined to a parent row by a key, and logs of who read and wrote. The schema also changes between versions: the stores we examined were versions 12, 20, 23 and 26, and a column present in one is absent in another.

The empty case needs care too. A store can exist and hold no records, and on all four Android 16 images it did. That is not an uninformative result, because the apps and the permission times still show what was registered, but it is not evidence that the person recorded nothing, and it does not show what an app did before the store was reset or trimmed. Figure 1 shows where the data sits.

Where Health Connect keeps its data and which artifacts read each partStorage layout. In system_ce/<user>/healthconnect, healthconnect.db holds four groups of tables, read by thirteen artifacts in total: apps and preferences; interval and instant record tables; series and session tables; and three log tables. A fifth file, the first-grant XML, feeds one artifact.In system_ce/<user>/healthconnect/Artifacts (13 in all)healthconnect.dbapplication_info_table, preference_tableApps, Preferencesone row per app, one per keyhealthconnect.dbinterval and instant record tables, one per typeActivity Records, Measurementsone row per recordhealthconnect.dbseries and session tables: heart rate, sleep, exerciseHeart Rate, Sleep, ExerciseHeart Rate Samples, Sleep Sessions,Sleep Stages, Exercise Sessions,Exercise Routehealthconnect.dbaccess_logs_table, read_access_logs_table,change_logs_tableAccess, Read Access, Change Logsone row per log rowhealth-permissions-first-grant-times.xmlfirst-grant time per package or shared userPermission First Grant Timesone row per entryAndroid 14 and later. One store per Android user. Diagram of the layout, no values.
Show this figure as a table
Figure 1 as a table
SourceTablesArtifacts
healthconnect.dbapplication_info_table, preference_tableApps, Preferences
healthconnect.dbinterval and instant record tablesActivity Records, Measurements
healthconnect.dbseries and session tablesHeart Rate Samples, Sleep Sessions, Sleep Stages, Exercise Sessions, Exercise Route
healthconnect.dbaccess_logs_table, read_access_logs_table, change_logs_tableAccess Logs, Read Access Logs, Change Logs
health-permissions-first-grant-times.xmlXML entriesPermission First Grant Times
Figure 1The store is many tables, not one. Record tables are per record type; sessions and series are joined to a parent row; logs record access. Source: ALEAPP pull request 1784, scripts/artifacts/healthConnect.py (paths, table names and artifact list; diagram, no values).

The essence

Each artifact reads one part of the store and keeps the values the writing app supplied. Numbers that the Android source names, such as record types, exercise types, sleep stages and recording methods, are shown as the stored number followed by the AOSP name; a number the source does not name is shown as stored. Times are converted from milliseconds to UTC with the zone offset the app supplied, and units follow the AOSP record classes.

The thirteen artifacts
ArtifactSourceOne row perNotable fields
Appsapplication_info_tableregistered apppackage, app name, stored icon, record types used
Preferencespreference_tablekeykey and value as stored
Permission First Grant Timesfirst-grant XMLpackage or shared-user entrytime as stored and as UTC, Entry Type
Activity Recordseight interval record tablesrecordsteps, distance, energy, floors, elevation, hydration, wheelchair pushes
Measurementssixteen instant record tablesrecordweight, height, body composition, resting heart rate, oxygen, blood pressure, glucose, temperature
Heart Rate Samplesseries table with its parentsampletime and beats per minute
Sleep Sessions, Sleep Stagessession and stage tablessession, stagetimes with zone offsets; stage type named from AOSP
Exercise Sessions, Exercise Routesession and route tablessession, route pointtype named from AOSP; position as the app supplied
Access, Read Access, Change Logsthree log tableslog rowoperation and record types named from AOSP; time in UTC

Our methodology

We began with the schema. We listed the tables of each store and, independently of the parser, listed the non-empty ones. On the four Android 16 images that showed only the application and preference tables and, on the version 26 stores, device and data-source tables. The parser reads each database's schema at run time and substitutes NULL for any column the version lacks, so older stores read without an error.

Names and units came from the AOSP HealthFitness module, at a commit the notes cite. We did not guess units: distance and elevation are in metres, energy in calories (the unit of the AOSP energy class, not kilocalories), hydration in litres. The notes warn that distance and calorie totals can overlap between apps, and that a value is what the writing app supplied, so manual entry or an import from another source is a possibility the examiner must rule out with the recording method, the app and the device columns.

The checks were layered. For the three artifacts that had data on the four images, row counts equalled independent counts: Apps 2, 2, 3 and 3, Preferences 7, 7, 9 and 9, First Grant Times 1, 2, 2 and 1. A second Android user at system_ce/10, built from the real files, doubled all three exactly. The record artifacts were exercised on a synthetic database built by a fixture script with invented values, including two users. In the merged notes, on two other research stores the activity artifact reported 68,617 and 79,052 rows, equal to the rows of the tables it reads, and for every steps row the start time and count equalled the stored values. Thirteen new test cases brought the suite to 145 with no failure. The storage-view mode skipped 90 cases, because this store sits under system_ce, not data/data, so the harness has nothing to restage.

Not everything is read. Speed records and skin temperature records exist in the research stores and no Health Connect artifact reads them; nor do reproductive health, medical record and the version 26 device tables. Figure 2 shows the steps and Figure 3 shows where each artifact has been seen with data.

How a Health Connect store is read, from file to report columnsFour steps: find each healthconnect.db and take the Android user from the system_ce path; read the schema so absent columns read NULL; join app, device and parent tables and name the stored numbers from AOSP classes; report times in UTC with the writing app's zone offset.Find the storehealthconnect.dbuser from thesystem_ce/<n> path;sidecars skippedRead the schematable_info per table;a column a versionlacks reads NULL, soolder stores still readJoin and nameapp and device tables,series to parent row;numbers named fromAOSP classesReporttimes in UTC with thewriter's zone offset;units per AOSP;13 artifactsA number the AOSP class does not name is shown as stored. Values are what the writing app supplied.Diagram of the parser steps in healthConnect.py; no values are shown.
Show this figure as a table
Figure 2 as a table
StepWhat the parser does
Find the storehealthconnect.db under system_ce//healthconnect; Android user from the path
Read the schematable_info of each table; a column the version lacks reads NULL
Join and nameapplication_info_table and device_info_table joined by row id; series and stage tables joined to the parent record; numbers named from AOSP classes
Reportmilliseconds to UTC; zone offset as +HH:MM; units per the AOSP record classes
Figure 2From file to report. Schema-aware reading is what lets one parser cover store versions 12 to 26. Source: ALEAPP pull request 1784, scripts/artifacts/healthConnect.py (diagram of the steps).
Rows read by each Health Connect artifact on three kinds of storeCoverage matrix of row counts. Apps, Preferences and First Grant Times had rows on all four Android 16 images; every record artifact was empty there. Two further research stores held thousands of rows for the activity, heart rate, sleep, exercise and log artifacts. Read Access Logs held no row anywhere.ArtifactAndroid 16 imagesrows per image (4)Research store AAndroid 15, version 20Research store BAndroid 14, version 12Apps2 / 2 / 3 / 323Preferences7 / 7 / 9 / 953Permission First Grant Times1 / 2 / 2 / 123Activity Records068,61779,052Measurements05358Heart Rate Samples0169,09835,166Sleep Sessions09817Sleep Stages05,9571,025Exercise Sessions012616Exercise Route (points)064,5330Access Logs02,972855Read Access Logs000Change Logs09,9573,472Record tables wereempty on all fourAndroid 16 images(store versions 23,20, 26 and 26).rows were readno rowsCounts are real aggregates from the Lab's research images. Read Access Logs was checked against the AOSP source only.Store A and store B are two different research stores, not the same phone at two times.
Show this figure as a table
Figure 3 as a table
ArtifactAndroid 16 images (4)Store AStore B
Apps2 / 2 / 3 / 323
Preferences7 / 7 / 9 / 953
Permission First Grant Times1 / 2 / 2 / 123
Activity Records068,61779,052
Measurements05358
Heart Rate Samples0169,09835,166
Sleep Sessions09817
Sleep Stages05,9571,025
Exercise Sessions012616
Exercise Route (points)064,5330
Access Logs02,972855
Read Access Logs000
Change Logs09,9573,472
Figure 3Where the artifacts have been seen with data. Three of the thirteen had rows on the Android 16 images; the other ten were checked on two further research stores and on a synthetic database. Source: ALEAPP pull request 1784 body (Android 16 counts) and artifact notes in healthConnect.py (stores A and B); real aggregate counts, not synthetic.

One defect found on real data shows why the real stores mattered. On the two version 26 stores the first-grant file holds a shared-user element and no package element, so the first version of the artifact returned no rows and raised no error. It was found by running the artifact on those stores. The artifact now reads both element types and adds an Entry Type column, and a synthetic case and a unit test cover both. Figure 4 shows the mechanism with an invented file.

A silent zero found on real data: the first-grant file with a shared-user entryBefore and after. A synthetic first-grant file holds one shared-user element and no package element. Before the fix the artifact looked for package elements only and returned no rows; after, it reads both element types and adds an Entry Type column. Synthetic example.Synthetic first-grant file<shared-user name="com.example.shared" first-grant-time="2026-01-01T00:00:00Z"/>no package element in the fileBefore: package elements only0 rows on the two version 26 storesAfter: package and shared-user elements1 row, Entry Type: shared-userFirst Grant Time as stored and as UTCElement name and value are invented. After the fix the four Android 16 images gave 1, 2, 2 and 1 rows.The defect was found by running the artifact on real stores.
Show this figure as a table
Figure 4 as a table
StateElements readRows on the Android 16 images
Before the fixpackage only0 on the two version 26 stores
After the fixpackage and shared-user, with Entry Type1, 2, 2, 1
Figure 4A silent zero in new code. The artifact ran without error and reported nothing, because the file on version 26 stores holds an element the first version did not look for. Source: synthetic example with invented values; the defect and the 1, 2, 2 and 1 row counts are from the PR body (test record) and LAB-TR-0006.

The value

With the module an examiner can answer, from the report, which apps are registered with Health Connect and when each package was first granted permission, and, where the store holds records, what was recorded, over which intervals, by which app, with which recording method and on which device. The access and change logs add which app read, wrote or deleted which record types and when. These are indicators of what the store holds. They are not evidence of what the person did.

The limits are specific. A session is what the writing app reported, so a sleep session does not establish that the person slept. A route point is a position the app reported and does not by itself show where the device was. A log row records an access the platform logged, but the retention period is not established and the store trims the change log, so a missing row is not evidence that nothing happened. The record artifacts were verified on two research stores and not on a populated Android 16 store, and the pull request asks anyone with a populated store to check the units before relying on them. Health data is among the most sensitive a phone holds; the Lab reports counts and structure only.

Where this sits in the Lab's research

No Lab report is devoted to this store; the method above comes from the pull request. It is one of five case studies in Lab Technical Report LAB-TR-0006, Section 7.1 and Table 8, which records the apps, preferences and first-grant counts, the empty record tables on all four images, and the ten record artifacts as validated on synthetic data only at the time of that report. The report also lists the first-grant defect among the defects that were reproduced with a synthetic file, fixed and covered by a test.

Cite as: Addai, D. (2026). Empty Is Not Absent: Silent Failure Modes in Open-Source Mobile Forensic Parsers on iOS 26 and Android 16. LAB-TR-0006, version 1.0, preprint. doi:10.5281/zenodo.23717929.

Sources

Every number and claim on this page, and where it comes from. Figures marked synthetic example use invented values.

Sources for the numbers and claims in this post
Number or claimWhere it comes from
Merged 25 October 2026; 31 files, +2,746 and -1 linesPull request 1784 metadata (mergedAt, files, additions, deletions)
Thirteen artifacts; 13 new test cases; PASS=145, upstream PASS=132Diff, __artifacts_v2__ in healthConnect.py; PR body, test record table
Every record table empty on four Android 16 images; store versions 23, 20, 26, 26PR body, first checklist item; LAB-TR-0006 Section 7.1
Apps 2 / 2 / 3 / 3; Preferences 7 / 7 / 9 / 9; First Grant Times 1 / 2 / 2 / 1PR body, second checklist item; LAB-TR-0006 Table 8
Doubling test at system_ce/10 reads the three artifacts exactly doublePR body, fourth checklist item
First-grant defect: shared-user element, no package element, 0 rows before; 1, 2, 2, 1 afterPR body, test record row Real-data defect; LAB-TR-0006 defect table
Research stores A and B: activity 68,617 and 79,052; measurements 535 and 8; heart rate 169,098 and 35,166; sleep sessions 98 and 17; stages 5,957 and 1,025; exercise 126 and 16; route points 64,533 and 0; access logs 2,972 and 855; change logs 9,957 and 3,472; apps 2 and 3; preferences 5 and 3; first grants 2 and 3Diff, notes and sample_data of each artifact in healthConnect.py
Store A is Android 15, version 20; store B is Android 14, version 12Diff, sample_data of healthconnect_apps
Read Access Logs: source-verified, no row on any extraction or storeDiff, notes of healthconnect_read_access_logs
Speed records 1,784 rows and skin temperature 65 rows exist and are not readDiff, notes of healthconnect_activity
Units: metres, calories (not kilocalories), litres; names from the AOSP HealthFitness moduleDiff, notes of healthconnect_activity and the RECORD_TYPES comment
Storage-view run skipped 90 cases because the store is under system_cePR body, test record table
Request for a populated store to check unitsPR body, Anything reviewers should know
Figure 4 element and valueSynthetic example, invented for illustration