trainlog/docs/android.md

34 KiB
Raw Blame History

Android application

1. Purpose

The Android application is Trainlog's low-friction capture client.

It is a native Kotlin/Jetpack Compose application with local SQLite persistence.

The desktop remains the canonical long-term history and analytics store.

Session exchange V3

Completed session occurrences persist an entry_id; it is never regenerated for exchange. Android schema v14 added feedback roots and completion ended_at anchors; v15 adds immutable correction history and the separately versioned trainlog-training-feedback-v2.json (while still reading V1). No audio is stored. Android publishes trainlog-mobile-export-v3.json as the active desktop snapshot and imports trainlog-pc-mobile-export-v3.json after the PC catalogue. The artifact preserves occurrence order, continuous metrics, set weights and equipment. The legacy V1 contract remains separate and readable.

V3 preserves ordinary plan metadata atomically with occurrence identity, equipment, actual sets and MAX. V1/V2 remain readable legacy artifacts and are never silently rewritten as V3.

2. Implemented navigation

Accueil
Séances
Exercices
Statistiques
Synchronisation
Paramètres

The fixed Android application shell uses a Material 3 modal drawer, a Scaffold, system sans-serif typography, and local small VectorDrawable icons. Each of the seven destinations is a complete drawer item with a minimum 48 dp touch target. The active drawer section is derived from the canonical route and exposes selected semantics; it is not maintained as a second navigation state. The drawer itself scrolls when vertical space is limited.

STATS_V1

The Statistics landing page defaults to 30 days and is a Material 3 dashboard using the existing Catppuccin Mocha/Lavender semantic theme. It filters a read-only projection by 7 days, 30 days, 90 days, one year, or all history, then presents performance, body-measurement, and weekly-frequency cards plus detail routes.

The landing performance chart contains weekly classified event counts, never summed or averaged kilograms. Every actual performed set is an observation. A working event is strictly later in canonical time and greater than the prior best for the same canonical exercise, nonempty resolved equipment, external equipment semantics, external-load mode and exact performed reps/duration dose. Explicit MAX uses a separate strictly-later-record-above-prior-MAX rule in the same exercise/resolved-external-equipment context. Equal-instant IDs only order presentation and never create events. A one-point class cannot create an event; context-owned raw series remain available in the exercise drill-down. Assistance, plans and missing actual loads are omitted.

The selected-period summary reports actual sessions, performed sets, distinct actual exercises and explicit MAX records. Body curves use only recorded observations (no fabricated interpolation), are bounded and ordered by the most recently observed metric; one point has no delta or graph. Observable session history and frequency include a stable session ID exactly once when one of its occurrences owns a persisted performed set, continuous activity, or explicit MAX result; ended_at is not an inclusion gate. Planned targets and empty occurrences do not count. MAX sessions are labelled separately. Calendar buckets use the local date written in each persisted timestamp (including its literal offset), with Monday as the first day of the week; rolling windows and chronology still compare the exact represented instant and never use the device timezone. Malformed legacy timestamps are omitted only from the statistics projection; the dashboard remains available and reports that invalid history was ignored.

The section roots are:

Accueil          durable-draft resume, manual capture, recent BODY ZONES exposure, concise local links
Séances          current session, programme a session, manual entry, completed sessions
Exercices        catalogue, detail, create, and contextual edit
Legacy equipment catalogue/detail routes remain compatibility-only and are
not exposed in the normal drawer.
Statistiques     STATS_V1 dashboard, conservative graphs and detailed local drill-downs
Synchronisation  existing request, result, and diagnostic workflow
Paramètres       exchange-folder authorization and explicit PC-catalog refresh

AppRoute is typed and carries stable IDs and an explicit caller where a detail or creation workflow needs one. AppNavigationController is the sole owner of route transactions in the Compose root. Its bounded history returns inline exercise/equipment creation to the caller exactly once. Back first resolves visible input/overlays and the drawer; it then follows the caller or route history, falls back to the section root, then Home.

