The radar circle is a square, so it is limited by the shorter side of
the card it is drawn in: the small-window layout gave the radar card a
height of ~156dp in a 411x450 window, which shrank the circle to ~36% of
the page width while the full-screen layout shows ~84%.
- measure the real window with BoxWithConstraints instead of trusting the
window size class, which can still report the full screen while the app
runs in a split/floating window
- when the classic 1:1 split would give the radar less than 75% of the
page width, use a fill layout: the radar takes the largest square that
fits, a usable pager block is reserved under it while that keeps the
circle at its full-screen share, otherwise the pager collapses into a
48dp translucent tab strip with a tappable panel
- keep the normal two-row top area in the vertical layout: TimerRow and
NextPassRow both carry weight(1f) internally, so putting them in a
single row splits that row into two 50% blocks
- extract the geometry into pure Dp functions (useFillRadarLayout,
radarFillSizes) covered by unit tests
The previous compact-mode trigger used currentWindowAdaptiveInfo(),
which reports the full-screen size class in split-screen / multi-window,
so the radar never enlarged there. Measure the actual window bounds with
BoxWithConstraints: compact (radar weight 1f fills all space, pager fixed
132dp strip) when maxWidth < 600dp && maxHeight < 480dp.
build: release version 4.4.7-ba7opf.17.6 (529)
When the window is vertical (narrow) and too short for split-screen or a
small floating window, give the radar card 1.6/0.6 weight instead of the
usual 1:1 so the square radar plot stays large. The pager card shrinks
but all its pages are scrollable, so nothing is clipped. Full-screen
vertical layout (enough height) is unchanged.
P2 — markUploaded now only marks the records that actually made it
into the accepted TQ8 batch (preview.submittedIds), never the whole
candidate list. Previously, un-signable records skipped by prepare()
were still marked UP after any accepted upload, falsely showing them
as uploaded.
P4 — surface why records did not upload:
- When audit finds nothing pending, distinguish unavailable records
(invalid call/date — check logbook) from unknown-result records
(not retried automatically) instead of a blanket 'no pending'.
- Upload preview dialogs (Log tab + logbook) show a breakdown line:
N already uploaded/duplicate · N unknown result · N un-uploadable.
- Remove LOCATION_MISMATCH check: the recorded myGrid never enters the
TQ8 (the tSTATION grid is signed from the current certificate station),
so a portable/ADIF-imported record with a different grid must not block
upload. Logging and uploading are now fully decoupled from the
certificate location.
- Fingerprint now covers the CONTACT only (call, date, band, mode,
satellite, freq), excluding station fields: changing grid/zones/county/
IOTA no longer invalidates ledger entries, so already-uploaded contacts
are not re-queued and rejected as duplicates.
- prepare(): catch un-signable contacts per-record instead of aborting
the whole batch; count them as skipped and upload the rest.
- audit(): count lotwUploaded records as uploaded (ledger-loss fallback).
The LoTW upload card read uiState.lotwCertificate which was only
populated when the config dialog was opened (LoadLoTWUploadStatus).
On first entry to the settings screen it stayed null, showing
'certificate not imported' even though a certificate was stored.
Load certificate + station once in init so the card is correct
from the first frame.
The import previously swallowed all exceptions silently. Now the upload
config dialog shows a specific message for: wrong password, expired
certificate, invalid file, and unknown failures.
- displayMode no longer trusts a stale submode for non-MFSK modes: FM/CW/SSB
records (including ones persisted with the old default submode=FT4) now show
their real mode; only MODE=MFSK uses the FT4 submode. QsoRecord defaults
changed mode=MFSK->FM, submode=FT4->''.
- LoTW report bridge sets submode=FT4 for MODE=MFSK so confirmations still
match local FT4 records.
- Logbook and LoTW upload config dialogs now use SharedDialog (bottom-sheet),
matching the LoTW confirmed-grids dialog style.
The Choose .p12 button was gated on a non-blank password, which forced users
to type the password before selecting the file. Now the file picker is always
available; after selection the filename is shown, then the password field and
an Import button appear.
- Port TQ8 signing/upload pipeline (core/data/lotw: LoTWConfig, LoTWKeyMaterial,
LoTWSigner, LoTWStorage, LoTWUploadRepository) with bundled ARRL config.tq6
- Multi-grid station locations: primary grid in GRIDSQUARE + full set in
MY_VUCC_GRIDS (unsigned tSTATION extra, IOTA precedent); signer matches
myGrid against the whole grid set
- Satellite-name normalization: SatNOGS names ("SO-50 (SaudiOSCAR 50)") resolve
to official LoTW names by exact-then-substring match
- Logbook storage: separate QsoDatabase (version 1, no migration on existing
installs), QsoRepository with dedupe/confirmation merge; LoTW sync (manual +
auto) feeds confirmations into the logbook and marks local uploads confirmed
- Radar Log tab: single-callsign recording, per-satellite mode preset
(CW/SSB/FT4), English social post generation (copy/share), LoTW upload with
one-shot preview confirmation + receipts ledger
- Settings: 日志本 section (records + confirmation state, delete on tap) above
LoTW config; LoTW upload config (TrustedQSL .p12 import + password, station
grid list incl. IOTA/CQZ/ITUZ)
- i18n strings (en/zh)
A LoTW station location (台址) can span several grid squares: the report
emits <MY_VUCC_GRIDS> (comma-separated, MY_GRIDSQUARE often absent) on
those records, but the app parsed only <MY_GRIDSQUARE>, so every extra
grid of a multi-grid 台址 silently vanished from both the blue roamed
stripes and the operated-grid (VUCC) selector.
- GridQso gains myGrids (full own-grid set), myCallsign and stationKey
(MY_* station snapshot); myGrid stays as the first grid for back-compat.
- parseConfirmedGridQsos parses MY_VUCC_GRIDS + the MY_* snapshot fields;
parseRoamedGrids parses MY_VUCC_GRIDS too and uppercases (lowercase
grids never matched the overlay labels).
- SettingsRepo persists mgs/mc/sk; old data falls back to single-grid sets.
- MapScreen groups the selector's 台址名单 by the 台址's full grid set:
a multi-grid 台址 is one entry listing all its grids (e.g. 'OM60,PM01'),
and selecting it fills every worked grid worked from any of them.
Identical grid sets merge (VUCC-equivalent), so partial MY_* snapshots
can't split one 台址 into duplicate entries. Own grid still never
striped.
Verified against real reports: BH6RJD (807 SAT QSOs) yields 7 台址
including 4 multi-grid ones; BA7OPF's own data stays a single 'OL62'
entry. 28 LoTW parser tests pass (5 new).
User correction: AO-123 is ASRTU-1 (NORAD 61781, launched 2024-11-04,
in orbit), not Fox-1D (43137, re-entered 2024-02). Update the hardcoded
FM set and its tests to include ASRTU-1.
Root cause of the recurring empty AMSAT Live filters: OkHttp defaults to a
10s read timeout and amsat.org's live pages take 7-10s+ from many
networks — every sync timed out, fmNames/linearNames stayed empty, and
(with no previous list, e.g. right after clearing data) the filters were
written as EMPTY. The stale pre-whitelist FM list survived only because
older syncs had occasionally succeeded and the failure path preserved it.
Fix:
1. OkHttp timeouts raised (connect 20s / read 30s / write 30s / call 60s)
in MainContainer.provideRemoteSource so slow AMSAT pages actually load.
2. updateAmSatLiveLists gains a last-resort fallback: when the AMSAT page
is down AND there is no previous list to clean, derive FM/Linear from
the local transceivers (FM / SSB,CW,USB,LSB modes) intersected with
the amateur whitelist, so a network timeout never wipes the filters.
ISS module aliases stay excluded by the whitelist intersection.
Regression test: page fails + empty previous list -> fallback yields
{25544} (26700 excluded by whitelist).
resolvePerName crashed with NoSuchElementException (all.first()) when an
AMSAT live-page name had no matching local entry (TEVEL2-1..9, RS95S, any
brand-new sat not yet in the local TLE). The exception was swallowed by
updateAmSatLiveLists' runCatching, so the FM/Linear lists were NEVER
rewritten — they stayed at the stale pre-whitelist snapshot containing
all five ISS modules, which is why ISS kept showing as DESTINY despite the
15.4/15.5/15.6 disambiguation fixes.
Fix: return an empty set for names with no local match (correct skip
semantics) so the rest of the page still resolves and the ISS module
collapse runs. Regression test covers the real AMSAT page mix (ISS +
unknown names).
Root cause of "All filter is empty": 'All' is a real TLE source type
(CelesTrak active group) in satelliteDataUrls, but setSatelliteTypeIds
had an upstream-legacy 'if (type == "All") return' that silently dropped
its ids on every sync. Selecting 'All' in the type dialog then resolved
to an empty catnum set -> empty list, while entries stayed populated
(which is why satellite counts looked normal).
Also fix getTypesList: removeAt(0) removed the alphabetically-first real
source ('ARISS') instead of the intended placeholder; now excludes only
'Other' (empty URL), so all real source types incl. 'All'/'ARISS' are
selectable.
Tests: remote update persists All ids; All type resolves to persisted
CelesTrak active ids; getTypesList excludes only Other.
Two independent defects kept ISS (DESTINY) visible in the satellite-page
AMSAT Live FM filter even after installing the fix and syncing data:
1. UI-layer staleness (SelectionRepo): resolveTypeIds ran only when the
filter type changed and snapshotted the catnum set. A background data
sync rewriting the FM/Linear lists never re-triggered resolution, so
the UI kept filtering with the pre-sync (dirty) set until restart or a
type toggle. ISettingsRepo now exposes a monotonic amSatListsVersion
StateFlow, bumped by SettingsRepo.setAmSatCatnums; SelectionRepo
combines it with currentTypes so every rewrite re-resolves instantly.
2. Data-layer staleness (DatabaseRepo): when the AMSAT FM page fetch
failed, the previous list was kept verbatim — including pre-whitelist
era lists containing all five ISS module entries. The stale list is now
re-disambiguated: previous catnums are looked up by local name and run
through the same whitelist / smallest-catnum collapse, so ISS modules
always collapse to the primary ZARYA (25544).
- Failed whitelist fetch (raw.githubusercontent.com unreachable on some
networks) no longer wipes the persisted whitelist or admits every ISS
module alias: fall back to the persisted whitelist, and only persist
when a stream was actually fetched.
- Multi-match collapse without any whitelist now keeps the primary entry
(smallest catnum, ISS ZARYA=25544) instead of every alias (DESTINY etc.).
- Failed FM/Linear page fetch keeps the previous lists instead of
clearing the filters.
- Amateur whitelist gets a jsDelivr CDN mirror as fallback source.
- Resolve ISS station modules (ZARYA/UNITY/ZVEZDA/DESTINY/NAUKA) to the
primary 25544 instead of whichever module appears first in the TLE source
- Match OSCAR designators to local names spelled 'OSCAR N'/'AMSAT-OSCAR N'
(AO-7 -> OSCAR 7) so Linear lists keep AO-7 on SatNOGS-sourced data
- SSTV virtual type now uses the transceiver service class (Amateur only)
plus launcher-debris name exclusion, dropping retired weather birds
(TIROS) and rocket bodies (ARIANE 6 R/B) while keeping ISS/CSS/TEIKYOSAT
- Persist the AMSAT active-frequency whitelist (palewire mirror) on sync;
used only to disambiguate multi-match names, never as a hard filter
- matchesAmSatName: exact token match instead of substring contains,
so 'ISS' no longer matches AISSAT-1 and 'AO-7' no longer matches
AO-73 (FUNCUBE-1); resolves missing ISS in FM and missing AO-7 in
Linear lists
- resolveTypeIds: intersect SSTV catnums with current in-orbit TLE set,
excluding retired satellites (e.g. TIROS) whose radios are tagged SSTV
- add regression tests for token matching
Two correctness fixes for the FM/Linear/SSTV filters:
1. Satellite type picker: virtual transponder types (AMSAT Live FM,
AMSAT Live Linear, Live SSTV) are now mutually exclusive with the
regular TLE-source types - ticking a virtual type clears Amateur/
CubeSat/etc and vice versa, so selecting 'AMSAT Live FM' no longer
unions in hundreds of non-transponder satellites. Virtual types
still combine with each other. Also: an empty virtual-type resolve
now yields an empty list instead of falling back to 'show all'.
2. Mutual-match fallback: when the AMSAT lists are empty (first run /
sync failure) the DB mode filter used downlinkMode alone, so any
downlink-only beacon sharing a mode label was misclassified
(ISS/IO-86 CW beacons into Linear, FO-29's FM Digitalker into FM).
New getIdsWithModesAndUplink() requires uplinkLow IS NOT NULL,
matching real transponders only. SSTV filter keeps the old query
(SSTV sats are often downlink-only).
Shared virtual-type names moved to Sources.virtualTypeNames.
The satellite selection screen's 'Select satellite type' dialog now lists
three virtual types first (AMSAT Live FM, AMSAT Live Linear, Live SSTV)
before the TLE-source types. Selecting one filters the satellite list to
that transponder/activity set; multiple selections union. 'All' is kept.
- SelectionRepo.getTypesList: virtual types prepended (All preserved)
- SelectionRepo.resolveTypeIds: FM/Linear -> AMSAT catnum sets,
Live SSTV -> mode=SSTV radios; other types via persisted IDs
- Tests: virtual-type filtering, union, clear, list-order assertion
Mutual match page gains FM/Linear transponder-type filters. The
authoritative source is AMSAT's human-maintained Live FM/Linear
Satellites pages, fetched during data sync and stored as catnum sets;
filters resolve to those sets (falling back to DB mode filter when
empty), and unchecking both shows a prompt instead of querying.
- DataParser.parseAmSatLivePage: table rows to satellite names,
TEVEL2-1 thru TEVEL2-9 range expansion, bracket-alias matching
- SettingsRepo: AMSAT FM/Linear catnum sets persisted in prefs
- DatabaseRepo: fetch AMSAT pages in updateFromRemote, best-effort
(failure keeps previous list, never blocks the update)
- getIdsWithModes: AND isAlive = 1
- MutualScreen: FilterChip row inside MatchSearchCard
- Tests: parser (4) + viewmodel (3) + repo fakes
- MarkedStation storage upgraded to an ordered list per grid
(Map<String, List<MarkedStation>>); JSON array shape with legacy
v14.1 single-object migration on read
- MapAction: SetMarkedStation appends (duplicate callsign ignored),
RemoveMarkedStation deletes by callsign, PinMarkedStation moves a
mark to the top of the list
- MaidenheadGridOverlay: red fill for any marked grid unchanged;
first-call label mode shows the FIRST (pinned) marked callsign
- MapScreen: one row per mark (pale-yellow callsign), pin icon
(冖 + stroke ↑, drawn in-app) left of the Marked label — shown only
when the grid has 2+ marks; pinned row's icon grey, others pale
yellow; delete icon per row
- New ic_pin.xml vector drawable; strings grid_mark_pin
- MapScreen: mark-a-station callsign input uppercases as you type
- MutualViewModel: read passesSettings.hoursBefore, shift the mutual
match query start to now-hoursBefore so historical passes are
included when the main Passes page has a lookback window set;
endTime stays at now+hoursAhead
- New MarkedStation model + ISettingsRepo/SettingsRepo persistence
(single JSON key markedGridStations, org.json only)
- MapState/MapViewModel: markedGrids state, Set/RemoveMarkedStation
actions; marks auto-clear once the grid becomes worked
- MaidenheadGridOverlay: red fill (alpha 90, same as green worked
fill) for marked grids at both zoom levels; first-call label mode
shows the marked callsign in pale yellow on red cells
- MapScreen: unworked-grid dialog gains a 'mark a station you want
to contact' button + callsign input dialog; marked row shows red
callsign, 'Marked' in the count column, marking date, delete icon
- i18n: values/ + values-zh/ (grid_mark_*)
Tapping the grid-QSO dialog "Match" button now starts the pass query on
the map page and only navigates to the match page once it finishes, so the
page's first frame already contains the results — the time-range card at
the top and the pass curves right below, with no scroll flicker. The dialog
button shows a spinner and is disabled while calculating. A uiState
collector in MainScreen performs the actual navigation (still a push, so
the back gesture returns to the map). Failures/exceptions still navigate so
the user is never stuck on the map page.
queryMutualPasses() reset listScrollIndex to 0 on every query, so tapping
"query" on the match page jumped the list back to the top. Now the default
keeps the current position; the stale results stay on screen while the new
query runs (clearing them shrank the list below a screen and clamped the
position away) and are replaced when the new results arrive. New VM test
covers position + stale-results behaviour through a settled re-query.
On a device the first frame of the match page only contains the station
cards and the time-range card, which can be shorter than the viewport —
scrollToItem(1) then has no scroll range and silently does nothing, and
by the time the async query results grow the list past one screen the
one-shot flag is already consumed. Retry every 100ms (up to 2s) until
firstVisibleItemIndex reaches the time-range card. Verified with a new
Robolectric test that grows the content after a delay. Bump 12.6.
Temporary Log.d probes at prefill, list-layout gate, scroll effect entry,
scrollToItem outcome and consume, to locate why the map Match prefill
scroll does not land on the device (Robolectric covers it). Bump 12.5.
Run the scroll tests at 411x891dp portrait (device-like layout) and add
a NavDisplay entryProvider scenario that composes MutualScreen the way
the app does, so the prefill scroll is exercised end-to-end.
On a device the Mutual entry is composed inside the NavDisplay transition;
LaunchedEffect could run before the LazyColumn is measurable and the
scrollToItem then never lands, leaving the page at the top. Only scroll
once onGloballyPositioned confirms the list has actually been laid out.
The grid-QSO dialog Match button now adds Mutual on top of the Map entry
instead of clearing the stack, so the system back gesture pops back to
the map page (the page the user came from). Bottom-nav tab switches keep
their replace semantics.
MutualMatchPrefillScrollTest verifies the map grid-QSO Match prefill:
time-range card lands at the top after scrollToItem, default open stays
at station inputs, and the query auto-starts with grid+24h pre-filled.
Adds Robolectric + compose ui-test-junit4 to the mutual test classpath.
Compose ui 1.11.x (BOM 2026.06.01) ignores the LazyListState
firstVisibleItemIndex constructor parameter - the list always starts at
item 0. Restore the scrollToItem approach (verified effective by
Robolectric Compose UI tests) and keep the auto-started query.
- Match prefill now positions the LazyColumn via its initial state
(queryMutualPasses(initialScrollIndex = 1)) instead of a post-layout
scrollToItem, so the page reliably opens with the time-range card at the
top while the 24h query runs.
- The install-time what's new dialog text now scrolls inside a bounded box
(max 360dp) instead of overflowing the fixed dialog frame.
- Grid-QSO dialog gains a Match action (radio-tower icon + label) in its
header; tapping it navigates to the match page pre-filled with the tapped
grid as the opposite station and a 24h time range, auto-starts the query,
and scrolls the page straight to the time-range card.
- Every grid is now tappable (previously only worked grids opened the
dialog); unworked grids show an empty QSO list instead of a hint.
The 11.2 gate only relaxed the pre-return zoom check, but the showLabels
gate below it still required LABEL_ZOOM_SUB (6.5), so first-call labels
stayed hidden in the 6.0..6.5 band where the 4-char grid lines render.
- Gate showLabels on GRID_ZOOM_SUB (6.0) when showFirstCallLabels is on.
- Relax MIN_LABEL_CELL_PX (48) to 0.6x for first-call mode so 1° cells
(~45 px on the equator at zoom 6.0) are not filtered out.
Normal grid-code labels are unchanged (still LABEL_ZOOM_SUB).
- pass_whatsnew_message rewritten as a single line with escaped \n bullets:
real newlines in the value are invalid for aapt2 ('not a valid string').
- Escape the apostrophe in "map's"; mark grid_qso_calls_count
formatted=false (two %d + '·' triggered multiple-substitutions).
- values-si: remove stray comma after map_altitude </string>.
All were latent resource errors surfaced by the forced recompile.
- Pass history (Iu-yang1 fork): PassesSettings.hoursBefore look-back window;
second 'Time before' slider in the pass filter; completed passes stay
listed (progress clamped to [0,1]) in the history window.
- Compass calibration (Iu-yang1 fork): OtherSettings.compassOffsetDegrees,
CompassAccuracy from the magnetic sensor, calibration dialog with
figure-eight accuracy meter, corrected heading and manual offset field;
RadarViewModel applies the offset.
- What's New dialog now shows the cumulative 4.4.6->4.4.7 changelog and
follows the system language (values/ + values-zh/).
- pass_filter_history_hours localized in es/ru/si/tr/uk.
Move the first-call label gate from LABEL_ZOOM_SUB (6.5) to GRID_ZOOM_SUB
(6.0): worked (green) cells are labelled with their first callsign as soon
as the 4-char grid lines render, not only at the name-label zoom.
Add a 'first call' pill toggle below the grid-mode switch (map top-right,
visible only in grid mode + VUCC award). When on, worked (green) cells are
labelled with the first callsign worked in that grid (earliest confirmed
QSO by time) instead of the Maidenhead code; non-worked cells get no label
at all. Persisted via OtherSettings.stateOfMapFirstCall (full MVI chain).
The feature is VUCC-only: All and other award overlays never draw it.
Day-cell numbers now count the most recent consecutive reports sharing the
newest status (counting back from the newest report, stopping at the first
different status), instead of the total report count of the day. Empty gaps
do not break the streak; statuses compare by exact report text; counting
does not cross day boundaries.
- SatDay gains streakCount (computed in AmSatRepository.buildStatuses)
- DayCell renders day.streakCount instead of slot count sum
- Add real org.json to core:data unit tests (android.jar stub cannot parse)
- Add 2 streak tests: mixed-status slot breaks, all-same-status counts all
The satellite search only matched exact continuous substrings, so names with
dashes, spaces or brackets (AO-7, ISS (ZARYA), FO-29) could not be found by
queries without those separators. Now the query is normalized (lowercased,
non-alphanumeric separators stripped) and split into space-separated tokens,
and every token must appear in the normalized satellite name. This makes
'ao7' match 'AO-7 (AMSAT-OSCAR 7)' and 'iss zarya' match 'ISS (ZARYA)'.
Numeric queries still match catnum exactly. Covered by 8 new unit tests.
The mutual pass list LazyColumn reset to the top on every return to the page.
Scroll position is now stored in the Activity-scoped MutualViewModel and
restored on re-entry; a new query (queryGeneration bump) rebuilds the list at
the top so stale positions never leak into fresh results.
The osmdroid map was recreated on every return to the Map page, resetting
center/zoom to the default and forcing grid mode back onto the station grid.
Now the viewport (center + zoom) is saved in the Activity-scoped
MapFilterViewModel only in grid mode and restored on re-entry, so the user
keeps exactly where they left off. Satellite mode keeps its original
follow-the-satellite behavior; cold-start entries and in-page grid toggles
still center on the station grid as before.
The award chip (VUCC/DXCC/WAJA/...) now lives in an Activity-scoped
MapFilterViewModel instead of a per-composition remember, so switching
to another page and back keeps the last selection; it resets to VUCC
only once per process (cold start). Removes the effect that forced
VUCC on every grid-mode entry.
The stored sync cursor was 'yyyyMMdd', which LoTW's qso_qslsince silently
ignores (measured live: it falls back to the system-supplied default).
Write a full UTC 'yyyy-MM-dd HH:mm:ss' cursor instead — the documented
format LoTW honors — so incremental pulls are exact (second precision)
and the settings page can show the last successful sync time on the LoTW
card, formatted like the ephemeris update time (prefs_updated_title).
Legacy yyyyMMdd cursors are normalized for the API, the gate and the
display (lotwCursorApi/lotwCursorDate/lotwCursorEpochMs).
Mirrors the ephemeris auto-update check: on every app start, if LoTW
credentials are configured, the new 'auto-sync LoTW grids' toggle is on,
at least one manual sync was done before, and today has not synced yet
(ARRL rate limit, ~once a day), pull confirmations since the last sync
and merge them. Same callsign -> incremental merge; callsign change ->
full replace. Manual dialog and automatic path share the orchestration
(mode resolution, gate, merge/persist) moved to core:domain
LoTWGridSync.kt; LoTWSyncMode moved to core:domain. 8 unit tests.