The orbital maths, the satellite models and the repository contracts sat in a Kotlin/JVM
module, so an iOS target could not share a single line of them: java.lang.String.format,
InputStream, System.currentTimeMillis, java.util.Locale and org.json are all JVM-only, and
the tests that covered them used JUnit4. core:domain now declares jvm, iosArm64 and
iosSimulatorArm64 targets, its sources moved to commonMain/commonTest, and the JVM-only
pieces were replaced with multiplatform equivalents: java.lang.String.format by a shared
printf implementation, System.currentTimeMillis by kotlin.time.Clock, InputStream by
ByteArray, org.json by kotlinx-serialization, Locale by nothing at all. Tests that read
classpath resources (javaClass.classLoader) moved to jvmTest, because that is JVM-only
behaviour rather than a JVM-only API.
Auditing the migration against the old module turned up four things that were wrong rather
than merely ported:
- java.lang.String.format rounds the shortest decimal representation of a double half-up,
not the binary value: "%.3f" of 0.5005 is "0.501", because the stored double is
0.50049999999999994493. The shared implementation scaled in binary first and printed
"0.500", which would have changed APRS position packets and the Wavelog frequency fields
against the released Android app. It now takes the digits from the decimal representation
and rounds them with integer arithmetic, and jvmTest compares it against
String.format(Locale.ROOT, ...) over 40 000 sampled doubles plus the boundary cases, while
commonTest pins literals so the iOS run checks the same digits.
- The queue mutators lost the kotlin.jvm.Synchronized monitor each of them had. It is not a
JVM-only annotation - it is an optional expectation, so it still compiles in common code -
but the stdlib deprecated it for common use in 1.8 and made it an error in 2.1. The monitor
is a platform actual now: the JVM keeps the real monitor, since Compose and the upload
coroutine both reach the queue there, and iOS carries a documented placeholder until the
iOS side has a second thread to protect against.
- 107 assertions in DataParserTest and QthConverterTest were bare kotlin.assert calls, which
a build without -ea skips silently: they are assertTrue now, so the iOS run cannot pass
vacuously. The three Locale.setDefault cases (ar-EG, bn-BD, fa-IR) that used to guard APRS
output against Eastern Arabic digits moved to jvmTest instead of being deleted with the
Locale dependency - APRS-IS is an ASCII protocol, and Locale.setDefault does not exist on
iOS.
- @Volatile on the LoTW name cache would not have compiled for iOS either: kotlin.jvm's
variant is an error in common code since 2.1. kotlin.concurrent.Volatile is the
multiplatform annotation, and it is the stronger form: it takes effect on Kotlin/Native
rather than being ignored.
A second audit pass over the files the first one could not reach - the HTTP client, the
parsers, the queue and the injection - found three more:
- OkHttpHttpClient built its Request outside the try, so a URL OkHttp refuses to parse left
postQso/testToken/getStation as an exception, and neither caller catches one. The client it
replaced reported HTTP -1 and let the caller treat it as a failure; building the request
inside the try restores that, and a transport failure reports -1 again rather than 0.
- WavelogQueue's readers were stricter than the org.json ones they replaced. A timestamp
stored as 1234.0 (or "1234.0") read back as 0L instead of 1234 - a QSO uploaded as 1970 -
and a field holding an object or array threw the whole list away instead of falling back.
The readers coerce decimals, keep the old defaults and no longer throw, matching optLong,
optInt, optString and optBoolean.
- The ADIF dates went through the JVM default locale before, so a device set to Arabic wrote
Eastern Arabic digits into the QSO date. The shared formatter only ever produces ASCII,
which the locale cases in AprsPacketDefaultLocaleTest pin down.
Verified locally with ./gradlew jvmTest (343 tests, 0 failures) and the multiplatform gate
in check-multiplatform.sh, which now also refuses JVM-only stdlib APIs that resolve in common
code but fail to compile for iOS: @Synchronized, kotlin.jvm.Volatile, synchronized(),
toUpperCase/toLowerCase/capitalize, BigDecimal. The iOS targets themselves need the macOS
runner in .github/workflows/ios-kmp.yml.
Two defects in our own AMSAT page, both found by auditing the change that exposed
them.
The day columns claimed to be dates but were a rolling window anchored on the
fetch time. Fetching at 06:07 UTC put 17.9 hours of yesterday into the cell
labelled today; measured against a live amsat.org page of 1021 reports, 73% of
them landed in the wrong day column and none matched the official cell. Days are
now UTC calendar days and slots are fixed UTC bands - slot 0 is 22:00-24:00, slot
11 is 00:00-02:00 - so a cell's contents match its label whenever it is fetched.
The day cell painted one colour for the whole day, taken from the first slot that
had a report, so a satellite that worked all morning and failed all afternoon
looked identical to one that worked once - the reported symptom. It now draws one
stripe per two-hour slot in the same 64x28 dp footprint. Twelve stripes are about
5 dp each, roughly 15 px at 440 dpi, and runs of the same status merge visually,
so a day reads as a few blocks rather than twelve lines. Every density from ldpi
up allocates all twelve without dropping one, and the 4 dp corner radius leaves
95% of the end stripes visible. The report count text is gone; tapping a day
still lists every report from it, which was already the richer view.
buildStatuses and ApiReport are internal rather than private so the grid contract
can be tested. AmSatSlotBuildTest drives it directly: fetchStatus cannot be
tested here because the parsing around it uses Android's JSONObject, a JVM stub
that makes every call return null - eight of nine tests written against it failed
for that reason before being rewritten.
Also corrects three KDoc comments claiming 5 days when the code builds 3, and
records in AGENTS.md that the status colours are ARGB literals in core:data,
duplicated in MainTheme, which anything needing themeable or colour-blind-safe
colours has to fix first.