Leaving an unaccepted generator proposal or a dirty exercise, equipment, or measurement form installs an explicit keep/discard guard. Keeping retains the transient editor state and its current route; discard clears only the transient state that raised the guard. Route changes do not save, synchronize, finalize, or otherwise write repository data. A successful generator acceptance clears its transient proposal before routing; an existing_active_draft result remains guarded and retains the proposal until the user resolves it.

Route content uses a SaveableStateHolder, keyed by stable route identity, with a 16-entry bound for modest list/scroll state. The root ViewModel retains navigation and transient forms across Activity recreation. It does not serialize a session or generated proposal into a Bundle: after process death, only the repository-owned durable active draft is restored through Home.

3. Local persistence

Android local database version:

11

Domain tables cover:

exercises
sessions
session_exercises
performed_sets
continuous_activity
body_observations
exercise_body_zones
exercise_body_zone_sync

This database is Android-local. It is not copied to the PC.

Schema v4 introduced active_session_draft, draft_session_exercises, draft_performed_sets and draft_continuous_activity. The implemented additive v4 -> v11 chain preserves catalog, completed sessions/actuals, body observations and the draft while adding the shared equipment catalogue, occurrence-level equipment links and stable completed/draft entry_id values. Exactly one active draft is supported; it is separate from completed history. Schema v14 also gives its occurrences durable Android-local draft_exercise_feedback. The active-session 🎙 Ressenti action saves there immediately and reads all observations back in timestamp/ID order. Multiple records are allowed. They survive process restart and transfer by stable entry_id, without regenerated feedback IDs, in the atomic finalization transaction. Explicit draft discard cascades this draft-owned data. Schema v9 adds explicit completed/draft MAX results. Schema v10 additively stores direct primary/secondary body-zone relations and their private sync baseline; the taxonomy itself remains the shared manifest asset. Schema v11 additively stores optional occurrence/draft planning metadata after the explicit v10 -> v11 migration; existing rows retain load_mode=none, zero rest and NULL targets.

Session generator V1

The V1 generator remains implemented but hidden from normal UI; Home exposes no generator action. The generator uses the shared frozen policy for all 11 selectable BODY ZONES, four goals, policy duration presets and custom bounds. A generated preview is read-only until acceptance and explains target dose, rest, equipment, observed-load source or absence, exposure/recency, and explicit shortages. It never claims measured recovery.

Users can edit, remove, reorder, regenerate or cancel the in-memory proposal. Acceptance of a nonempty proposal is one transaction into the ordinary active draft, with target plans and zero actual rows. An existing draft yields the non-mutating existing_active_draft conflict. Empty results cannot be accepted; nonempty partial results may be accepted and edited normally. Final completion continues to require actual captured work.

Body focus Home V1

Home displays Zones à travailler immediately after the active-session or manual-session action. This is a read-only description of recent recorded training exposure. It does not estimate recovery, readiness or fatigue and it does not prescribe or generate a session.

The production getBodyZoneHomeOverview(now) read model processes exactly the eight meaningful child zones chest, back, shoulders, arms, core, glutes, thighs and calves. Grouping zones upper_body, lower_body and full_body are never overlays and never contribute duplicate exposure. Only completed occurrence-owned performed_sets, continuous_activity and max_results are evidence; targets, empty occurrences and active drafts are excluded. Timestamps use the canonical parser and inclusive rolling instant boundaries, never SQLite date functions or lexical ordering. Alias history is resolved to the canonical exercise identity and counted once.

Each zone reports the last primary and secondary exposure, primary/secondary actual-work counts for 7 and 30 days, distinct exposed-session count for 30 days, active canonical exercise availability, a textual state and reasons. Primary exposure is the dominant signal. Secondary exposure is only a weaker modifier and can never stand in for missing primary exposure.

Eligible zones have at least one present canonical exercise with a direct resolved child-zone relation. Unsupported zones remain visible with Aucun exercice résolu disponible but are never recommended. The top three (or fewer) use this exact stable ordering: no primary exposure first; oldest last primary exposure; lower primary work over 7 days; lower primary work over 30 days; lower secondary work over 7 days; lower secondary work over 30 days; oldest last secondary exposure; stable zone_id. Empty history says Pas encore d’historique d’entraînement. and ranks only supported zones.

