Android Plugin - cyberconical/halbmarathon GitHub Wiki
The only native part of the app. It lives in plugins/run-tracker/ and is included in the build. It only does what a web page can't: read location in the background, vibrate at fixed times and speak while the screen is off.
The ready-made Capacitor location plugins depend on Google Play Services. The app is meant to run without Google services. So run-tracker only uses Android's LocationManager with the GPS provider.
| File | Job |
|---|---|
RunTrackerPlugin.java |
bridge to JavaScript: methods, permissions |
RunTrackerService.java |
foreground service: read GPS, collect fixes, cues, notification, voice announcements |
PaceTracker.java |
counts distance in the background with the same rules as analyze() and reports every 500 m or 1 km. Plain Java, testable on a computer |
PaceSpeech.java |
the English words of an announcement. Plain Java, testable on a computer |
Announcer.java |
the phone's text-to-speech: English voice, other audio turned down, short wake lock |
RunLog.java |
anonymous diagnostic log |
AndroidManifest.xml |
permissions and service entry (type location) |
build.gradle, package.json
|
integration into Capacitor 6 |
Called from index.html via Capacitor.Plugins.RunTracker.
| Method | Parameters | Returns | Purpose |
|---|---|---|---|
checkPermissions() / requestPermissions()
|
{permissions: ["location" | "notifications"]} |
status per alias | from the base class |
start |
cues (JSON text), meta (session ID), lang ("en" or "de"), voice (0, 500 or 1000: metres between announcements, 0 = off), maxAcc (accepted GPS error in metres, 35 or more in test mode) |
{startedAt} |
start the service. Error codes: PERMISSION, GPS_OFF, RUNNING, START_FAILED
|
testVoice |
– | – | speak a sample announcement. Error codes: NO_ENGLISH (no English voice installed), UNAVAILABLE (no speech engine) |
pause / resume
|
– | {t} |
cancel and reschedule cues |
stop |
– | {count} |
end the recording; fixes stay available until the next start |
getFixes |
from |
{fixes, next, running, paused, gps, voice, voiceStep} |
fixes from position from, at most 2,000 per call. voice is off, init, ready, no_english or unavailable; voiceStep is the step in metres the running service uses (0 = off) |
getStatus |
– | {running, paused, gps, gpsEnabled, count, startedAt, meta, cues, state} |
state for reattaching |
saveState |
state (JSON text) |
– | store the UI's small state in the service |
diagLog |
e (text) |
– | write a UI event to the log |
getLog |
– | {text, enabled} |
read the log |
clearLog |
– | – | delete the log |
setLogEnabled |
on |
– | switch the log on or off (off deletes it) |
The app shows its own, translated error messages based on the error codes. The plugin's English reject messages are only for developers.
- Starts as a foreground service of type
locationwith a notification in the channel “Run recording” (low importance). Title, text and channel name follow the app's language (langpassed withstart). Tapping the notification opens the app. - Requests a GPS fix every 1,000 ms, without a minimum distance.
- Stores fixes as
[time, lat, lon, accuracy, speed]in a list only in memory, at most 30,000 (over 8 hours at one fix per second). - Schedules the cues on active running time. On pause they are cancelled, on resume rescheduled. The cue texts come from the app in its language.
- Holds a wake lock (at most 4 hours) for runs with cues, so cues come on time with the screen off. A failure there is logged and does not stop the recording.
-
Voice announcements (if
voiceis 500 or 1000, or 10 in test mode): every fix also goes toPaceTracker, which uses the same distance rules as the app (accuracy limit, jump filter, GPS speed first, noise limit, test mode). When a mark is crossed,Announcerspeaks it in English, see GPS recording. The speech engine is started with the recording and released when it ends. If it is missing or has no English voice,getFixesreports it and the recording continues without speech. Nothing is counted or spoken while paused. - Does not restart after the process ended (
START_NOT_STICKY). Without fixes there is nothing to continue. - If Android starts the service only to end it right away, it still calls
startForeground(). Otherwise Android would end the app for a broken promise.
The state lives in static fields in the same process as the plugin. If only the UI dies (WebView, activity), it survives. If the process dies, it is gone.
| Permission | Why |
|---|---|
ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION
|
GPS. Android 12 and newer require the approximate location too. |
FOREGROUND_SERVICE, FOREGROUND_SERVICE_LOCATION
|
service in the foreground of type location |
POST_NOTIFICATIONS |
the service notification on Android 13 and newer |
VIBRATE |
cues |
WAKE_LOCK |
keep the CPU awake for punctual cues, and for the few seconds a voice announcement takes. Without it, Android ends the app when an interval recording starts. |
The build checks the merged manifest for exactly this list, see Build and release.
Besides permissions the manifest has a <queries> entry for android.intent.action.TTS_SERVICE. From Android 11 an app can only find the phone's speech engine if it asks for it there. Without it, voice announcements would silently not work.
A small, anonymous log for troubleshooting. The rules are enforced in code, not just documented:
-
Where:
files/diagnose.login the app's private storage. At most 200 lines, then trimmed to the newest 150. -
Time: only seconds since the process started (
+12s), no clock time and no date. -
Characters: only letters a to z, digits, space and
_ : . = + - @ $ ( ). Everything else becomes?. At most 100 characters per line. -
Coordinate guard: every number with three or more decimals is replaced with
#. -
Crashes: class and code location of the exception (
SecurityException top=… app=RunTrackerService.startTracking:212), never its message. The normal crash path then continues. -
Why the last process ended: from Android 11 via
ApplicationExitInfo, for exampleexit:LOW_MEMORY imp=fgservice vor=3min. Each reason is noted only once. -
Switch: in
SharedPreferences(halbmarathon_diag). Switching off deletes the file.
| Event | Meaning |
|---|---|
proc:start api=N |
new process, Android API level |
exit:REASON imp=… vor=Nmin |
why Android ended the previous process, N minutes ago |
plugin:load |
plugin loaded (UI started) |
start:ok, start:reject CODE, start:fail …
|
start of a recording |
svc:create, svc:cmd running=…
|
service created and started |
svc:fg ok cues=N, svc:fg fail …
|
foreground reached or not |
svc:gps ok enabled=…, svc:gps fail …
|
GPS requested |
svc:wakelock ok, svc:wakelock fail …
|
wake lock |
fix:first after=Ns, fix:n=N
|
first fix after N seconds, counter every 500 fixes |
gps:off, gps:back
|
GPS switched off or back |
svc:voice step=N maxacc=M, svc:voice fail …
|
voice announcements started (or failed) |
voice:ready lang=en_US, voice:no-english, voice:init-fail
|
the speech engine is ready, has no English voice, or did not start |
voice:say n=N |
announcement number N was spoken (a counter only) |
voice:test |
the voice test was pressed |
voice:error …, voice:speak-error …, voice:setup-error …
|
a problem with speech. The recording continues without it |
svc:pause, svc:resume, svc:stop fixes=N
|
controls |
svc:taskRemoved |
app swiped away from recents, service keeps running |
svc:trim level=N, svc:lowMemory
|
memory pressure |
svc:destroy running=1 |
the system ended the service (with running=0 it was the person) |
crash:… |
crash with class and code location |
js:boot, js:begin …, js:restore:…, js:end …
|
events of the UI |
(vor is German for “ago”; event words stay unchanged so older logs read the same.)
How to read a log: Troubleshooting.
- In Java:
RunLog.add("area:what n=" + number); - In
index.html:hmlog("area:what n=" + number);
Only fixed words and small numbers. Never places, points in time, text people typed or session identifiers.