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.
- Cancel: the dialog's cancel/back while syncing now aborts the download job
(cooperative cancellation in the stream reader, CancellationException
propagated, nothing written to storage). An accidental full sync can be
stopped immediately.
- Progress: the size estimate is conservative (900 B/record vs measured 720)
so the bar never hits 100% before the download truly ends; storage writes
moved to the IO dispatcher (multi-MB per-grid QSO JSON used to freeze the
main thread after download); when the bar caps, the UI switches to
'Processing downloaded data…' instead of looking stuck.
- Removed the outdated '3-10 seconds' hint (misleading for large accounts).
Stripe spacing now scales as 44px * 2^(zoom-7) instead of a fixed 44px on
screen: at the map's max zoom (7.0) the stripes keep the established look,
and at lower zooms they shrink with the map, so stripes stay pinned to the
map (same stripes-per-cell at every level) — denser on screen when zoomed
out, sparser when zoomed in.
- SharedDialog title gets start padding when only a cancel button is shown
(was flush against it); dialogs with both buttons unchanged.
- LoTW dialog: full-sync button bottom-left, incremental-merge bottom-right
(SpaceBetween instead of side-by-side).
- Pass card header: radar icon button enlarged to 48dp touch target with a
24dp icon, spanning both header rows vertically (name/time + elevations);
the pass info column narrows so the Opposite elevation shifts left.
- RadarDestination no longer auto-returns instantly when the mutual pass has
already ended — the auto-return now only runs while the pass is actually in
progress. Previously the collapsed-card radar shortcut appeared broken
('flash back to Mutual') whenever the computed pass had finished.
- Fakes: add the 4 last-sync bookkeeping members missed by the LoTW commit.
Icon-only radar button at the right end of the collapsed pass header (always
visible, not only when expanded); the pass info shifts left. The button's tap
is consumed and does not toggle the card expansion.
Two explicit sync buttons in the LoTW dialog:
- Full sync: qso_qslsince=2000-01-01, fresh report replaces all stored data.
- Incremental merge: since=last successful sync date, merged into the stored
set (grids union, QSOs deduped by call + time). First sync / callsign change
automatically falls back to full replace so no stale grids from another
account linger.
Progress & estimates:
- Report body is now streamed; the header's <APP_LoTW_NUMREC> gives the real
QSL count, total size is estimated as NUMREC * 720 B (measured), and the UI
shows a determinate bar with 'N QSOs, ~X s remaining' once known.
- Pre-sync hint shows the measured rate (~8 s per 100 QSOs).
- readTimeout raised to 600 s (stall timeout, not a total cap — huge accounts
legitimately take 30+ min at ARRL's ~10 KB/s stream rate); connectTimeout
raised to 60 s (a busy server took 36.5 s to accept a connection).
Also: ISettingsRepo gains last-sync date/callsign bookkeeping, and the mutual
test fake implements the new members.
DayCell displayed the count of the newest non-gray slot only; it now sums the
counts of all 12 slots of the day. The tile color still reflects the newest
reported status.
Grid mode is toggled from the map page pill (MapAction.ToggleGridMode writes
stateOfMapGrid); the settings-page switch was a duplicate entry point. Removes
the SwitchRow, the ToggleMapGrid action and its ViewModel branch, and trims
the OtherCard height by one row.
The expanded option rows used fillMaxWidth inside a full-screen Box, which
stretched the selector's dark surface across the whole map. The Column now
sizes to its widest child (IntrinsicSize.Max) so the box is just wide enough
for the label, count and expand arrow.
The pass card time range and the elevation-curve X-axis labels were hardcoded
to the device timezone. MutualUiState now carries isUtc, initialized and kept
in sync with OtherSettings.stateOfUtc (same pattern as PassesViewModel), and
both the card timeFormat and the chart axis use UTC when the setting is on.
Also fixes pre-existing test-fake compile breakage: FakeSettingsRepo built
OtherSettings with positional args missing shouldSeeWhatsNew, and never
implemented the wavelogSettings/lotwSettings/worked-grids members added by
later interface extensions.
Root cause: LocalElevationThresholds was provided inside MainScreen, but
RadarDestination is a root-level sibling entry in NavRoot's NavDisplay, so
it fell outside the CompositionLocal scope and always used the default
15/45 thresholds.
Fix: hoist the CompositionLocalProvider to NavRoot so both the tab content
and the root-level RadarDestination read the user's low/high elevation
thresholds. Also affects upstream (same navigation structure).
WAZ/DXCC award rings are stored with the closing edge implicit (the renderer
close()s single-segment rings). Rings that straddle ±180° are split into
multiple segments, and multi-segment rings must not be close()d (each close()
would draw a spurious chord across the map), so the implicit closing edge
was never drawn -> the shared boundary between WAZ zones 31/32 (165E,-8 ->
180,-12), and the closing edges of Russia's main ring / Fiji / Russia's
Chukotka tip in DXCC, stayed open.
Fix: store every ring explicitly closed (append first point). splitRingAt
Antimeridian then cuts the closing edge at ±180 exactly like any other edge,
so each segment ends on the correct side and the boundary lines render.
Android plurals have no 'one' category in Chinese (zh plural rules only
contain 'other'), so a single QSO rendered as '1 QSOs' on Chinese-locale
devices. Replace the plurals resource with explicit singular/plural
strings selected in code.
Switching the operated-grid selector now re-filters the green worked
cells on the map (the overlay receives the per-grid worked set instead
of the global one, on every recomposition via setGridMode). Previously
only the VUCC progress count changed while the map stayed green for all
operated grids.
The selector gains an 'All (n)' entry at the bottom of the list: it
selects no operated grid (null), showing every worked grid combined and
restoring the global VUCC count.
Maidenhead row R (80-90N) and row A (-90..-80S) vertical lines were drawn
with projectionToY(±90). Web Mercator diverges there: osmdroid's
TileSystemWebMercator yields y01=±Infinity -> Long.MIN/MAX_VALUE pixel
coordinates (verified via JVM test: lat=90 -> -9.2e18, lat=-90 -> +9.2e18).
The canvas cannot rasterize such coordinates, so the polar rows' grid
lines silently disappeared at field zoom (2-char labels).
Fix: clamp the projected latitude to the Mercator limit ±85.0511 in
projectionToY(). The polar rows then project onto the screen edge and
their lines render (the sliver beyond 85.05 is off-screen anyway). Row A
kept its symmetric handling; row R now uses the same path.
- QSO dialog: each callsign row shows 'n QSO'/'n QSOs' (plurals) centered
between callsign and date, styled like the date.
- Grid mode + VUCC filter: top-left selector picks the operated grid the
VUCC count is computed from (defaults to the most-worked grid), with a
collapsible list of all operated grids; arrow style matches the QSO
dialog rows.
GridQso gains myGrid (ADIF MY_GRIDSQUARE) captured per QSO during LoTW
parsing (handles alphabetical field order + EOR reset) and persisted as
JSON key 'mg' (absent in pre-myGrid data -> null). Legacy syncs keep the
global VUCC count; one resync unlocks per-grid counting.
Port of upstream PR #252 (wty2019wty): combine otherSettings.stateOfUtc
into initRepository so an existing AOS time window is reapplied in the
newly selected timezone right after toggling UTC, instead of waiting for
the next selection/position change.
- DB v2 migration: entries.ndot column so hasDecayed() actually works after
reload (parsed ndot was dropped on write, making the decay detection dead
code); radios.isCustom column to mark manually imported transceivers.
- updateTransceiversFromFile marks imported radios isCustom=true; remote
updates now deleteManagedRadios() then insert the fresh snapshot, keeping
manual imports (no source can refresh them, so nothing would bring them back).
- Wire MIGRATION_1_2 into MainContainer (fallbackToDestructiveMigration=false
would otherwise crash on upgrade).
- PassesViewModel: use the DEVICE timezone for the natural-day anchor in
findTodaySunRiseSet instead of the display timezone. With UTC enabled the
anchor was UTC midnight = 08:00 local for UTC+8 stations, when the sun is
already up — tripping the white-night guard and rendering '--:--'.
- SatelliteRepo.isAosInRange: follow the UTC display toggle so the pass time
window is interpreted in UTC when UTC mode is on. Previously the list
showed UTC times while the filter silently used local time, off by the
timezone offset (8h for China).
- wapc.json: add name_en for all 34 provinces (Beijing/Guangdong/...)
- waja.json: add name_en for all 47 prefectures (Gifu/Kyoto/...)
- AwardBoundaryOverlay: render English label on non-Chinese UI,
fall back to source name when name_en is absent
- bump version to 4.4.6-ba7opf.10.4 (469)