The compact side-by-side Compose Face/Dos maps use independent filled, anatomically contoured cubic Path regions for shoulders, chest/back, arms, core/glutes, thighs and calves. The canvas contains no zone-name text and is only a stylized BODY ZONES selector, never medical anatomy or a recovery model. Direct taps are resolved against the same normalized paths used to draw each region. Selection changes both fill and a high-contrast outline. Separate transparent accessibility controls follow the region bounds, provide at least 48 dp targets, use the Button role, and announce view, zone, textual exposure state and selected state; color is never the sole signal. Zone details remain below the maps and show last-primary age, 7-day primary/secondary work and available exercise count. Voir les exercices opens the existing Catalogue with its body-zone filter, while Voir les statistiques opens the existing Statistics landing route.

Load editing offers compact choices for automatic V1 qualification, a user-selected %MAX, or no numeric target; direct manual kg remains available. %MAX accepts an integer 1..100 and uses exactly MAX × percentage / 100 only for the chronologically latest explicit MAX of the same exercise ID and same external-resistance equipment ID. Assistance and incompatible/missing contexts produce an empty target labelled compatible_max_unavailable; this is unavailable rather than a fallback or a recommendation. Only the resulting plan target_weight_kg is persisted on acceptance; the percentage and MAX provenance are transient. Zone, objective, duration and load choices use compact localized chips and do not expose internal IDs.

The ordinary set-based manual exercise form exposes the same separation from actuals with Valeur en kg / % de mon MAX / Aucune. It composes the confirmed choice into the existing SessionExerciseDraft.plan; performed-set weights are never used as target storage. Editing an existing generated/manual occurrence preserves its target dose and rest while allowing the numeric target to change. The read-only %MAX lookup shows the compatible MAX date/value and calculated target before confirmation, recomputes for exercise/equipment/percentage changes, and performs no draft mutation by itself.

The preview shows requested and estimated duration, warns on a meaningful shortfall without padding, and states that warm-up and cool-down are absent in V1. SESSION_GENERATOR_V2 remains future-only.

4. Exercise catalog

Exercise creation records:

name
recording_mode
tracking_mode
data_fields
primary_zone_id       nullable only for explicit unclassified/history cases
secondary_zone_ids   zero or more distinct canonical IDs

Stable identity:

ex_<uuid-v4>

The UI rejects invalid profile combinations and local normalized-name collisions.

An exercise may be created standalone or inline while building a session.

The form groups translated values from catalog/body-zones-v1.json under Membres supérieurs and Membres inférieurs, with autonomous Corps entier and Abdominaux / tronc entries. Group nodes organize and filter; they cannot be assigned directly. A new set-based exercise requires one primary zone. Selecting it removes/disables that same ID among secondaries. Historical unclassified exercises remain visible as Zone : Non renseignée and may be classified later.

The existing exercise list combines SQL-backed normalized prefix search with a zone filter. Parent filters include descendants, Non renseignés selects only exercises with no relations, and Toutes les zones never hides those rows. Each row displays primary, secondaries and the primary's derived group.

Editing an exercise

Every existing catalog item exposes Modifier. Editing a name trims its input, recomputes normalized_name, and rejects a normalized-name collision. The row retains its existing exercise_id; naming is presentation metadata, not identity. Completed session rows and the active draft retain their catalog row relationship and immediately resolve the renamed display text after reopen.

Profile fields (recording_mode, tracking_mode, data_fields) are editable only while an exercise has no completed-session or active-draft reference. Once referenced, Android displays the lock and returns an explicit incompatible profile result rather than silently reinterpreting work or creating another exercise. Renaming remains available independently.

The shared Compose shell supplies navigation context and the page header. Its content host provides one scrollable destination area; screens do not redraw a global banner or own a competing navigation control.

5. Session recording

Stable session identity:

se_<uuid-v4>

Session entry is profile-aware.

