The second CI round got past the missing configure import but still
failed in the same file: bare name accessors inside sourceSets { }
(commonMain.dependencies { ... }, commonTest, jvmTest) are kotlin-dsl
script syntax generated for .kts files. Plugin source compiled as plain
Kotlin has no such accessors on its classpath, so the four dependency
blocks failed with receiver type mismatches while every real member call
around them - jvmToolchain, jvm(), the ios targets, binaries.framework -
already resolved.
Configure the source sets through the container API instead:
sourceSets.getByName("commonMain").dependencies { ... }. getByName,
dependencies and implementation are all members on types that ship with
KGP 2.4.10, verified against the gradle plugin jars byte for byte.
getByName is safe at this point because jvm() above has just created the
jvm source sets synchronously; commonMain and commonTest exist as soon
as the multiplatform plugin is applied.
The iOS CI run failed in both jobs before reaching any product code: the
convention plugin itself did not compile. CoreDomainPlugin.kt used the
reified extensions.configure<KotlinMultiplatformExtension> { } form, but
unlike every other plugin in this directory it was missing the
org.gradle.kotlin.dsl.configure import. Without it only the member
overloads taking an explicit type parameter resolve, so the extension
receiver cannot be inferred and all twenty subsequent unresolved
references - jvmToolchain, jvm(), iosArm64(), binaries.framework,
sourceSets with the commonMain/commonTest/jvmTest accessors - are one
cascading failure, not twenty bugs.
build-logic is the first thing both CI jobs compile, and it has never
been compiled anywhere before this run, so the workflow is doing exactly
what it was added for: catching what no local machine can check.
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.
Carries the CW record fixes: the record no longer deletes its own text while decoding,
a drifting tone no longer wipes it instead of filling it, the archive path no longer
races itself when capture stops, and the decoded text can finally be copied off the
screen.
Also stops the APRS version string drifting, which the comment on it had predicted and
which had already happened again: it still read 4.6.0 while the app shipped 4.6.1, so
every station on the network was told the wrong version. AprsReporter now takes the
version as a parameter and the app module passes BuildConfig.VERSION_NAME, which
required turning on the buildConfig feature - AGP 8 does not generate the class
otherwise. The literal cannot fall out of step with the build again.
New feature/status module: fetches https://amsat.org/status/ and
renders a live status grid in the official site colors:
- Parser (AmSatParser): 47 satellites x 6 days x 12 two-hour slots,
official colors (blue=Active, orange=TLM/Beacon, pink=Not Heard,
deep-orange=Conflicting, gray=none); 598+ report details extracted
from inline JS tooltips (callsign/date/time/grid)
- Three-level viz: color grid -> report count -> tap day cell opens
report list dialog
- Manual refresh with spin animation + last-updated timestamp +
legend row; loading/error states
- New "AMSAT" entry in the More menu (Screen.AmSat), integrated with
page-order / hide-page settings (SettingsScreen screens list,
defaultSubMenuOrder, allNavItems, migration for existing users)
- AmSatRepository via IRemoteSource.getStatusHtml() (UA header);
shared remoteSource promoted to a lazy class property in MainContainer
Radar page Log tab: local entries now grouped by pass session with a
thick divider + satellite label between groups (matches the log page).
What's new updated in EN/ZH/TR/IN/ID. Version bumped to 4.5.3/453.
Not released (user gates all releases).
Compose integration of the ported CW decoder engine:
- CwDecodeScreen: AndroidView embedding the ported activity_main.xml,
lifecycle delegated to the ported MainActivity controller (onCreate ->
onResume, onDispose -> onPause/onDestroy), RECORD_AUDIO runtime
permission flow (with permanent-denial -> app settings), original
options_menu actions as a top button row (pause/clear/save/record/
settings), double-back-to-exit preserved
- CwSettingsDialog: message_type (general_text/ham_radio_qso),
text_font_size (7-99), and the 9 color keys (bg_color/text_color/...)
reading/writing the same prefs keys as the original app
(getPackageName()+"_preferences"), colors sourced from I2.b tables
- Navigation: Screen.CwDecode ("CwDecode") placed between Roaming and
Map in the default order; defaultScreenOrder updated; ic_cw morse icon;
nav_cw strings (en/zh/tr); app depends on :feature:cw
Verified: :feature:cw:compileDebugKotlin + :app:compileDebugKotlin
BUILD SUCCESSFUL (first pass, no errors).
Ports the QTH定位器 (com.us1pm.gridsquarelocator) location panel into Look4Sat as a new 'Roaming' page, restyled with the app's own look.
UI (top to bottom):
- Info header: GPS status dot, big 8-char locator, Lat/Lon rows with DMS + 5-decimal display, and a GPS 定位 button
- 3x3 grid panel: the current 4-char Maidenhead square (e.g. OL42) centered, surrounded by its 8 neighbors (OL33..OL51), with a red position marker in the center cell
Logic:
- QthConverter gains qthNeighbors(square) building the 3x3 grid with field/square carry at boundaries (AA00 wraps to RR99, IO91 crosses into J field), and qthToSquare(locator) extracting the 4-char square
- Verified against the decompiled app algorithm and the reference screenshot (OL42 grid matches exactly); 9 unit tests cover normal, boundary and field-wrap cases
Navigation:
- New bottom-nav item 'Roaming' between Match and Settings, with a crosshair icon
- New feature:roaming module (ViewModel + Compose screen) registered in the app
Build config:
- Lowered Gradle JVM heap from -Xmx6g to 768m: the 2GB build server froze on the old value; heavy release builds stay on GitHub Actions
The official app signs com.rtbishop.look4sat with its own certificate. A fork sharing that applicationId cannot be installed over the official build (signature mismatch) and users saw overwrite/install failures.
- Add applicationId version catalog entry; namespace stays com.rtbishop.look4sat so source imports are untouched, applicationId becomes com.rtbishop.look4sat.bg7nta.
- Update PROPERTY_SATELLITE_DATA_OPTIMIZED meta-data to the fork id.
- Document the fork-applicationId requirement in the version catalog.
Port satlover.de dual-station pass matching feature:
- New feature/mutual module with mutual pass data model
- MutualViewModel: compute overlapping passes for two stations
- ElevationCurveChart: Canvas-based dual elevation curve with drag
- MutualScreen: input form + results with expandable cards
- Navigation: add Mutual tab to bottom navigation bar
- i18n: add Chinese/English strings for new feature