Sets + repetitions

Actual set values may be heterogeneous. The SETS + REPS editor presents ordered rows, each with its own repetitions and optional load. Ajouter une série appends one blank row and Supprimer la série removes only the chosen row; editing or removing a row does not alter the remaining row values.

The load heading is Charge (kg) for external resistance and Assistance (kg) for assistance equipment. A blank load is no recorded load, not 0; an entered load is finite and non-negative, and French decimal commas are accepted. The durable raw form preserves partial row input (including a blank row or a fragment such as 32,) across draft save and restore, and a failed validation or draft write presents a specific error without claiming the row was saved.

When an older compact raw draft is reopened, its repetition text can be expanded into the row editor from:

5x10
4,5,6,7,8,9,10,9,8,7,6,5,4
4..10..4

Sets + duration

Each performed set stores its own duration.

Continuous + duration

The form asks for duration and only the configured supplemental fields such as speed or distance.

Continuous work does not create fake sets.

Occurrences, equipment and actual loads

The same catalogue exercise_id may be added more than once to a session. Every occurrence receives its own stable entry_id, position, actual sets and optional equipment selection. Editing one occurrence replaces only that entry; it does not merge or alter another passage of the same exercise.

Machine / équipement (optionnel) searches the shared manifest by display name, physical-machine label and aliases. A selected equipment identity is stored on that occurrence in both the active draft and completed session. For SETS + REPS, the form is a row editor: every set owns an independently editable repetitions field and optional load field, and rows can be added or deleted without changing their neighbours. French decimal commas are accepted. Assistance (kg) is an explicit alternative load semantic, not an external charge. Empty load and an entered zero remain distinct. The completed-session detail renders the persisted rows in order with the matching Charge or Assistance heading, including an explicit empty-load marker.

6. Session draft editing

The repository durably saves every meaningful mutation, including session type, exercise selection/addition/removal, actual values and raw per-set form edits. Partial row text such as 32, is retained without normalization. A failed write displays a specific error and does not claim the latest change was saved.

Home shows Reprendre la séance en cours and an exercise-count/type summary. The ordinary new-session action opens an existing draft without overwriting it. Back returns Home and preserves the draft. Backgrounding, switching apps, Activity/configuration recreation, background process death and force-stop with relaunch preserve the draft; these paths were validated on the Samsung SM_G990B.

Retirer removes only that draft exercise and its actual values. It does not change the catalog or completed history. Removal survives restart. Supprimer la séance en cours requires deliberate confirmation; cancellation preserves the draft. Confirmed deletion leaves no completed session or stale resume action after relaunch.

Final save validates the durable draft, inserts the completed session and actual values, and removes the draft in one SQLite transaction. Failure rolls back and retains the draft for retry; repeated completion does not create duplicates. The existing completed-session save-time timestamp behavior is unchanged.

PC catalog reconciliation preserves draft references through catalog row ownership. If an editing selection no longer resolves, only the selection is cleared; added exercises and raw text remain, with a specific diagnostic. When a received catalog entry has the same exercise_id, a changed display name is reconciled in that same row. When the incoming PC identity differs but the normalized name matches, Android rekeys or merges only if recording and tracking modes match, the bounded data_fields masks are comparable by inclusion, and overlapping equipment metadata has the same load semantics. The incoming PC identity is canonical and the bit-mask union retains the richer profile. Completed and draft occurrence row IDs, entry_id, positions, values, equipment references and the active selection are moved transactionally. Any incompatible condition rejects the catalog transaction; name equality alone never authorizes a merge. Zone relations follow the same row-identity move only when one side is empty or both states are equal. Two different non-empty states are an explicit conflict; the repository never invents an automatic union of secondary zones.

7. Session history

Android exposes persisted local session history and profile-aware detail.

Set-based history renders ordered performed sets.

Continuous history renders its one activity record with configured supplemental values.

8. Body measurements

Supported metrics:

weight
neck
shoulders
chest
waist
hips
left/right arm
left/right forearm
left/right thigh
left/right calf

Rules:

empty field = not measured
at least one positive metric required
comma or dot accepted for decimal entry

Stable identity:

bo_<uuid-v4>

9. Automatic mobile snapshot

Android maintains:

Download/Trainlog/trainlog-mobile-export-v3.json

The V3 snapshot is refreshed after relevant local changes, including exercise, session, body-observation, equipment association and PC-catalog updates. It preserves entry_id, occurrence position, optional equipment and actual per-set weights and ordinary planning metadata. Android also publishes the V2 companion trainlog-equipment-associations-v2.json; its set and cleared states are targeted by (session_id, entry_id).

Before either dependent artifact, Android publishes its user-created equipment definitions as trainlog-mobile-equipment-definitions-v1.json. The strict trainlog-equipment-definitions v1 format uses stable IDs and the fields equipment_id, display_name, label_name, equipment_type, and load_semantics. Absence never deletes a definition; equal same-ID definitions are idempotent and divergent same-ID definitions conflict. Supplied-manifest IDs are reserved.

Android also writes trainlog-exercise-body-zones-v1.json, the sole zone companion used in both directions. It contains stable exercise/zone IDs only. After the file write succeeds, an identical local replay establishes/refreshes the publisher's shared baseline. A one-sided change is applied transactionally, and simultaneous divergence returns an explicit conflict without changing local relations. The unclassified state contains neither a primary nor orphan secondaries.

The user does not need a separate manual export step before synchronization.

An active draft is never included in completed history, session detail or this snapshot. Synchronization continues to exchange completed data while the draft stays local; no draft fields were added to the frozen mobile artifact. Training Feedback V1 likewise excludes draft_exercise_feedback; only its exact transferred completed rows are exported after finalization.

10. PC catalog access

PC-created files are accessed through a persistent Storage Access Framework grant.

The selected folder must be:

Download/Trainlog

The Sync screen always permits changing the stored folder selection.

No application-data reset is required to fix a wrong folder choice.

11. Android-triggered synchronization

The Sync screen exposes:

Synchroniser maintenant

Android writes:

trainlog-sync-request-v1.json

If MediaStore cannot reopen an older MTP-created canonical object, it may publish the exact collision sibling trainlog-sync-request-v1 (N).json. The desktop engine selects the newest canonical-or-suffixed request deterministically, while the stable request_id remains the replay boundary.

and waits for a matching:

trainlog-sync-receipt-v1.json

The receipt is matched by request_id.

On success Android then applies the latest PC catalog and displays the final result.

Before applying the PC catalog or its V2 artifacts, Android applies trainlog-pc-equipment-definitions-v1.json. Thus custom definitions are known before a received V2 association references them. Android schema v13 retains the non-destructive v7 -> v8 migration required for load_semantics = none. It also applies trainlog-exercise-aliases-v1.json before catalog and session reconciliation. The additive v11 → v12 alias table lets legacy exercise IDs resolve to one live canonical row; a live-source rekey preserves completed occurrences, the durable draft, selected form exercise, equipment relations, actuals, MAX and targets. Android republishes the same deterministic companion with no exercise-merge UI. After catalog/session/equipment reconciliation, Android applies the same body-zone companion so custom exercises receive their classifications.

A receipt belonging to another request is ignored as pending rather than misreported as the current result.

12. Synchronization ownership

Android does not initiate raw MTP operations itself.

MTP is host-initiated:

Android request
    -> PC trainlog-syncd
    -> shared desktop sync engine
    -> receipt

13. Build

Example local configuration:

cd android

printf 'sdk.dir=%s\n' "$HOME/Android/Sdk" > local.properties

JAVA_HOME=/usr/lib/jvm/java-17-openjdk \
./gradlew testDebugUnitTest assembleDebug

Install to a connected test device:

adb install -r app/build/outputs/apk/debug/app-debug.apk

local.properties is local machine configuration and must not be committed.

The APP_SHELL_V1 Android validation ran the JVM command above successfully: 84 tests ran, with 83 passing and one skipped historical-fixture test. The skip is RealAndroidV9BodyZonesMigrationTest.realVersionNineCopyMigratesWithoutChangingExistingTables, whose external TRAINLOG_ANDROID_V9_FIXTURE was unavailable. The build produced app/build/outputs/apk/debug/app-debug.apk (12,649,975 bytes; SHA-256 ddb1221d25db5be60eb2261d4b1dcf0fb7446e2a780c67aae862f37c15b7396d).

The host regressions include the production Compose callback for an existing active draft: it verifies that the generator's route, preview and raw input remain intact until the explicit keep/discard decision, and that keeping does not write the initialized SQLite database. The expected nine shell icons and ten referenced catalogue assets were checked byte-for-byte in the APK.

No emulator is installed, and this checkpoint did not install or run on the connected daily phone. Human visual/accessibility review remains pending for 320/360/393/412 dp widths, 100/130/200% font scale, IME behavior, drawer and form reachability, long translated/source text, scroll restoration feel, and TalkBack. See tests for the broader validation boundary.

14. Non-goals

Android is not intended to own:

  • canonical long-term analytics;
  • complex body/performance graphs;
  • cloud accounts;
  • direct SQLite-file synchronization;
  • exercise-name heuristics;
  • a mounted-filesystem dependency.

15. Equipment and multi-occurrence exchange V2

During exercise entry, Machine / équipement (optionnel) searches the shared catalogue by display name, physical-machine label and aliases. The selected canonical ID belongs to that session exercise entry, is durable in the active draft and completed session, and is visible in session detail. It may be cleared. The active V3 exchange preserves multiple ordered occurrences of the same exercise in one session through entry_id. The frozen V1 artifacts remain readable only as legacy artifacts and keep their historical one-exercise identity assumptions; V1 is not rewritten to claim V2 support.

For a SETS + REPS exercise, selecting equipment never changes that exercise profile: the form retains per-set repetitions and exposes Charge (kg). French decimal input is accepted (12,5); one value applies to all sets or values may be separated with ;. Assisted equipment is explicitly labelled Assistance (kg). Empty load remains distinct from an entered zero.

Nouvelle machine in that same selector creates a persistent local custom equipment entry with a generated stable eq_… ID and selects it immediately. The shared bundled catalogue uses reserved canonical IDs. User-created IDs are exchanged first by definitions V1, so a V2 association can resolve them on the receiving side; conflicts remain explicit and are never converted to null. The active-session list exposes Modifier <exercice>; saving replaces that entry in place, while cancelling only discards the form and preserves it.

16. Test-max sessions

Android session entry exposes:

Entraînement
Test max

The selection is persisted in the existing Android sessions.session_type column and exported in the mobile snapshot as:

training
max_test

History and detail visibly identify max-test sessions.

Selecting Test max is explicit metadata; Trainlog does not infer max tests from large repetition or duration values.

Its exercise form contains only the existing exercise search, optional machine/equipment selection, and Poids max (kg). French decimal commas are accepted; empty is distinct from zero and only a finite positive value can be saved. Each saved line is immediately visible as Exercice · Max : N kg, can be edited in place with the same entry_id, or cancelled without mutation. Several movement results may be appended successively.

Schema v9 stores the result in max_results or draft_max_results, never in a synthetic one-repetition set. Equipment remains occurrence context. History also lists the newest explicit maximum for each exercise_id, with date and equipment used.

A completed max-test detail exposes Reprendre ce Test max. The one durable draft then records the source session_id and retains all existing occurrence IDs, order, movement IDs and equipment. Finalization atomically replaces that same completed session and may append new occurrences. The completed source is left intact as the crash-safe baseline until finalization.

17. Audited limitations

Body Zones V1 defines no custom zone creation and no session generator. The repository already exposes manifest lookup/hierarchy, descendant and primary-only exercise filtering, direct exercise relations and the existing latest-MAX/history reads needed for future composition; it calculates no suggested load.

Android's stored normalized_name currently removes diacritics, while the frozen desktop/Python normalization contract uses NFC, Unicode whitespace collapse and case folding without accent removal. Existing Marche/Leg press data is unaffected, but changing this safely requires an explicit Android schema migration that recomputes every normalized key and handles newly exposed collisions. Schema v13 changes only the additive machine-exercise metadata and the approved exact-ID historical contexts.

The bundled exercise/equipment relationship metadata is seeded and preserved, including during exercise-identity reconciliation, but the current equipment picker searches all known supplied and custom definitions. Filtering or ranking that picker by exercise relation remains future gym-catalog policy.

Custom equipment remains identified only by equipment_id: Android never coalesces two different custom IDs merely because their labels match. This can leave conceptually duplicated definitions created independently on two devices, but avoids guessing across potentially different load semantics and persisted occurrence references.

The PC-catalog V1 inbox validates required IDs, modes, names, and bounded field masks, but unlike the newer mobile V2 and equipment-companion parsers it does not reject every unknown root or item key. Tightening this published V1 reader requires a compatibility decision rather than an incidental schema-v10 change.

Android requires data_fields = 0 for SETS, while the desktop model/API currently accepts known supplemental bits on either recording mode. Supplied profiles do not exercise this difference. Supporting a future set-based supplemental field requires an explicit shared-model decision.

18. Training knowledge V1 read APIs

Android bundles the six authored training-knowledge catalogs as immutable assets. TrainingKnowledgeCatalog validates and exposes their stable-ID records; it is not a second manually authored scientific table. TrainlogRepository.getTrainingExerciseContext() composes an exact persisted exercise with its direct/ancestor persisted zones, optional scientific mapping, compatible equipment, latest explicit MAX and recent occurrence/set preview. listExerciseOccurrences() and listExerciseOccurrenceSets() provide bounded follow-up pages. Occurrence and set limits are 1–32 and 1–64 respectively. Cursors order current data chronologically by original timestamp, session ID and occurrence ID, and do not preserve a snapshot across calls.

This read-only feature makes no further Android schema change (the runtime schema remains v11), does not seed rows, and does not export/synchronize new training-knowledge data. It does not implement recommendations or fatigue scores; planning metadata belongs to the separate schema-v11/session-generator contract. Its occurrence and latest-MAX readers use the same explicit temporal grammar, exact fractional comparison and bytewise ID tie breakers as C. The Android writer's omitted-seconds form is admitted, and emitted cursors retain the original source text. Production pagination/MAX parity tests pass. The tranche is TRAINING_KNOWLEDGE_V1=PASS. The Android loader enforces canonical exercise and equipment identity syntax, bidirectional exercise/capability compatibility, HIGH evidence source type, and non-unresolved BODY ZONE audit evidence. Its full source and uncertainty contract is in Training knowledge system V1.

The normal shell exposes manual sessions but not SESSION_GENERATOR_V1 pending V2. When importing the PC catalog, Android applies the versioned canonical exercise-name mapping for mapped IDs; unmapped user exercises remain editable. Equipment Detail reads the shared V2 relation asset, shows verified possible exercises with French confidence/evidence status, and opens Exercise Detail. Exercise Detail shows exercise-owned muscles, movements, BODY ZONES, and compatible physical equipment. Custom equipment receives no inferred anatomy. Saved exercise feedback and session follow-ups expose Modifier. The latest text is prefilled, resumed dictation appends to it, and save creates an immutable v15 revision without changing observed_at or the H+ label. The feedback editor presents microphone and stop actions with accessible labels and keeps the live transcript in a wrapped, multi-line area. Its speech intent requests 1.5 seconds of complete silence (and 1.1 seconds of possibly complete silence) before completion. These durations are best-effort hints: Android recognition engines may ignore them. Trainlog neither fabricates results with a local timer nor enters an automatic restart loop; natural completion preserves the captured text, while an explicit Stop remains immediate.

Every user-initiated destructive commit uses an action-specific confirmation. Cancel and Back are safe, outside taps cannot confirm, and the destructive button is explicit. Draft occurrence warnings name nonzero performed-set, continuous, MAX and feedback children; series deletion and whole-draft abandon use the same gate.