Compare commits

..
Author SHA1 Message Date
atsunatsu 5bf4546ee3 chore: bump version to 4.4.6-ba7opf.9.18 2026-09-10 01:36:18 +08:00
atsunatsu 030a6eecc9 fix(map): split antimeridian edges instead of dropping — closes WAZ zone 1/19/31/32 outlines
splitRingAtAntimeridian used to drop any edge crossing ±180°, leaving
visible gaps in WAZ zone outlines (zone 31's 40°N Pacific boundary,
zone 1/19's polar arcs, zone 32's Antarctic arc). The edge is now split
in two at ±180° with interpolated latitude, each half kept in its own
segment, so outlines close across the map's left/right edges.
normalizeLon now maps to (-180, 180] so edges ENDING on the
antimeridian are not mistaken for crossings.
2026-09-10 01:05:25 +08:00
atsunatsu 42174da0be fix(map): chip order — VUCC first, DXCC second
chore: bump version to 4.4.6-ba7opf.9.17
2026-09-09 09:20:51 +08:00
atsunatsu 06421f8cfc fix(map): eliminate spurious pole-to-pole lines in award boundary overlay
- projectionToY clamps latitude to Web Mercator max (±85.05) so raw -90
  polar vertices (Antarctica ring, WAZ zones 29/30/32/39) no longer project
  to infinity, removing vertical pole spikes
- traceSegment unwraps longitude deltas per-vertex (continuous with previous
  point) instead of folding each vertex independently — a full-circle ring
  (Antarctica, span ~358°) no longer jumps ±180° mid-ring to draw a line
  across the whole map
- closing chord now folds to the nearest world copy (short-side seal at the
  antimeridian) instead of a full-width chord
- AwardCalculator: DXCC chip now listed before VUCC
2026-09-09 08:49:37 +08:00
atsunatsu 9ea5b533c1 chore: bump version to 4.4.6-ba7opf.9.16 2026-09-09 02:58:09 +08:00
atsunatsu b6a9331ba3 refactor(map): drop redundant 'All' award chip — VUCC already shows plain worked-grid view 2026-09-09 02:50:31 +08:00
atsunatsu 390fd7717d chore: bump version to 4.4.6-ba7opf.9.15 2026-09-09 02:31:54 +08:00
atsunatsu ef0a34723b fix(map): award boundary fixes — HK/MO labels, MO DXCC entity 152, antimeridian ring closure
- wapc/dxcc assets: add Hong Kong & Macau labels (forceLabel) and MO boundary in dxcc
- AwardBoundaryOverlay: rings split at antimeridian no longer close() per segment — removes spurious Russia cross-continent chord
- AwardCalculator: Macau DXCC entity 330 -> 152 (+test)
- Settings LoTW dialog: add sync duration hint (zh/en)
2026-09-09 02:30:54 +08:00
atsunatsu 36d091a7e1 chore: bump version to 4.4.6-ba7opf.9.14 2026-09-08 16:44:11 +08:00
atsunatsu 20143cf096 fix(lotw): strip STATE ' // Name' suffix for award stats
LoTW returns STATE as 'CODE // Name' (verified with a real report 2026-09),
e.g. 'GD // Guangdong' (China), '34 // Tottori-ken' (Japan), 'CA // California'
(US). The award calculator matched on the bare CODE, so every mainland China
province (WAPC), all Japan prefectures (WAJA) and US states (WAS) failed —
WAPC only showed HK/MO/TW (entity branch, no STATE needed) and WAJA was empty.

Normalize STATE once at parse time: strip the ' // Name' suffix so the stored
GridQso.state is the clean code consumed by persistence and AwardCalculator.
Add a test covering China/Japan/US formats.
2026-09-08 11:22:24 +08:00
atsunatsu a64f9c80b9 fix(map): split award boundary rings at antimeridian + anchor projection
DXCC (Russia/Fiji/Antarctica) and WAZ (Pacific CQ zones 1/19/31/32) polygon
rings cross ±180. The old overlay normalized every vertex to [-180,180)
independently, folding far-side vertices onto the opposite screen edge and
drawing a spurious line sweeping across the whole map (esp. at low zoom:
DXCC horizontal/vertical lines, WAZ extra long edges).

Split each ring into contiguous segments at the antimeridian, then project
each segment anchored on its first vertex with continuous longitude offsets
(no per-vertex fold). Ring geometry stays intact while panning/zooming.
2026-09-08 11:22:16 +08:00
atsunatsu db0320f521 chore: bump version to 4.4.6-ba7opf.9.13 2026-09-08 08:47:56 +08:00
atsunatsu adc9ac008b fix(map): restore grid lines between worked cells
The .9.11 skip logic removed every line segment bordering a worked
gridsquare, which also erased the shared edges between adjacent worked
cells — adjacent greens merged into one unreadable patch. Revert to
drawing the full Maidenhead grid unconditionally; the green fills stay
on top exactly as before.
2026-09-08 08:26:44 +08:00
atsunatsu 4a33d1b19a feat(map): award filters with boundary overlay in grid mode
- Grid-mode top bar replaced with award filter chips (All/DXCC/VUCC/
  WAPC/WAJA/WAZ/WAS) showing live progress (count/target)
- New AwardBoundaryOverlay renders real administrative boundaries
  (provinces/prefectures/CQ zones/states/countries) with the VUCC visual
  language: yellow boundary lines, green fill for worked regions, labels
- Boundary assets packaged from public GeoJSON (DataV China, Japan land,
  HB9HIL CQ zones, Natural Earth states/countries) with ADIF match keys:
  WAPC province codes, WAJA ADIF prefecture numbers, US state abbrs,
  ARRL DXCC entity codes
- GridQso extended with dxcc/country/cqz/state parsed from LoTW report
  (qso_qsldetail); AwardCalculator derives six-award progress; STATE is
  interpreted per DXCC entity (ADIF 'STATE depends on DXCC')
- Grid-mode toggle pill floated at map top-right (Grid mode / Satellite mode)
- New tests: AwardCalculatorTest (9 cases), LoTW award-field parsing (3 cases)
2026-09-08 08:22:12 +08:00
atsunatsu cce9209b3d chore: bump version to 4.4.6-ba7opf.9.12 2026-09-08 06:14:34 +08:00
atsunatsu 237e4cb27a fix(update): APK asset URL used stale '-release' filename — downloads 404'd
Since v4.4.6-ba7opf.9.9 releases are uploaded as Look4Sat-<version>.apk (no
'-release' suffix), but parseRelease still built the download URL as
Look4Sat-<version>-release.apk. Update check succeeded (page parses) while the
download 404'd ('下载失败请重试'). Align the generated URL with the actual
asset name; mirror-prefixed URLs fixed too.
2026-09-08 06:09:42 +08:00
atsunatsu cd3eded77c chore: bump version to 4.4.6-ba7opf.9.11 2026-09-08 06:03:35 +08:00
atsunatsu 7d7f8f1eda feat(map): expanded QSO list — drop ordinal prefix, add per-QSO date
Expanded per-callsign rows previously read '2 · FO-29 · CW · V/U · 11:30Z'
(ordinal + summary, no date). Now each expanded line is
'yyyy-MM-dd · FO-29 · CW · V/U · 11:30Z' — the first QSO's date already
lives in the header row, so only subsequent QSOs carry a date prefix.
2026-09-08 05:57:27 +08:00
atsunatsu 11b46c6710 feat(map): tap worked cells from GRID_ZOOM_SUB (lines visible), labels from zoom 6.5
- Tap-to-show-QSOs now works as soon as 4-char grid lines appear (zoom 6),
  not only when 4-char labels show (was zoom 7). Selection outline already
  used GRID_ZOOM_SUB, so feedback is immediate.
- 4-char labels now appear from zoom 6.5 (was 7.0), so the sub-square name
  shows shortly after the lines appear instead of a full zoom level later.
2026-09-08 05:54:06 +08:00
atsunatsu da1facac64 fix(map): world-repeat copies no longer repaint in place — alpha stacking made worked cells brighter and opaque
Pixel analysis of the user's screenshot: worked-cell green measured RGB(57,160,74)
against background RGB(16,16,15), which matches alpha≈178 — i.e. the alpha=90
fill painted ~3 times in place. Root cause: at low zoom colRepeats>0 and every
turn loop shifted the cell by dLon=turn*360°, but projectionToX() normalizes
longitude back into [-180,180), so all turns collapsed onto the SAME screen x
and each cell/line/label was painted 2*colRepeats+1 times at one spot.

Fix: keep the turn offset as a pixel shift (turn * worldWidthPx) on the x
coordinate in ALL turn loops (worked fills at both zooms, vertical lines,
horizontal lines, labels). World-repeat copies now land on their true screen
positions instead of stacking on top of each other.
2026-09-08 05:46:08 +08:00
atsunatsu cb1f341ed9 fix(map): field-zoom grid lines no longer cut through worked cells — root cause of 'green too bright'
The previous fixes only handled sub-square zoom lines. At field zoom the 20°x10°
grid lines were drawn unconditionally: cellLabel(zoom<6) returns 2-char fields
which never match the 4-char workedGrids set, so every field line crossed every
worked 2°x1° square. Bright-yellow (alpha 160) lines through the green fill is
what read as 'green got brighter' — the fill color itself never changed.

Now both line passes iterate the finer 2°x1° sub-grid (forced 4-char labels via
GRID_ZOOM_SUB) and skip segments bordering worked cells, at field zoom and
sub-square zoom alike. Revert the alpha softening (c2e496c4) — the fill was
never the problem; also restores worked-cell labels (7afce7ee revert).
2026-09-08 05:28:12 +08:00
atsunatsu c2e496c406 fix(map): soften worked-cell fill at sub-square zoom, restore cell labels
Reverting the label-skip from 7afce7ee: worked cells keep their 4-char
labels. The 'green too bright' report is an area effect, not an overlay
overdraw — the same alpha=90 fill that reads as a small patch at field zoom
covers half the viewport at sub-square zoom. Use a softer fill (alpha 50)
for the sub-square branch, field zoom unchanged.
2026-09-08 05:02:05 +08:00
atsunatsu 7afce7ee68 fix(map): skip labels on worked cells — bright-yellow 4-char text on green fill read as 'green too bright'
The previous fix skipped grid LINES through worked cells, but each worked cell
still got a bright-yellow (alpha 220 + black shadow) 4-char label drawn dead
center on the green fill at zoom>=7. Field zoom never shows this because 2-char
labels sit at field centers and almost never overlap a worked 2x1 square. Now
worked cells stay completely label-free, matching the field-zoom look.
2026-09-08 04:47:43 +08:00
atsunatsu 86048ee1fd fix(lotw): run report fetch on Dispatchers.IO — main-thread blocking caused ANR/crash
fetchReportBody is a synchronous HttpURLConnection call (up to 30s connect +
120s read) but was invoked directly from a Main-dispatcher viewModelScope
coroutine: the whole UI froze for the duration of the request, then Android
killed the app with an ANR dialog. Wrap both fetch methods in
withContext(Dispatchers.IO). Bug present since the endpoint was introduced.
2026-09-08 04:32:21 +08:00
atsunatsu 42a6ffa4c9 chore: bump version to 4.4.6-ba7opf.9.9 2026-09-08 04:17:52 +08:00
atsunatsu cf1d8e6539 LoTW sync: distinguish failure causes (bad credentials / rate limit / timeout / network)
Rate limit classification per ARRL policy (one download in progress per user
id; HTTP 503 'Page request limit' observed on throttled pulls) and the HTML
login-error page returned for wrong credentials. Settings screen now shows a
specific message per cause instead of one generic 'check callsign/password/network'.
2026-09-08 04:07:03 +08:00
atsunatsu ab8b072f1f fix(map): worked cells at sub-square zoom stay line-free — grid lines skip worked segments
Lines (alpha 160 yellow) were drawn AFTER the worked fills (alpha 90 green),
so at zoom>=6 every worked cell's four borders got yellow-on-green
composited — the cells looked brighter than the same cells at field zoom
(no square lines there). Segments bordering a worked cell are now skipped
in both meridian and parallel passes.
2026-09-08 03:44:21 +08:00
atsunatsu 1d4036e3d0 fix(map): vertical grid lines vanish at global views — bbox now anchored on view center, not left/right edge pixels
The antimeridian unwrap (span > 180 => shift left edge +360) misfired on
every global view (span is legitimately > 180): the range inverted and the
meridian loop drew nothing. Anchor the bbox on the view-center longitude
plus half viewport width in degrees instead; continuous unwrapped range
handles both antimeridian crossing and world repeats.
2026-09-08 03:25:12 +08:00
atsunatsu ab22244f86 fix(map): grid-mode field center offset by -90/-180 base
Maidenhead field index 0 spans -90..-80 lat / -180..-160 lon, so the
field-center must subtract those bases. Without them, entering grid mode
centered a hemisphere away (OL62 -> 115N/290E instead of 25N/110E).
2026-09-08 03:19:40 +08:00
atsunatsu 3b0874d707 chore: bump version to 4.4.6-ba7opf.9.8 2026-09-08 02:56:16 +08:00
atsunatsu 2c5b1479f1 test(lotw): VUCC up to 4 grids in one contact; clarify comment 2026-09-08 02:51:24 +08:00
atsunatsu 39d4498a54 feat(map): tap worked grid at zoom>=7 to show per-grid satellite QSO dialog
- LoTW sync now stores per-QSO detail (call/time/sat/mode/bands) grouped by
  gridsquare (PROP_MODE=SAT only), single JSON blob in SharedPreferences
- GridQso model derives band label (V/U, U/V, V/A...) from BAND_RX/BAND
- Tap detection via osmdroid MapEventsOverlay; selected grid outlined green
- Centered dialog: calls sorted by first-contact time, first-QSO summary,
  expandable call rows list subsequent QSOs in that grid; tap outside closes
2026-09-08 02:47:54 +08:00
atsunatsu 9ed2aebc9d fix(lotw): only collect satellite QSO grids (PROP_MODE=SAT); keep both grids of VUCC_GRIDS pairs 2026-09-08 02:09:25 +08:00
atsunatsu 72d5e13acf tune(map): 4-char sub-square labels at zoom>=7; zoom 6 shows grid lines only 2026-09-08 01:55:40 +08:00
atsunatsu 736dc23aae chore(map): drop unused FIELD_ZOOM_LABELS constant 2026-09-08 01:52:01 +08:00
atsunatsu 052ba79c94 fix(map): field labels visible at all zooms (pixel-size gated), not gated by FIELD_ZOOM_LABELS 2026-09-08 01:51:14 +08:00
atsunatsu cb648caf65 tune(map): raise thresholds — sub-square grid lines at zoom>=6 (was 5), field labels at zoom>=4 (was 3) 2026-09-08 01:48:24 +08:00
atsunatsu 05170541ed fix(map): grid mode centering via view.post (first-layout reset); antimeridian unwrap of visible bbox fixes Pacific-centered vanishing grid; tile meridians/fills across world repeats at low zoom 2026-09-08 01:39:21 +08:00
atsunatsu e75d2dc410 chore: bump version to 4.4.6-ba7opf.9.7 2026-09-08 00:27:17 +08:00
atsunatsu d55a07bc26 feat(map): fill individual worked 2x1 degree squares green at field zoom without square grid lines 2026-09-08 00:14:05 +08:00
atsunatsu c372386a77 fix(data): LoTW sync resilience — longer timeouts (30s/120s), gzip transfer, follow redirects; ARRL server-side latency is the bottleneck 2026-09-08 00:05:22 +08:00
atsunatsu de6ade025c feat(settings): LoTW dialog — hint rewritten without Wavelog, confirm button is now Sync, remove separate sync button 2026-09-07 23:29:19 +08:00
atsunatsu 2eb6d6cd5b fix(map): stop satellite tracking animation in grid mode so it stays centered on the local grid 2026-09-07 22:54:19 +08:00
atsunatsu c09f718aae feat: mutual chip uses theme primary yellow, plain text distance; top bar text indent; low-zoom meridians via direct Mercator math (bypass osmdroid wrap bug); remove Wavelog sync entry — LoTW is the sole grid source (replace instead of merge) 2026-09-07 19:10:44 +08:00
atsunatsu a9fe0b74e9 feat: mutual match screen target-grid chip (needle bearing + grid + distance); grid mode centering no longer swallowed when station position is not yet available 2026-09-07 17:54:14 +08:00
atsunatsu 2c9a5b376f feat: release-notes body in update checker; grid mode centers on station grid on every entry; own grid square drawn with thick outline; low-zoom meridian vanishing while panning fixed 2026-09-07 15:06:33 +08:00
atsunatsu 422e480b2f release: v4.4.6-ba7opf.9.3 2026-09-07 14:07:57 +08:00
atsunatsu efd6931abb fix: LoTW sync — force full report with qso_qslsince (LoTW default only returns new confirmations since last query) and accept lowercase <eoh> header 2026-09-07 13:51:41 +08:00
atsunatsu c7ca77a943 fix: sync dialogs use typed values directly (save+sync in one step); LoTW card wording; center map on station grid when entering grid mode; center grid labels in cells; update-check falls back to GitHub accelerator mirrors 2026-09-07 07:22:41 +08:00
atsunatsu be6473e692 feat: LoTW confirmed-grids sync card — fetch confirmed QSO grids directly from ARRL LoTW (lotwreport.adi) and merge with Wavelog grids; credentials stored locally 2026-09-07 06:55:42 +08:00
atsunatsu 75d162301b fix: meridians vanish at low zoom (osmdroid clips Mercator X; normalize longitude to view center); hide satellite layer and bottom info card in grid mode 2026-09-07 06:28:58 +08:00
atsunatsu 07c09ec3e9 fix: VersionComparator parses multi-level suffixes (4.4.6-ba7opf.9.1) by comparing all numeric segments, not just the last one 2026-09-07 06:11:31 +08:00
atsunatsu 513d50e56a chore: bump version to 4.4.6-ba7opf.9.1 2026-09-07 06:01:45 +08:00
atsunatsu 366edd8bbb fix: correct longitude bounds in Maidenhead grid overlay (left/right were swapped, so meridians and worked-grid fills never drew) 2026-09-07 06:01:10 +08:00
atsunatsu 13df0dfcec fix: Wavelog sync via v1 API logbook_get_worked_grids (3.0.2 has no API v2) 2026-09-07 02:11:40 +08:00
atsunatsu eb1a5d51a2 chore: bump version to 4.4.6-ba7opf.9 2026-09-07 02:02:36 +08:00
atsunatsu 4e3547649f feat: Maidenhead grid mode with worked-grid highlights from Wavelog (API v2 grid=all) 2026-09-07 01:42:42 +08:00
atsunatsu 7abbeb6eb3 fix: update check via GitHub web endpoint, avoiding API 60 req/hour rate limit 2026-09-06 21:33:41 +08:00
atsunatsu 5c73d5ed7a feat: default TLE source order with LAPAN-A2 (Kaggle) first, R4UAB second 2026-09-06 20:18:04 +08:00
atsunatsu a663ef7f1b build: read release signing credentials from local.properties/env instead of hardcoding
Passwords for the release keystore were committed in plaintext to a
public repo. Move them to gitignored local.properties with an
environment-variable fallback. Keystore file itself stays in $HOME.
2026-09-05 07:57:33 +08:00
atsunatsu 1f1e1fb144 fix: keep update install action visible 2026-09-01 11:03:47 +08:00
atsunatsu 5166daa297 fix: localize AMSAT date labels in Chinese 2026-09-01 10:18:56 +08:00
atsunatsu 604c2ec59d feat: check for updates page in settings
Add a full-screen update checker accessible from a button at the
bottom of the settings page. It queries the fork's GitHub latest
release and can download + install the APK in place.

- UpdateRepository (core/data): fetch releases/latest via GitHub API,
  parse tag/description/APK asset, stream APK download to cacheDir
- VersionComparator (core/domain): compare 4.4.6-ba7opf.N style tags
  (base version first, then build number; v-prefix tolerant) + tests
- IMainContainer/MainContainer: inject updateRepo
- UpdateCheckerScreen (feature/settings): shows current version,
  checks for updates, displays latest version + release notes, and a
  Download & install button; FileProvider hands the APK to the
  system installer
- SettingsScreen: bottom entry button, full-screen page toggle
- AndroidManifest: REQUEST_INSTALL_PACKAGES permission + FileProvider
- i18n strings (en/zh)
2026-08-29 16:25:51 +08:00
atsunatsu 1904a6b3e6 feat: drag reorder handle in data sources dialog
Port the upstream drag-reorder pattern (dragHandle/draggedVisual/
autoScroll/DragRowState) so custom data source rows can be reordered
by long-pressing the handle icon, replacing the old move-up button.

- Add 40dp drag-handle box at row start with long-press gesture
- Live reorder while dragging (neighbour squeeze + spring settle)
- Edge auto-scroll during drag
- Keep per-source enabled checkbox + HTTP status label

This file also carries the #242 data-source dialog UI (enabled
checkbox, status code label, restore-default button) which shares
the same sourceSection function and cannot be cleanly split.
2026-08-29 16:25:09 +08:00
atsunatsu 7f390117b8 feat: data source toggles + HTTP status codes (upstream #242)
Port upstream PR #242 data layer: per-source enabled toggles and HTTP
status codes for custom data sources.

- IRemoteSource: NetworkResult(code, stream) + getNetworkStream
- RemoteSource: return HTTP status code (200 ok / error code / ERR)
- ISettingsRepo: dataSourcesStatus StateFlow<Map<String,Int>>
- SettingsRepo: persist enabled flags (single JSON per key) + status
- DatabaseRepo: filter by enabled toggles, write back status codes
- Settings.kt: DataSourcesSettings + satelliteEnabled/transceiversEnabled
- Sync test fakes (AmSatRepositoryTest, DatabaseRepoTest, Fakes.kt)

Fork-specific features preserved: builtinTypesByUrl reverse lookup,
satnogs migration, map-type data source structure.
2026-08-29 16:21:59 +08:00
atsunatsu 346803fe6e fix: normalize Maidenhead grid input to uppercase in mutual pass
- onStationAGrid/onStationBGrid store the trimmed upper-cased grid instead
  of the raw input (ol63 used to stay lowercase in the UI)
- drop the local latLonToGrid/gridToLatLon copies and reuse the domain
  QthConverter, removing the drift risk between two implementations
- inject computeDispatcher (default Dispatchers.Default, no behaviour
  change) so the pass computation is unit-testable on a test scheduler
- add MutualViewModelTest (15 cases): init prefill, grid/lat-lon linkage,
  error paths, main-list reuse, fallback search, sorting, clearError
2026-08-29 10:55:15 +08:00
atsunatsu 47b87a1604 refactor: consolidate Maidenhead conversion into domain QthConverter (4/6/8-char)
- QthConverter.qthToPosition now accepts 4/6/8-char locators (previously
  6-char only: 4-char returned null, 8-char was silently truncated)
- positionToQth now emits upper-case sub-square letters (IO91VL) so all
  callers render grids consistently
- Extended QthConverterTest: 4/6/8-char parsing, odd-length and out-of-range
  rejection, lower-case input normalisation
2026-08-29 10:54:55 +08:00
atsunatsu 935fe6d99f chore: bump version to 4.4.6-ba7opf.6 2026-08-28 15:39:17 +08:00
atsunatsu 69af1c38bb fix: use real SatNOGS TLE source so newly launched satellites appear
- Point the built-in SatNOGS source at db.satnogs.org/api/tle/?format=3le
  (the CelesTrak satnogs group is a separate, slower subset); rename the
  old CelesTrak source to 'CelesTrak SatNOGS'.
- Add a one-time migration in SettingsRepo: installed apps that already
  persisted a satelliteUrls list containing the old CelesTrak SatNOGS URL
  get the real SatNOGS TLE URL inserted right after it.
- Dedupe imported satellites by catnum and radios by uuid in
  DatabaseRepo.updateFromRemote so multiple sources don't insert the
  same primary key twice.
- Test: importing JAMX-0825b (NORAD 98248) from the real SatNOGS source.
2026-08-28 15:23:23 +08:00
atsunatsu 5c35f2418d chore: bump version to 4.4.6-ba7opf.5 2026-08-28 01:07:26 +08:00
atsunatsu 2aa9858660 feat: open map from radar header
Replace the radar page calendar action with the map icon and route the action through the Passes map flow so the Passes navigation item remains selected.
2026-08-28 00:42:00 +08:00
atsunatsu bc6e065804 feat: prefetch AMSAT status on foreground
Start AMSAT status prefetch when the app enters foreground and share the in-flight request with the status page fetch path to avoid duplicate network requests.
2026-08-28 00:41:32 +08:00
atsunatsu 7924a6f69e feat: cache AMSAT status while app is foreground 2026-08-23 19:27:16 +08:00
atsunatsu 9a2a715d1f chore: bump version to 4.4.6-ba7opf.3 2026-08-22 21:15:41 +08:00
atsunatsu ee32c7a1eb feat: confirm AMSAT report uploads 2026-08-22 20:50:39 +08:00
atsunatsu 1d54ac4718 style: color code AMSAT upload status chips 2026-08-22 19:43:46 +08:00
atsunatsu 433ce85561 feat: upload AMSAT status reports 2026-08-22 19:14:24 +08:00
atsunatsu cebe3b7ec4 refactor: move map access under passes 2026-08-22 18:35:53 +08:00
atsunatsu d8f60cf270 docs: add Mutual Pass and AMSAT status features to README 2026-08-20 14:28:09 +08:00
atsunatsu 4a3332fdb5 chore: remove dist/ from tracking, add to .gitignore 2026-08-19 22:19:29 +08:00
atsunatsu 9e7fdbfe71 fix: passband RX update now uses full Doppler round-trip instead of pure passband mapping
When adjusting RX in passband mode (via +/- buttons or slider), the old
code used TransponderMapper.mapDownlinkToUplink (pure passband mapping
without Doppler compensation), producing ~10kHz error at LEO velocities.

Fix: use DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset
for the full TX-from-RX round-trip, matching the RX→PASSBAND button
onClick logic. Also fixed frame mismatch: position now uses txLow/txRange
(Doppler-shifted frame) consistently.
2026-08-19 22:19:14 +08:00
atsunatsu 463df58eb9 fix: keep calculator page content composed while scrolling 2026-08-12 17:50:10 +08:00
atsunatsu 58b8801c0c fix: preserve calculator frequencies while scrolling 2026-08-11 23:42:06 +08:00
atsunatsu 2581ba92c0 feat: remember per-satellite doppler offset in calculator
Store the offset (kHz) per NORAD catalog id in SharedPreferences so
each satellite's last entered offset is restored when reopening the
frequency calculator.
2026-08-10 15:11:02 +08:00
atsunatsu e5131ebb72 fix: amsat icon position, swap mutual icon to radio-tower
- Adjust ic_satellite_alt translateY from -226.13 to -230.5 so icon
  is centered vertically (not clipped at bottom)
- Replace ic_match with ic_radio_tower (MDI) for Mutual page
2026-08-09 01:53:16 +08:00
atsunatsu f9accdd517 feat: add AMSAT status page to bottom navigation bar
- Add ic_satellite_alt.xml icon (from upstream author rt-bishop)
- Add Screen.AMSAT to Navigation.kt
- Add AMSAT entry in MainScreen.kt nav bar + NavDisplay entry
- Wire up feature:status module dependency in ApplicationPlugin
- Chinese/English nav_amsat strings already exist from earlier port
2026-08-09 01:03:30 +08:00
atsunatsu ee3c1e7058 feat: add AMSAT satellite status tracking page (#234)
Port upstream PR #234 to fork main:
- New feature:status module (SatStatusScreen, SatStatusViewModel)
- New AmSatRepository + IAmSatRepository + SatStatus models
- RemoteSource: AMSAT API methods (getAmSatCatalog, getAmSatReports)
- MainContainer/IMainContainer: DI injection
- MainTheme: AMSAT status colors (tertiary slots)
- Chinese translations added for all AMSAT strings
- 26 unit tests passing

Note: navigation entry not included — upstream PR #234 intentionally
left it out pending UI restructuring. Add AMSAT to your nav bar or
menu as desired.
2026-08-08 22:13:56 +08:00
atsunatsu a08a28df39 fix: recalculate passes when station position changes
Use combine(selectedIds, stationPosition) in SatelliteRepo.initRepository
so that passes are recalculated on either satellite selection change or
station position change. StateFlow already emits distinct values.
2026-08-08 20:30:08 +08:00
atsunatsu 759a6de2c6 chore: replace workflow badge with release version badge 2026-08-08 15:58:11 +08:00
atsunatsu ba5a984c16 fix: broaden linear transponder matching and deduplicate same-range entries
- isNamedLinearTransponder: match ' Lin' prefix (AO-7 style) and
  add fallback branch (hasLinearName && hasLinearMode) for entries
  with 'Linear'/'Lin' but no 'transponder' word (AO-7, AO-73)
- add deduplicateTransponders: merge entries sharing the same
  uplink/downlink frequency range, preferring non-CW entry
  (AO-7 Mode A had separate SSB and CW entries; JO-97 had
  separate CW and SSB Transponder entries)
- apply dedup in CalculatorPage filter chain
- 6 new tests, 23 total, all passing
2026-08-08 15:42:33 +08:00
atsunatsu 083b8a2847 Merge pull request #5 from MCKero6423/feat/thanks-bg7nta
Add BG7NTA (CW decoding feature) to the thanks list
2026-08-08 12:44:05 +08:00
mckero e8ebfb1ef3 feat(credits): add BG7NTA to the thanks list
Add BG7NTA (CW decoding feature) to the in-app credits/thanks section
across all three locale files (en, zh, tr).
2026-08-08 03:52:24 +00:00
atsunatsu 12eed1d9a9 i18n: use string resources for mutual match page (zh/en) 2026-08-07 21:27:01 +08:00
atsunatsu b3859fbece i18n: translate '友台' to 'opposite' for clarity 2026-08-07 20:06:43 +08:00
atsunatsu f40a67e1b7 revert: remove fldigi CW decoder migration, keep Morse Expert 2026-08-07 20:01:53 +08:00
atsunatsu 16dce5df2a chore: remove .cxx build artifacts from git tracking 2026-08-07 19:59:03 +08:00
atsunatsu 80a58bed61 i18n: localize mutual match page to English 2026-08-07 19:53:36 +08:00
atsunatsu a8afc63bba fix: default CW decoder to ham radio QSO mode 2026-08-07 18:43:01 +08:00
atsunatsu dbf9a2a65f fix: native decoder crash on 64-bit devices; restore calculator layout 2026-08-07 11:25:42 +08:00
atsunatsu ddecaa6017 docs: update README for BA7OPF fork features 2026-08-06 13:19:32 +08:00
atsunatsu 418a05e343 fix(i18n): update Chinese station position wording 2026-08-04 21:03:35 +08:00
atsunatsu c80e351212 fix: correct linear calculator offset mapping 2026-08-04 18:41:53 +08:00
atsunatsu a333192df5 feat: add CW decoder and linear calculator page 2026-08-04 17:49:28 +08:00
atsunatsu a7b41c95c7 fix: improve match header and daily sun times
Match page UI:
- Always show a meaningful status line in the top bar instead of leaving the second row blank on first entry.
- Add a compact status chip for waiting, calculating, result, no-match, and error states.
- Remove the duplicate intro card so the first screen starts directly with station inputs.

Pass list sun times:
- Compute sunrise/sunset from each visible date group's 00:00 in the selected timezone.
- Avoid using an arbitrary pass AOS as the rise/set search start, which could jump later-day headers to the following day's events.
2026-08-03 21:38:57 +08:00
atsunatsu 2298d8eea4 feat: add mutual radar overlay arrows 2026-08-03 17:27:48 +08:00
atsunatsu 1813e1bfa4 fix: refine mutual pass matching and radar overlay 2026-08-03 13:04:42 +08:00
atsunatsu bd09ad22ff chore: remove .hermes from git tracking, add to gitignore 2026-08-03 08:55:04 +08:00
atsunatsu 7e90071c4d fix: revert radar pager to Transceivers tab, default minElev to 0
- Radar pager now defaults to Transceivers tab (initialPage=0)
- Transceivers don't auto-expand (selectedUuid defaults to null)
- Mutual page default minElev changed from settings value to 0.0
- Display filters out portions below the minElev threshold
2026-08-03 03:58:45 +08:00
atsunatsu d1cc944be2 feat: filter mutual page curves to minElev window (satlover.de style)
Elevation curve chart and mutual radar plot now only show the portion
where both stations are above their respective minimum elevation.
The curves are filtered at display time, the pass search still uses
the 0° horizon boundary for consistency with the Passes page.
2026-08-03 03:47:25 +08:00
atsunatsu fb28283679 feat: default pager to SSTV, remove radar plot drag, filter B track by elevation>0 2026-08-03 03:44:37 +08:00
atsunatsu 81ae8772d2 feat: add 'current exact position' button for station A
Removed the automatic posA=stationPos override. Added a button
'当前精确位置' below the grid input that fills in the exact station
position from settings (lat/lon + grid). Users can now freely edit
the position fields and use the button when they want the exact
position.
2026-08-03 03:32:03 +08:00
atsunatsu 571145838d fix: gridToLatLon normalization bug + bidirectional grid/latlon sync
Root cause: gridToLatLon used (lon+180)%360-180 and (lat+90)%180-90
for normalization, treating grid values as from prime meridian/equator
when they are actually from IDL/South Pole. A 6-char grid like OL62AA
(112°E, 22°N) was returning -67.96°, -67.98° (Atlantic Ocean).

Also added bidirectional sync: entering a grid auto-fills lat/lon,
entering lat/lon auto-fills the 6-char grid, so users can verify.
2026-08-03 03:27:15 +08:00
atsunatsu 017b178f2e fix: use refineEdge with exact station position to get correct AOS/LOS boundaries 2026-08-03 03:15:39 +08:00
atsunatsu 670444858c fix: use getElevation for elevation sampling (same function as getLeoPass)
getFullPosition and getElevation both call calculateObs internally,
but the elevation values reported by the user (-60°) suggest they may
differ. Now elevation is computed via getElevation (same as getLeoPass),
while getFullPosition is only used for azimuth.
2026-08-03 03:13:11 +08:00
atsunatsu 7bdff5df81 fix: use passes list with corrected station position, fallback to independent search 2026-08-03 03:08:35 +08:00
atsunatsu df083840ac fix: use independent search algorithm directly, no passes list reuse
The passes list AOS/LOS times can be stale or computed for a different
station position, causing elevation curves to show -60° at pass start.
The independent search (refineEdge + sampleMutualPass) always computes
elevation for the actual positions. With posA now set to the exact
station position, the pass times should match the main page.
2026-08-03 03:03:08 +08:00
atsunatsu a0279bac17 fix: use pass AOS/LOS times directly without refineEdge
The refineEdge function re-computed boundaries using the grid-center
position (posA), which differs from the exact station position that
the passes list was computed with. This caused all passes to be
filtered out. Now we trust the pass list's AOS/LOS times directly
and just sample the elevation/azimuth curves.
2026-08-03 02:47:09 +08:00
atsunatsu 3d630041e1 chore: add debug info when mutual pass search finds nothing
Shows pass count and satellite count in the error message to help
diagnose why the main pass list yields no results.
2026-08-03 02:27:01 +08:00
atsunatsu ea92d3c150 fix: add fallback pass search when main pass list yields no results
If the main pass list (satelliteRepo.passes) is empty or the common
window check filters everything out, fall back to the independent
search algorithm so the user always gets results.
2026-08-03 02:23:51 +08:00
atsunatsu 86ee3dcbac fix: reuse main page pass list for mutual search
findMutualPasses was computing passes independently with its own
search algorithm, which produced different results from the main
page's getLeoPass. Now it directly reuses satelliteRepo.passes,
which is the same list shown on the main passes page. The only
additional computation is refining the common window for station B
and sampling elevation/azimuth curves.
2026-08-03 02:16:15 +08:00
atsunatsu 43788ba7a7 fix: mutual pass search now matches main radar pass definition
Root cause: mutual search used the min-elevation threshold (default 10°)
as the AOS/LOS boundary, while the main radar uses the 0° horizon
(getLeoPass). With identical stations the windows therefore never
matched. Also, an in-progress pass was counted as a new one.

- AOS/LOS boundaries now at the 0° horizon (refined to 1s)
- Skip in-progress mutual windows at search start (like getLeoPass)
- Filter requires BOTH stations to reach their min elevation
- Default min elevation follows the main radar passes setting
2026-08-03 02:04:13 +08:00
atsunatsu 1a3f1bb34c fix: radar track rendering bugs
- Elevation ring labels were inverted (90/60/30 from outer to inner);
  now 30/60/90 correctly from outer ring to center
- Split track paths at the 0/360° azimuth wrap in both MutualRadarView
  and main RadarView, so passes crossing due north no longer draw a
  line straight across the plot
2026-08-03 01:53:37 +08:00
atsunatsu 998c98267b fix: draw full station-B track on main radar, live dot on top
Track line is no longer time-limited (always shows the whole mutual
arc). The station-B live position is drawn separately on the line
using the same pulsing-dot mode as the local station.
2026-08-03 01:29:17 +08:00
atsunatsu 28fa373db0 feat: bidirectional drag sync between elevation curve and radar track
- ElevationCurveChart is now controlled (progress + onProgressChange)
- MutualRadarView shows shared time-cursor positions for both stations
  and is draggable/tappable to move the cursor
- MutualPassCard owns a single dragProgress feeding both charts, so
  dragging either chart moves the other in sync
2026-08-03 00:58:09 +08:00
atsunatsu 4ca33c37ad feat: keep mutual state across nav + overlay station-B track on main radar
- MutualViewModel is Activity-scoped, so returning from Radar keeps query results
- TrackSampleData gains time field for live cut-off
- Removed standalone mutual card from radar page
- RadarViewCompose draws optional dashed station-B track + current dot
- RadarScreen builds trackB from mutual data up to current time
2026-08-03 00:55:05 +08:00
atsunatsu bd42e643ea feat: dual-station radar track plot (satlover/satmatch style)
- MutualPass now carries TrackSample (azimuth/elevation for both stations)
- New MutualRadarView: polar plot with elevation rings (30/60/90),
  cardinal spokes, solid A-track vs dashed B-track, AOS/LOS markers
- Shown in expanded mutual pass card and radar page mutual card
2026-08-03 00:21:20 +08:00
atsunatsu 4baf3821e5 fix: elevation curve x-axis float precision loss
Absolute epoch millis (~1.7e12) exceeds Float precision (ULP ~131s
at that magnitude), so 5s-spaced samples collapsed onto identical
x coordinates, producing a stepped/jagged curve. Use Long relative
time deltas (sample - start) before converting to Float.
2026-08-02 23:36:59 +08:00
atsunatsu d7e1ce11c9 fix: smooth dual-station elevation curve
- Fix cubic Bezier control points (proper Catmull-Rom to Bezier conversion)
- Refine AOS/LOS more robustly by walking from edge into the pass window
- Still 5s sampling for smooth curves
2026-08-02 23:07:28 +08:00
atsunatsu 5de370983d fix: lock screen orientation to portrait 2026-08-02 22:19:37 +08:00
atsunatsu f272e268ab fix: smooth elevation curves using cubic Bezier instead of lineTo
Replace Path.lineTo() with Path.cubicTo() using Catmull-Rom
to Bezier conversion for smooth elevation curves.
2026-08-02 22:10:17 +08:00
atsunatsu e59507a4d3 fix: smoother elevation curves with refined AOS/LOS and 5s sampling
- Reduce sample interval from 10s to 5s for smoother curves
- Add refineEdge() to find exact AOS/LOS at 1s resolution
- Curve now starts/ends at the correct horizon-crossing points
2026-08-02 21:59:51 +08:00
atsunatsu 0bae306312 fix: default station A grid, find all mutual passes, color toArgb()
- Pre-fill station A grid from settingsRepo.stationPosition
- Find ALL mutual passes per satellite (not just first)
- Fix Color.hashCode() -> Color.toArgb() for native canvas paint
- Use 2min gap between pass searches to avoid duplicates
2026-08-02 21:44:31 +08:00
atsunatsu 65f61a5147 fix: mutual pass elevation unit (radians->degrees) + grid input
Bug: OrbitalObject.getElevation() returns radians, but
MutualViewModel was comparing it directly with degree values
from the slider (10-90), causing no matches to be found.

Fix: add elevationDeg() helper that converts to degrees.

Also add Maidenhead grid square input support (4/6/8 chars)
alongside existing lat/lon fields. Grid takes priority when
filled.
2026-08-02 21:36:44 +08:00
atsunatsu 1064dc739e feat: add mutual pass query with dual-station elevation curve
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
2026-08-02 21:22:42 +08:00
atsunatsu 8aedd38b51 fix: consistent Chinese date format for pass headers and sun times
Extract dateFormat() helper to ensure computeSunTimes and
groupPasses use identical date format, fixing sunrise/sunset
display bug.

For Chinese locale: "2026年8月1日 星期六"
For English locale: "Sat, 01 Aug 2026"
2026-08-02 20:28:56 +08:00
atsunatsu ad8f08a577 fix: revert DateFormat.FULL to SimpleDateFormat with locale
DateFormat.FULL may cause inconsistent date labels between
computeSunTimes and groupPasses, leading to missing or
wrong sunrise/sunset times. Revert to the original pattern
"EEE, dd MMM yyyy" but with Locale.getDefault() so day
names follow the system language.
2026-08-02 20:26:01 +08:00
atsunatsu 2afce54472 fix: restore satellite catalog number in satellite selection list
Keep number removed from pass list (Components.kt) and
pass detail (PassesScreen.kt) as requested, but restore
it in the satellite selection list (SatellitesScreen.kt).
2026-08-02 20:09:51 +08:00
atsunatsu 8e50fcf8fb style: use primary color for satellite name (same as former number)
Use MaterialTheme.colorScheme.primary for satellite name
text in all three lists (Components, SatellitesScreen,
PassesScreen), matching the color that was previously
used for the catalog number.
2026-08-02 19:58:00 +08:00
atsunatsu e11b03d56b refactor: remove satellite catalog number from UI
Remove the NORAD catalog number (e.g. '44444 - ') prefix
from satellite names in three places:
- Pass list items (Components.kt)
- Satellite selection list (SatellitesScreen.kt)
- Pass detail view (PassesScreen.kt)

The number is still accessible via satellite details if needed.
2026-08-02 19:48:14 +08:00
atsunatsu fa55b82a5a fix(i18n): use locale-aware full date format for Chinese convention
Replace SimpleDateFormat("EEE, dd MMM yyyy") with
DateFormat.getDateInstance(DateFormat.FULL, locale) which
automatically uses the correct format for each locale:

- Chinese: "2026年8月1日 星期六"
- English: "Saturday, August 1, 2026"
- Other locales: their respective conventions
2026-08-02 19:34:43 +08:00
atsunatsu d57832b0d4 fix(i18n): use system locale for date formatting instead of English
PassesViewModel and PassesScreen used Locale.ENGLISH for
SimpleDateFormat, causing day-of-week names like 'Sun' to
always show in English. Changed to Locale.getDefault() so
the device's language setting is respected.

Affects: 'EEE, dd MMM yyyy' date labels in the pass list
(group headers and sun times), and 'HH:mm:ss' time format.
2026-08-02 19:30:42 +08:00
atsunatsu 1d0e8921a9 i18n(zh): complete Chinese translation with new features
- Add Doppler calculator strings (多普勒频率计算器)
- Add CW decoder strings (CW 解码器、开始、停止、清空)
- Add CAT radio control strings (电台控制、CAT 电台控制等)
- Add data import error messages
- Update outro thanks with xdsopl and Robot36 contributors
- Preserve all existing 154 lines of translation
- 20 new strings added
2026-08-02 19:23:17 +08:00
atsunatsu 23d95b8241 fix(cw): fix timing analysis - process per spectrogram column
- Add newColumnCount tracking to CwSpectrogram
- CwDecoder now processes each new column individually for timing
- Each column = 8ms at 8000 Hz sample rate
- Proper per-column iteration through spectrogram history
2026-08-01 19:52:39 +08:00
atsunatsu 8af80866bc feat(cw): v3 spectrogram-based multi-channel Bayesian decoder
Complete rewrite inspired by Morse Expert / CW Skimmer (VE3NEA):
- CwFFT: radix-2 FFT (256-point) for time-frequency analysis
- CwSpectrogram: sliding-window waterfall (40 cols x 33 bins, 8ms resolution)
- CwBayesianDecoder: Gaussian probability replaces hard dit/dash thresholds
- CwChannelTracker: multi-channel peak detection (up to 3 signals)
- CwDecoder: integrates all components, monitors 200-1200 Hz simultaneously

Key advantages over v2 (ggmorse):
- Frequency-agnostic: full spectrum monitored, not locked to one tone
- Multi-channel: tracks multiple signals in parallel
- Bayesian: probability-based decisions, not hard ratios
- Doppler tolerant: frequency drift just moves energy between bins
2026-08-01 19:41:32 +08:00
atsunatsu b9e5ff70f5 feat(cw): add continuous pitch tracking for Doppler drift
- After initial pitch lock, re-scan every ~8 seconds in a narrow
  ±100 Hz window around the current pitch estimate
- Only re-lock if pitch change > 20 Hz (avoids jitter)
- Helps maintain lock when Doppler tracking has residual error
2026-08-01 19:25:27 +08:00
atsunatsu c176d6ad88 feat(cw): port ggmorse algorithms - auto pitch/speed detection, adaptive threshold
- Add CwResampler: linear resampler (downsample to 4 kHz base rate)
- Add CwFilter: first-order IIR high-pass (200 Hz) + low-pass (1200 Hz)
- Add CwGoertzel: running Goertzel filter for tone tracking
- Add CwPitchDetector: DFT-based pitch detection (200-1200 Hz, 10 Hz steps)
- Rewrite CwDecoder: auto pitch detection, auto speed estimation (5-55 WPM),
  adaptive threshold with exponential moving average, resampled 4 kHz pipeline
- 22 unit tests covering resampler, filter, Goertzel, pitch detector, decoder state
- All existing UI/ViewModel code unchanged (same class interface)
2026-08-01 19:22:14 +08:00
atsunatsu 5a6b162e4f fix(cw): add microphone permission check before starting decoder
- Dispatch CwPermissionResult alongside SstvPermissionResult
- Add requestMicPermission callback through TransceiversPage chain
- Check cw.hasPermission before starting audio capture
- Prevent crash when RECORD_AUDIO not granted
2026-07-31 19:00:56 +08:00
atsunatsu 33712c29fc feat(cw): add built-in CW Morse code decoder for linear transponders
- Add CwDsp with FIR bandpass filter, envelope detection, Goertzel tone detector
- Add CwDecoder with real-time Morse timing analysis and character lookup
- Add CW state/actions to RadarState, wire into RadarViewModel
- Add collapsible CW decoder panel to transceivers page
- 19 unit tests covering DSP, Morse table, and decoder state
- Shares IAudioCapture with SSTV, auto-stops SSTV when CW starts
2026-07-31 18:44:49 +08:00
atsunatsu eac1e6e273 feat: add Doppler frequency calculator for linear transponders (issue #91)
- Add DopplerFrequencyCalculator utility with passband mapping + Doppler correction
- Add MHz input UI with real-time 1Hz refresh during pass
- Add kHz offset for downlink frequency correction
- TX/RX fields on same row for compact layout
- 40 unit tests covering linear/FM/inverted transponders
2026-07-31 14:17:19 +08:00
Arty Bishop f585372594 Added cleartext traffic support for custom TLE URLs #227 2026-07-30 17:15:02 +02:00
Arty Bishop 4585332b1a Fixed manual OMM (.csv) data import, tweaked messaging 2026-07-30 15:56:50 +02:00
Arty Bishop 6d2b0ced49 Implemented AOS window and elevation highlight filters 2026-07-30 13:28:37 +02:00
Arty Bishop e86bc2c700 Added a few tweaks to SSTV sensitivity and reception 2026-07-30 12:01:54 +02:00
Arty Bishop 9a52fdfde5 Added support for Icom IC-705 CAT (#229) 2026-07-30 11:43:03 +02:00
Rui Oliveira 07504a2a95 Add the option to override the frequency from the radio 2026-07-24 20:29:51 +01:00
Rui Oliveira 81525b6dd3 Map AFSK (e.g. ISS APRS) to FM 2026-07-24 20:10:37 +01:00
Rui Oliveira c7089ab314 Fix setting the mode (e.g. FM, USB, LSB) 2026-07-24 20:10:10 +01:00
Rui Oliveira 77dfb7bb05 Remove PTT-aware logic, which is not necessary
I was setting the Tx frequency only when we were
in Tx mode, but the Icom 705 will accept the
command to set the Tx frequency even when in Rx mode, so
there is no need to check the PTT state before sending the command.
2026-07-24 20:09:43 +01:00
Rui Oliveira 58fb688593 Fix bugs in setting the Tx frequency
Uses 0x25 01 command to set the Tx frequency,
which is the correct command when in split mode.
2026-07-24 20:09:01 +01:00
Rui Oliveira dc596b4348 Fix bugs in setting Rx frequency 2026-07-24 20:08:46 +01:00
Rui Oliveira f9b1a97a37 Refactor "Disconnect" and "Track" logic 2026-07-24 20:08:23 +01:00
Rui Oliveira d700432829 Add initial support for Icom IC-705 radio
- Added support for Icom IC-705 radio model with split mode functionality.
- Updated RadioTrackingService to handle single-radio split mode and dual-radio configurations.
- Modified MainContainer to provide appropriate radio controllers based on selected model.
- Enhanced SettingsRepo to include split mode preference in radio control settings.
- Updated UI in SettingsDialog to allow toggling of split mode for IC-705 and adjust device selection accordingly.
- Improved handling of baud rates based on selected radio model.
- Added extended operations in IRadioController for IC-705 specific commands.
2026-07-24 20:07:11 +01:00
Arty Bishop b17ea2d918 v4.4.3 - Added required tweaks to support Android 17 (API 37) 2026-06-27 14:15:05 +01:00
Arty Bishop 976fdfd949 Added Star History graph to README, tweaked AGENTS 2026-06-27 12:31:24 +01:00
dependabot[bot] d8db258bd8 Bump actions/checkout from 6 to 7 (#221)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-24 16:38:32 +01:00
Arty Bishop 7b6fb5eb8f v4.4.2 - Fixed transceivers retention, various minor tweaks 2026-06-21 15:19:47 +01:00
Arty Bishop f4aef4f4f0 Fixed SSTV frequency display and "All" category retention 2026-06-21 15:10:28 +01:00
Arty Bishop 9147323db2 Cleaned up SensorsRepo class, removed deprecated calls 2026-06-21 15:03:45 +01:00
Arty Bishop 81d397a09d v4.4.1 - Continuous SSTV decoding, frequency display 2026-06-07 17:23:19 +01:00
Arty Bishop ec1a55c93d Added initial code for possible future deeplink nav support 2026-06-07 16:01:23 +01:00
Arty Bishop e5033cb2e9 Added continuous SSTV decoding of manually selected type 2026-06-06 19:20:35 +01:00
Arty Bishop 39b786ae82 v4.4.0 - Implemented CAT and SSTV support in RadarScreen 2026-06-02 20:40:06 +01:00
Arty Bishop 79809aaec0 Added SSTV image decoding functionality to RadarScreen 2026-06-02 20:06:22 +01:00
Arty Bishop f7f5a73c4c Added colored elevation and decay check to satellite passes 2026-05-30 15:35:52 +01:00
Arty Bishop 18c8ac1822 Merged RadarScreen and RadioControlScreen functionality 2026-05-30 14:14:40 +01:00
Arty Bishop 22607e7710 v4.3.2 - Implemented Swipe-to-Focus behavior, minor fixes 2026-05-30 13:16:14 +01:00
Arty Bishop ad0ff6859e Updated release workflow to avoid third-party dependencies 2026-05-25 18:30:49 +01:00
Arty Bishop a39b1716e7 Added consistent pass selection to Radar and Map screens 2026-05-25 18:30:49 +01:00
Arty Bishop 8a2339cfbf Implemented Swipe-to-Focus behavior for satellite passes 2026-05-25 18:30:48 +01:00
Arty Bishop 821fa0dfe7 v4.3.1 - Zipped custom sources handling, translation fixes 2026-05-04 16:55:15 +01:00
Arty Bishop ce8c2a4834 Added zipped data sources handling to DatabaseRepo 2026-05-04 15:36:42 +01:00
Arty Bishop 8cc4ef5610 Tweaked passes list to show DeepSpace ones at the top 2026-05-04 15:36:42 +01:00
Mubi-Baihua dde22faf46 Added minor tweaks to Chinese translation (#216) 2026-05-02 19:32:18 +01:00
Arty Bishop 7ad81e32e1 v4.3.0 - Sticky header, sun/moon positions, red night mode 2026-04-30 19:32:17 +01:00
Arty Bishop 5af963f3c9 Added several tweaks to sunrise/sunset time calculations 2026-04-28 21:00:43 +01:00
Arty Bishop bfb9fb6ca8 Tweaked ViewModel retention to mirror the Nav2 behavior 2026-04-26 14:44:29 +01:00
Emre Can AkdaşandArty Bishop df24c66ef3 Fixes for Turkish translation, by Emre Can Akdaş (TA3ECR)
Co-authored-by: Arty Bishop <44072814+rt-bishop@users.noreply.github.com>
2026-04-26 14:42:16 +01:00
Arty Bishop b166e896c2 Added current moon/sun positions to the RadarScreen 2026-04-26 14:32:16 +01:00
Arty Bishop 18f8ab517d Added current moon/sun positions to the MapScreen 2026-04-26 00:11:08 +01:00
Arty Bishop d3b8e951dd Added red night mode filter overlay for the whole app 2026-04-25 19:22:32 +01:00
Arty Bishop 6e5a3cb0bb Added sticky header with a date and sunrise/sunset time 2026-04-25 19:20:22 +01:00
Arty Bishop 31d329a19b Added CelestialComputer, extracted common functionality 2026-04-25 18:18:00 +01:00
Arty Bishop 3df358b88c v4.2.2 - Kotlin Serialization, Navigation3, translation fixes 2026-04-24 17:24:00 +01:00
Arty Bishop 7d5bca300d Migrated to Compose Navigation3, updated dependencies 2026-04-24 17:20:23 +01:00
Arty Bishop fb2cd85b11 Migrated to KotlinX Serialization library for JSON parsing 2026-04-24 17:18:06 +01:00
Arty Bishop 99158e74fa v4.2.1 - Turkish translation, DeepSpace filter, various tweaks 2026-04-19 13:14:31 +01:00
Arty Bishop 8265b73d75 Added DeepSpace passes filter, fixed refresh issue (#208) 2026-04-18 15:56:28 +01:00
Emre Can Akdaş d2b3184084 Added Turkish translation, by Emre Can Akdaş (TA3ECR) (#211) 2026-04-18 10:38:42 +01:00
Arty Bishop 0c5d3699c0 v4.2.0 - CAT control, HamLib commands, performance tweaks 2026-04-11 16:48:24 +01:00
Arty Bishop a94f95874b Added several tweaks to Settings and RadioControl screens 2026-04-10 14:13:53 +01:00
Arty Bishop 4a3a01729b Added several tweaks to Radar and RadioControl screens 2026-04-10 12:21:03 +01:00
Arty Bishop aadc285d7f Added multiple tweaks to Satellites, Passes and Map screens 2026-04-10 11:50:32 +01:00
jcmerg 2df0b9ba3d Add CAT radio control for full-duplex satellite operation (#207) 2026-04-10 11:21:52 +01:00
Arty Bishop 868155c532 Simplified data output code, aligned with HamLib defaults 2026-04-06 12:48:06 +01:00
Arty Bishop 3f981c09d5 Tweaked output dialogs ui, fixed manual import category 2026-04-06 11:57:12 +01:00
Arty Bishop b79fec13f4 Removed the eclipsed badge, tweaked visual indication 2026-04-05 11:45:07 +01:00
Arty Bishop 6588291997 Fixed UTC pass time problem mentioned in issue #201 2026-04-05 11:27:27 +01:00
Arty Bishop be96602c76 Added normalized time for AOS/LOS, fixed passes refresh 2026-04-04 18:43:05 +01:00
Arty Bishop b59f9a6c17 Simplified SettingsRepo.kt and SettingsScreen.kt classes 2026-04-04 15:44:51 +01:00
Arty Bishop 9afe2ab69e Heavily optimized MapScreen.kt and position calculations 2026-04-03 16:45:02 +01:00
theojalba 2edc892dc2 Switch to short-form commands in NetworkReporter (#206) 2026-04-03 16:42:18 +01:00
Arty Bishop 45eb6915c2 Optimized common composables and satellite selection 2026-04-02 08:28:37 +01:00
Arty Bishop 706c912b13 Simplified BluetoothReporter and NetworkReporter 2026-03-30 22:02:49 +01:00
Arty Bishop e523eb5517 Added tweaks to DataParser.kt and DatabaseRepo.kt 2026-03-29 13:37:04 +01:00
Arty Bishop 0bef2ffddc Added tweaks to positions and passes calculation 2026-03-29 13:09:03 +01:00
Arty Bishop 279594c425 Simplified RadarScreen/View, added sensors smoothing 2026-03-28 20:04:03 +00:00
Arty Bishop 8c1df334d8 Added even more tweaks to convention plugins setup 2026-03-28 20:03:11 +00:00
Arty Bishop e2bf2a09a4 v4.1.1 - Fixes for landscape/split screen issues, by londre7 2026-03-22 11:07:23 +00:00
londre7 1d48ce144f Use the compass in landscape 2026-03-21 12:29:46 +00:00
Arty Bishop 082b52c58b Added more tweaks to convention plugins setup 2026-03-18 15:59:55 +00:00
londre7 c1966442a7 Fixed track path creation issue for split screens (#199) 2026-03-09 13:50:00 +00:00
Arty Bishop 4d88214c1d Added basic convention plugins setup for modularization 2026-03-01 20:02:30 +00:00
Arty Bishop de99e09f5e v4.1.0 - Remote freq control for Hamlib/SDR++, by londre7 2026-02-22 13:51:58 +00:00
Arty Bishop e717d75324 Added a few visual fixes to extended data output feature 2026-02-21 12:31:59 +00:00
wty2019wty a545863473 Updated the Chinese translation for freq output (#196) 2026-02-21 09:35:22 +00:00
londre7 85ffe94266 Implemented extended data output (#195) 2026-02-20 18:42:55 +00:00
Arty Bishop 7adbf951bc v4.0.4 - R4UAB source priority, Chinese translation fixes 2026-02-14 14:12:36 +00:00
Mubi-Baihua 78dc217642 Revise the Chinese translation. (#194) 2026-02-14 12:22:25 +00:00
Arty Bishop 64137473b3 v4.0.3 - KeepScreenOn for RadarScreen, translation fixes 2026-02-10 15:25:28 +00:00
Kaitlyn Kenwell c08effb445 Add setting to keep the screen on in radar view (#193) 2026-02-09 19:31:55 +00:00
troilus 65410defe2 Translate custom data source strings to Chinese 2026-02-08 11:09:03 +00:00
Arty Bishop ed71c8148b v4.0.2 - Custom data import by londre7, minor UI tweaks 2026-02-07 17:22:42 +00:00
Arty Bishop 13a79af27e Added minor tweaks to import components and strings 2026-02-07 13:41:12 +00:00
londre7 f6564c3c71 Added custom data sources import, by londre7 (#189) 2026-02-07 13:03:12 +00:00
Arty Bishop 8e42f81ea3 Added tweaks to components, updated dependencies 2026-02-07 12:30:46 +00:00
Eugene ada5b48ba5 minor updates for uk localization 2026-02-02 20:28:48 +00:00
Arty Bishop a6a59877a8 v4.0.1 - Hotfix for RadarScreen track projection display 2026-01-31 13:55:44 +00:00
Arty Bishop 6aa455490b v4.0.0 - Major release with Jetpack Compose and API 36 2026-01-30 18:19:19 +00:00
Arty Bishop f81738bc8b Added even more tweaks to translations and strings.xml 2026-01-30 15:40:02 +00:00
Arty Bishop a57effd073 Added even more tweaks to resources and app icons 2026-01-28 12:36:24 +00:00
Arty Bishop c180f6e58e Added more tweaks to translations and strings.xml 2026-01-27 18:12:20 +00:00
Arty Bishop 1290b8f32f Added various tweaks to translations, sorted strings.xml 2026-01-26 18:04:02 +00:00
Arty Bishop 9e765a3575 Added various tweaks to dependencies and translations 2026-01-16 14:59:21 +00:00
Arty Bishop 65a0c33007 Migrated AGP to v9.0.0, tweaked gradle.properties 2026-01-16 13:54:13 +00:00
Arty Bishop 7add81e8b6 Added tweaks to GH actions workflow, refactored strings 2026-01-11 17:33:16 +00:00
Arty Bishop 1a69822d3f Added more tweaks to the Transceiver card component 2025-12-27 15:02:15 +00:00
Arty Bishop c370d9febd Added more tweaks to components and dependencies 2025-12-27 15:02:15 +00:00
CertainBot 24002827ed Created Spanish tranlation
Thanks for this great app!

A Spanish translation is missing, so I have added the spanish `.../values-es-rES/strings.xml`. Hope this will give access to many new users.

Pulled against development branch for you to use it as you wish.

Regards
2025-12-27 15:02:15 +00:00
Arty Bishop a835ac4e07 Added remote data output sections to SettingsScreen.kt 2025-12-27 15:02:15 +00:00
Arty Bishop a68e466040 Restructured SettingsScreen layout, tweaked components 2025-12-27 15:02:15 +00:00
Arty Bishop bfbe437ab1 Cleaned up classes and resources, updated dependencies 2025-12-27 15:02:15 +00:00
Arty Bishop 574ffe09c2 Switched back to bottom navigation, tweaked components 2025-12-27 15:02:15 +00:00
WaterBlock623 529a68f291 Update strings.xml
Add some new text in Chinese
2025-12-27 15:02:15 +00:00
Arty Bishop 266fbfed1f Added more tweaks to components and dependencies 2025-12-27 15:02:14 +00:00
Arty Bishop fb8053ef60 Added animated visibility to the FloatingBar component 2025-12-27 15:02:14 +00:00
Eugene 34e66f9ae0 Update Ukrainian translation 2025-12-27 15:02:12 +00:00
Arty Bishop 10fc88364f Integrated fixes from release v3.2.1, updated dependencies 2025-12-27 15:01:47 +00:00
Mubi-Baihua 2e5a15eec7 Update strings.xml 2025-12-27 15:01:47 +00:00
Arty Bishop 42aa4deb0d Tried using floating app bar layout for screen navigation 2025-12-27 15:01:47 +00:00
Arty Bishop 82f41940c7 Added a few minor tweaks to resources and screens 2025-12-27 15:01:47 +00:00
Arty Bishop 6c3aeab041 Added a few minor fixes to Dialogs and RadarScreen 2025-12-27 15:01:47 +00:00
Arty Bishop 609c83ed34 Implemented landscape support for MapScreen 2025-12-27 15:01:47 +00:00
Arty Bishop 5a0ff019e9 Added more tweaks to screens and dependencies 2025-12-27 15:01:47 +00:00
Arty Bishop ad89830c51 Set up more screens with portrait/landscape TopBar 2025-12-27 15:01:47 +00:00
Arty Bishop da2c3853b0 Added initial support for portrait/landscape TopBar 2025-12-27 15:01:47 +00:00
Arty Bishop 94735bc453 Reverted using SharedElement API and removing NavBar 2025-12-27 15:01:47 +00:00
Arty Bishop 42e7fa4356 Removed Material3 NavigationBar, other minor fixes 2025-12-27 15:01:47 +00:00
Arty Bishop 9ea4da9d21 Updated dependencies and gradle plugin, minor fixes 2025-12-27 15:01:47 +00:00
Arty Bishop a6cbd89ff1 Implemented experimental SharedElement transition API 2025-12-27 15:01:47 +00:00
Arty Bishop aa9365b2fd Updated to API 36 (Android 16), bumped dependencies 2025-12-27 15:01:47 +00:00
Arty Bishop bffb4842b1 Added Selection Warning and App Welcome Dialogs 2025-12-27 15:01:47 +00:00
Arty Bishop fcec36ec68 Updated dependencies and gradle plugin, minor fixes 2025-12-27 15:01:47 +00:00
Rico Beier-Grunwald 483d846233 Improvised solution to transmit frequencies to IC705 2025-12-27 15:01:47 +00:00
Arty Bishop 11841ca3d7 Extracted a few UI actions into reusable use cases 2025-12-27 15:01:47 +00:00
Arty Bishop 01139f76e7 Implemented EclipsedIndicator and MapScroll on select 2025-12-27 15:01:47 +00:00
Arty Bishop fdbdc513f1 Implemented a basic map data card on MapScreen 2025-12-27 15:01:47 +00:00
Arty Bishop 880fe10bfd Added multiple fixes to MapScreen and components 2025-12-27 15:01:47 +00:00
Arty Bishop 5b62474c27 Added multiple fixes to RadarScreen and components 2025-12-27 15:01:47 +00:00
Arty Bishop cdf642d9f3 Added multi selection for Satellite types filter dialog 2025-12-27 15:01:47 +00:00
Arty Bishop e3955239f3 Added even more tweaks to Dialog Layouts and UI 2025-12-27 15:01:47 +00:00
Arty Bishop 5842239e26 Added several tweaks to Screens, Dialogs and Layouts 2025-12-27 15:01:47 +00:00
Arty Bishop 86512a44d2 Updated dependencies and gradle plugin, minor fixes 2025-12-27 15:01:47 +00:00
Arty Bishop d1b579ac4c Updated dependencies and gradle plugin, minor fixes 2025-12-27 15:01:46 +00:00
Arty Bishop 5040767296 Updated dependencies and gradle plugin, minor fixes 2025-12-27 15:01:46 +00:00
Arty Bishop 2b6a892a50 Updated dependencies and gradle plugin, minor fixes 2025-12-27 15:01:46 +00:00
Arty Bishop 5aa837e5ef Updated dependencies and gradle plugin, minor fixes 2025-12-27 15:01:46 +00:00
Pr0pHesyer f1c978d167 Added light theme support, monochrome laucher icon 2025-12-27 15:01:46 +00:00
domnantas 34ec942e7b Implement audio cues for aiming 2025-12-27 15:01:46 +00:00
Arty Bishop 630fa2877f Integrated fixes from release v3.1.4 (leap year bug) 2025-12-27 15:01:46 +00:00
Arty Bishop ad0b611bcb Added several tweaks to PassesDialog, RadiosDialog 2025-12-27 15:01:46 +00:00
Arty Bishop 68d814d996 Added several tweaks to TimerRow and NextPassRow 2025-12-27 15:01:46 +00:00
Arty Bishop 08767c5589 Updated dependencies, added EntriesDialog tweaks 2025-12-27 15:01:46 +00:00
Vasileios Chroniadis 1f53ffbda9 Update compose activity and navigation libs 2025-12-27 15:01:45 +00:00
Arty Bishop 8dc0cf7051 Fixed a minor crash in SettingsRepo, updated deps 2025-12-27 15:01:45 +00:00
Arty Bishop ae53e5dc35 Added minor fixes to EntriesDialog and passes view 2025-12-27 15:01:45 +00:00
Arty Bishop 213ac7c278 Switched to gradle version catalog for dependencies 2025-12-27 15:01:45 +00:00
Arty Bishop ab02abbf4e Switched back to Wikimedia map tiles provider 2025-12-27 15:01:45 +00:00
Arty Bishop a1d5cda998 Updated dependencies, bumped the minSdk to 26 2025-12-27 15:01:45 +00:00
Arty Bishop a6b5498f55 Added type reset for satellite selection screen 2025-12-27 15:01:45 +00:00
Arty Bishop a52788e72f Added shared TimerBar composable function 2025-12-27 15:01:45 +00:00
Arty Bishop 4b63882c70 Fixed display and font size scaling for MainActivity 2025-12-27 15:01:45 +00:00
Arty Bishop 549a915b24 Slightly changed classes structure for readability 2025-12-27 15:01:45 +00:00
Arty Bishop 19d0c1b108 Added more frequency data fields to Radio class 2025-12-27 15:01:45 +00:00
Arty Bishop 269d48d250 Added passes filtering by modes to PassesScreen 2025-12-27 15:01:45 +00:00
Arty Bishop a088a7a0a2 Switched to MVI architecture for PassesScreen 2025-12-27 15:01:45 +00:00
Arty Bishop b9ade5f243 Cleaned up SharedPreferences keys and values 2025-12-27 15:01:45 +00:00
Arty Bishop e100484810 Updated gradle plugin, removed BuildConfig usages 2025-12-27 15:01:45 +00:00
Arty Bishop b1aa623b29 Slightly changed classes structure for readability 2025-12-27 15:01:45 +00:00
Arty Bishop e36e064374 Added minor tweaks to Dialogs and layouts 2025-12-27 15:01:45 +00:00
Arty Bishop d59f9c7506 Created future InfoScreen, added resources 2025-12-27 15:01:45 +00:00
Arty Bishop 49f713d82c Cleaned up EntriesScreen and ViewModel logic 2025-12-27 15:01:45 +00:00
Arty Bishop 1aefb04eb5 Enabled StrictMode, removed several deprecations 2025-12-27 15:01:45 +00:00
Arty Bishop dca3f2c773 Updated compileSdk and targetSdk to Android 14 2025-12-27 15:01:45 +00:00
Arty Bishop fc0b995bac Added various minor fixes, cleaned up classes 2025-12-27 15:01:45 +00:00
Arty Bishop 5a8108384f Added minor fixes to resources and build scripts 2025-12-27 15:01:45 +00:00
Arty Bishop 7191e02f99 Updated gradle plugin and dependencies versions 2025-12-27 15:01:45 +00:00
Arty Bishop 0ca372b73f Added Sinhala translation by Dilshan Hapuarachchi 2025-12-27 15:01:45 +00:00
Arty Bishop c4ecdb7a78 Added GeoConverter.kt and various minor fixes 2025-12-27 15:01:45 +00:00
Arty Bishop 1719f8cc2e Updated gradle wrapper and cleaned up files 2025-12-27 15:01:45 +00:00
Arty Bishop 3580854479 Migrated project build files to use kotlin script 2025-12-27 15:01:45 +00:00
Arty Bishop 28e926eb86 Extracted implementation classes into data module 2025-12-27 15:01:45 +00:00
Arty Bishop 5a449d4133 Added a basic .editorconfig file to the project 2025-12-27 15:01:45 +00:00
Arty Bishop f8d5542af5 Renamed base module into domain 2025-12-27 15:01:45 +00:00
Arty Bishop 9bb73f28b7 Integrated the data update fix from v3.1.3 2025-12-27 15:01:45 +00:00
Arty Bishop 7e2b3bc712 Updated dependencies and libraries versions 2025-12-27 15:01:43 +00:00
Arty Bishop f8e7bcdf39 Even more restructuring and code cleanup 2025-12-27 15:01:40 +00:00
Arty Bishop a807cfd8f6 Further classes restructuring and code cleanup 2025-12-27 15:01:38 +00:00
Arty Bishop f6fcd21404 Simplified packages and classes structure 2025-12-27 15:01:33 +00:00
Arty Bishop 208c587ddf Created SelectionRepository, cleaned up code 2025-12-27 15:01:33 +00:00
Arty Bishop 5d2a870319 Minor fixes for entries, settings and maps 2025-12-27 15:01:33 +00:00
Arty Bishop 942c7ce65f Switched to a different maps tile source 2025-12-27 15:01:33 +00:00
Arty Bishop 4c13cc98a1 Replaced Google Hilt with manual DI 2025-12-27 15:01:33 +00:00
Arty Bishop 622d4e3632 Further classes movement and cleanup 2025-12-27 15:01:33 +00:00
Arty Bishop 542cfcfd50 Fixes and refactoring for LocationSource 2025-12-27 15:01:33 +00:00
Arty Bishop 12fabc8f23 Fixes and refactoring for SettingsScreen 2025-12-27 15:01:33 +00:00
Arty Bishop f7f317cf8b Major classes refactoring and code cleanup 2025-12-27 15:01:33 +00:00
Arty Bishop 6572dd4eab More fixes for SettingsScreen.kt and its states 2025-12-27 15:01:33 +00:00
Arty Bishop 466c029be6 Added more functionality to SettingsScreen.kt 2025-12-27 15:01:33 +00:00
Arty Bishop 3da5b43e0a Added radar ping animation of satellite position 2025-12-27 15:01:33 +00:00
Arty Bishop 38ad7c46a8 Reimplemented RadarView in Jetpack Compose 2025-12-27 15:01:33 +00:00
Arty Bishop 05b5af41ba Added navigation parameters to RadarScreen 2025-12-27 15:01:33 +00:00
Arty Bishop 049d84e261 Further classes movement and cleanup 2025-12-27 15:01:33 +00:00
Arty Bishop bf51fddc4d Further classes and dependencies cleanup 2025-12-27 15:01:33 +00:00
Arty Bishop 007e6d8f9d Dependencies, resources and code cleanup 2025-12-27 15:01:33 +00:00
Arty Bishop 68d35f0104 Converted LocatorDialog and PositionDialog 2025-12-27 15:01:33 +00:00
Arty Bishop 39959c7a10 Implemented ModesDialog in Jetpack Compose 2025-12-27 15:01:32 +00:00
Arty Bishop c592b8d3ce Added map in compose, removed MapFragment 2025-12-27 15:01:32 +00:00
Arty Bishop acb851d4f6 Added filter dialog, removed PassesFragment 2025-12-27 15:01:32 +00:00
Arty Bishop a08cfc7fb8 Added compose pull to refresh for passes list 2025-12-27 15:01:32 +00:00
Arty Bishop 1ea4da3baf Added types dialog, removed EntriesFragment 2025-12-27 15:01:32 +00:00
Arty Bishop a817423e7e Even more fixes for pass item in Compose 2025-12-27 15:01:32 +00:00
Arty Bishop 46f0b9608b More fixes for pass item in Compose 2025-12-27 15:01:32 +00:00
Arty Bishop ac479f80ee Initial pass item Compose implementation 2025-12-27 15:01:32 +00:00
Arty Bishop f13662747c Various fixes for EntriesScreen class 2025-12-27 15:01:32 +00:00
Arty Bishop f8d1909737 Installed Timber, updated dependencies 2025-12-27 15:01:32 +00:00
Arty Bishop 7628582fd7 Added fixes after switching to Material3 2025-12-27 15:01:32 +00:00
Arty Bishop b1ff0921c3 Switched to using Material3 components 2025-12-27 15:01:32 +00:00
Arty Bishop 0755b20cf0 Added Material3 dependencies and Theme 2025-12-27 15:01:30 +00:00
Arty Bishop 7dca788c01 More changes to Entries Screen in Compose 2025-12-27 15:01:29 +00:00
Arty Bishop 853c36d19b Initial entries list implementation in Compose 2025-12-27 15:01:28 +00:00
Arty Bishop ee5b5a894b Recreated About screen in Compose 2025-12-27 15:01:28 +00:00
Arty Bishop 8f411d4c5e Added Compose dependencies and basic classes 2025-12-27 15:01:28 +00:00
Arty Bishop 0303c53a72 Switched back to BottomNavigationView 2025-12-27 15:01:28 +00:00
5930 changed files with 44379 additions and 11694 deletions

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
# indicate this is the root of the project
root = true
[*.{kt,java,xml,gradle,md}]
charset = utf-8
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
+5
View File
@@ -0,0 +1,5 @@
# GitHub Copilot Instructions
Read `AGENTS.md` first, then `CLAUDE.md`.
`AGENTS.md` contains the architecture, module boundaries, implementation details, conventions, and gotchas.
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
-70
View File
@@ -1,70 +0,0 @@
name: Look4Sat CI
on:
push:
tags:
- v**
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Latest Commit
uses: actions/checkout@v4
- name: Setup java environment
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Assemble APK and Bundle
run: |
chmod +x ./gradlew
./gradlew assembleRelease
./gradlew bundleRelease
- name: Sign APK
uses: r0adkll/sign-android-release@v1
id: sign_apk
with:
releaseDirectory: app/build/outputs/apk/release
signingKeyBase64: ${{ secrets.KEY_STORE }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
alias: ${{ secrets.KEY_ALIAS }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "35.0.1"
- name: Sign Bundle
uses: r0adkll/sign-android-release@v1
id: sign_bundle
with:
releaseDirectory: app/build/outputs/bundle/release
signingKeyBase64: ${{ secrets.KEY_STORE }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
alias: ${{ secrets.KEY_ALIAS }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "35.0.1"
- name: Rename APK and Bundle
run: |
mv ${{steps.sign_apk.outputs.signedReleaseFile}} app/build/outputs/apk/release/look4sat.apk
mv ${{steps.sign_bundle.outputs.signedReleaseFile}} app/build/outputs/apk/release/look4sat.aab
- name: Deploy Bundle to Play Store
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{secrets.SERVICE_ACCOUNT_JSON}}
packageName: com.rtbishop.look4sat
releaseFiles: app/build/outputs/apk/release/look4sat.aab
track: beta
whatsNewDirectory: fastlane/metadata/android/en-US/whatsnew
- name: Create release
uses: ncipollo/release-action@v1
with:
artifacts: "app/build/outputs/apk/release/look4sat.apk,app/build/outputs/apk/release/look4sat.aab"
bodyFile: fastlane/metadata/android/en-US/whatsnew/whatsnew-en-US
token: ${{ secrets.RELEASE_TOKEN }}
+81
View File
@@ -0,0 +1,81 @@
name: release
on:
push:
tags:
- v**
env:
TAG_NAME: ${{ github.ref_name }}
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout Repository
uses: actions/checkout@v7
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '21'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
- name: Assemble Artifacts
run: ./gradlew assembleRelease bundleRelease
- name: Sign APK
run: |
echo "${{ secrets.KEY_STORE }}" | base64 -d > keystore.jks
APK=$(find app/build/outputs/apk/release -name "*.apk" | head -1)
BUILD_TOOLS=$(ls -d ${ANDROID_HOME}/build-tools/*/ | sort -V | tail -1)
${BUILD_TOOLS}apksigner sign \
--ks keystore.jks \
--ks-pass pass:${{ secrets.KEY_STORE_PASSWORD }} \
--ks-key-alias ${{ secrets.KEY_ALIAS }} \
--key-pass pass:${{ secrets.KEY_PASSWORD }} \
--out app/build/outputs/apk/release/look4sat.apk \
"$APK"
rm keystore.jks
- name: Sign Bundle
run: |
echo "${{ secrets.KEY_STORE }}" | base64 -d > keystore.jks
AAB=$(find app/build/outputs/bundle/release -name "*.aab" | head -1)
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \
-keystore keystore.jks \
-storepass ${{ secrets.KEY_STORE_PASSWORD }} \
-keypass ${{ secrets.KEY_PASSWORD }} \
"$AAB" ${{ secrets.KEY_ALIAS }}
rm keystore.jks
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.4'
- name: Deploy Bundle to Google Play
run: |
gem install multi_json
gem install fastlane --no-document
AAB=$(find app/build/outputs/bundle/release -name "*.aab" | head -1)
echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
fastlane supply \
--aab "$AAB" \
--json_key service_account.json \
--package_name com.rtbishop.look4sat \
--track production \
--skip_upload_images true \
--skip_upload_screenshots true \
rm service_account.json
- name: Create Release
run: |
gh release create $TAG_NAME --title=$TAG_NAME --generate-notes
gh release upload $TAG_NAME app/build/outputs/apk/release/look4sat.apk
+7 -1
View File
@@ -1,6 +1,9 @@
# Built application files
*.ap_
# Hermes AI plans and metadata
.hermes/
# Files for the ART/Dalvik VM
*.dex
@@ -15,6 +18,7 @@ out/
# Gradle files
.gradle/
build/
.cxx/
# Local configuration file (sdk path, etc)
local.properties
@@ -62,4 +66,6 @@ fastlane/readme.md
/app/release/app-release.aab
/app/release/app-release.apk
/app/release/output-metadata.json
/app/release/
/app/release/
/.kotlin/sessions/
dist/
+122
View File
@@ -0,0 +1,122 @@
# Look4Sat AI Agent Instructions
This is the canonical project guide for all AI assistants working on Look4Sat.
All assistant-specific files (`CLAUDE.md`, `.github/copilot-instructions.md`) point here.
---
## Project Overview
Look4Sat is an open-source, fully offline Android satellite tracker and pass predictor. It tracks 9000+ active
satellites using TLE/OMM data from Celestrak/SatNOGS, calculates orbital positions via SGP4/SDP4 models, and displays
passes relative to the user's location. Features include polar radar visualization, SSTV image decoding, satellite
ground track mapping, and pass predictions up to 10 days ahead. No ads, no tracking, no network required after initial
data download.
## Architecture
**MVI (Model-View-Intent)** with unidirectional data flow:
- `State` data class → exposed via `StateFlow` from ViewModel
- `Action` sealed interface → user intents dispatched to ViewModel's `onAction()`
- Jetpack Compose UI observes state and recomposes reactively
**Clean Architecture layers:**
| Module | Responsibility |
|----------------------|---------------------------------------------------------------------|
| `app` | Entry point. Aggregates all modules |
| `core:data` | Android library. Room DB, OkHttp networking, repo implementations |
| `core:domain` | Pure Kotlin (JVM). Orbital math (SGP4/SDP4), models, repo contracts |
| `core:presentation` | Android library. Compose theme, shared UI components, NavKeys |
| `feature:map` | OSMDroid map with ground tracks |
| `feature:passes` | Pass predictions and upcoming events |
| `feature:radar` | Polar radar view of satellite positions, SSTV image decoding |
| `feature:satellites` | Satellite list, filtering, selection |
| `feature:settings` | User preferences |
- `feature:*` modules depend only on `core:domain` + `core:presentation`. Features never depend on each other.
## Build & Run
```shell
# Debug build
./gradlew assembleDebug
# Release build (minified, shrunk resources)
./gradlew assembleRelease
# Run tests
./gradlew test
```
- **Min SDK**: 24 | **Target SDK**: 36 | **JDK**: 17
- **Gradle**: Uses version catalog (`gradle/libs.versions.toml`) + convention plugins in `build-logic/`
## Key Libraries
- **Compose** (BOM 2026.05.01) + Material3 Adaptive
- **Navigation3** (type-safe, uses `@Serializable` NavKeys)
- **Room** (KSP code generation) for local satellite/TLE storage
- **OkHttp** 5.x for TLE downloads
- **OSMDroid** for map rendering
- **Kotlin Serialization** for navigation args and data parsing
- **Coroutines** + `StateFlow` for async/reactive patterns
## Conventions
- **Minimal dependencies**: Avoid adding libraries when a simple manual solution exists. Fewer deps = less maintenance.
- **DI**: Manual — ViewModels use companion `factory()` methods with `IMainContainer` interface.
- **Navigation**: Type-safe Compose Navigation3 with `@Serializable` data classes as nav keys.
- **State naming**: `<Feature>State` data class + `<Feature>Action` sealed interface per feature.
- **No feature-to-feature deps**: All cross-feature communication goes through core layers.
- **Localization**: 7 languages (en, es, ru, si, tr, uk, zh).
## Data Formats & Migration
**TLE vs. OMM/CSV format:**
Look4Sat supports both TLE and OMM (Orbit Mean-Elements Message) formats for backward compatibility:
- **TLE format**: Traditional 3-line element format (deprecated). NORAD catalog numbers are 5-digit integers, which
are running out of space. Celestrak has signaled that TLE format will eventually be phased out.
- **OMM/CSV format**: The future standard. CSV files contain the same orbital parameters as TLE but use ISO 8601
timestamps and support larger NORAD IDs. Celestrak and SatNOGS already provide OMM data in CSV format.
**Current implementation:**
- `DataParser.kt` handles both `parseTLEStream()` and `parseCSVStream()` seamlessly
- TLE data is downloaded from configured sources and stored in Room database
- When downloading satellite data, the app automatically detects format and parses accordingly
- Both formats produce identical `OrbitalData` objects, ensuring transparent format switching
**Migration path:**
As NORAD catalog space becomes constrained, OMM/CSV will become the primary format. Look4Sat is already positioned
to handle this transition without code changes — existing users can continue using TLE files while new sources
transition to OMM/CSV automatically.
## Code Style
- Prefer **short, focused functions** — single responsibility, easy to read.
- **Exceptions**: Composable functions and math-heavy algorithms (SGP4/SDP4) may be longer.
- Strict code style — no dead code, no unused imports, consistent formatting.
## Roadmap
- **KMP migration**: `core:domain` is to become a fully shareable KMM module. Keep it pure Kotlin/JVM.
## Gotchas
- Orbital math lives in `core:domain/predict/` — it's dense vector math (SGP4/SDP4). Tread carefully.
- TLE/OMM data must be refreshed weekly for accurate predictions (satellite orbits decay). TLE format is legacy and
will eventually be deprecated in favor of OMM/CSV as NORAD catalog numbers approach the 5-digit limit.
- SSTV decoding in `feature:radar` is experimental; image quality depends on signal strength during satellite pass.
- `build-logic/convention/` contains all shared Gradle configuration — edit there, not in individual modules.
- ProGuard is enabled for release builds — don't add reflection-based libs or any other dependencies without asking.
## Copilot Working Mode: Code-Only
- Default to code changes only. Provide explanations in chat only.
- If documentation seems useful, ask first before creating files.
- Do NOT create any `.md` documentation files unless explicitly requested.
- Do NOT add README, guides, summaries, migration notes, or how-to files unless asked.
- Prefer minimal diffs focused on requested implementation.
- Default validation is static checks (`get_errors`). Do NOT run Gradle compile/test tasks unless explicitly requested.
+5
View File
@@ -0,0 +1,5 @@
# CLAUDE.md
Read `AGENTS.md` first, then follow the instructions there.
`AGENTS.md` contains the architecture, module boundaries, implementation details, conventions, and gotchas.
+27 -23
View File
@@ -1,34 +1,38 @@
# Look4Sat: Satellite tracker
# Look4Sat-BA7OPF
[![Look4Sat CI](https://github.com/rt-bishop/Look4Sat/actions/workflows/main.yml/badge.svg)](https://github.com/rt-bishop/Look4Sat/actions/workflows/main.yml)
[![Release](https://img.shields.io/github/v/release/atsunatsu/Look4Sat)](https://github.com/atsunatsu/Look4Sat/releases)
[<img src="https://play.google.com/intl/en_gb/badges/static/images/badges/en_badge_web_generic.png" alt="Get it on Google Play" height="80">](https://play.google.com/store/apps/details?id=com.rtbishop.look4sat)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80">](https://f-droid.org/packages/com.rtbishop.look4sat/)
**BA7OPF 定制版** — 基于 [rt-bishop/Look4Sat](https://github.com/rt-bishop/Look4Sat) 的业余无线电卫星追踪器,增加了线性卫星频率计算器等功能。
### Radio satellite tracker and pass predictor for Android, inspired by Gpredict
## 本仓库特色功能
<p float="left">
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" width="180"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" width="180"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3.png" width="180"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4.png" width="180">
</p>
- **双站过境匹配(Mutual Pass)** — 输入友台经纬度/网格,筛选双方同时可见的卫星过境,在地图上同时显示双方仰角曲线和地面轨迹,支持一键跳转雷达页查看详情
- **线性卫星转发器频率计算器** — 在雷达页的 Calculator 标签页中,支持 TX/RX 双向多普勒频率计算,以及下行频率偏移(offset)输入,方便操作带偏移的线性卫星
- **CW 解码器** — 集成 Morse Expert 解码引擎,支持瀑布图、实时解码文本
- **Passband 模式** — 支持通过位置滑块(Passband)自动计算 TX/RX 频率,避免切换时跳变
- **AMSAT 状态页** — 实时查看卫星的业余无线电转发器状态(开启/关闭/待机),支持 72 小时历史记录回放
- **中文界面优化** — 翻译修正、UI 布局调整,系统语言自动切换
### Track satellite passes with ease!
## 上游仓库
Thanks to the huge database provided by [Celestrak](https://celestrak.com/) and [SatNOGS](https://satnogs.org/) you have access to more than 5000 active satellites orbiting Earth. You can search the entire DB by satellite name or by NORAD catnum.
本仓库是 [rt-bishop/Look4Sat](https://github.com/rt-bishop/Look4Sat) 的分支,上游仓库的原始功能包括:
Satellite positions and passes are calculated relative to your location. To get reliable info make sure to set the observation position using GPS or QTH Locator in the Settings menu.
- 基于 Celestrak / SatNOGS 数据的 9000+ 活跃卫星追踪
- SGP4/SDP4 轨道预测,10 天过境预报
- 极坐标雷达图、地面轨迹图
- SSTV 图像解码
- 无广告、无跟踪、完全离线
The application is built using Kotlin, Coroutines, Architecture Components and Jetpack Navigation. It is now and always will be completely ad-free and open-source.
## 许可证
Huge thanks to [DownloadAstro](https://appoftheday.downloadastro.com/app/look4sat-satellite-tracker/) team for their interest to the app and the interview published.
GNU General Public License v3.0。详见 [LICENSE](LICENSE)。
## Main features:
## Star History
* Predicting satellite positions and passes for up to a week
* Showing the list of currently active and upcoming satellite passes
* Showing the active pass progress, polar trajectory and transceivers info
* Showing the satellite positional data, footprint and ground track on a map
* Custom TLE data import is available via files with TXT or TLE extensions
* Offline first: calculations are made offline. Weekly update of TLE data is recommended.
<a href="https://star-history.dera.page/#atsunatsu/Look4Sat&type=timeline&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=atsunatsu/Look4Sat&type=timeline&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=atsunatsu/Look4Sat&type=timeline&legend=top-left" />
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=atsunatsu/Look4Sat&type=timeline&legend=top-left" />
</picture>
</a>
-114
View File
@@ -1,114 +0,0 @@
plugins {
id "com.android.application"
id "org.jetbrains.kotlin.android"
id "org.jetbrains.kotlin.kapt"
id "dagger.hilt.android.plugin"
id "androidx.navigation.safeargs.kotlin"
}
android {
namespace 'com.rtbishop.look4sat'
compileSdk 35
defaultConfig {
applicationId "com.rtbishop.look4sat"
minSdk 21
targetSdk 35
versionCode 323
versionName '3.2.3'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
arg("room.expandProjection", "true")
}
}
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
// compose true
viewBinding true
}
composeOptions {
kotlinCompilerExtensionVersion compose_compiler_version
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
testOptions {
animationsDisabled true
unitTests.includeAndroidResources true
}
androidResources {
generateLocaleConfig true
}
}
kotlin {
jvmToolchain(17)
}
dependencies {
implementation project(":base")
implementation "androidx.core:core-ktx:$core_ktx_version"
implementation "androidx.core:core-splashscreen:$core_splashscreen_version"
implementation "androidx.constraintlayout:constraintlayout:$constraint_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.navigation:navigation-runtime-ktx:$navigation_version"
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "com.google.android.material:material:$material_version"
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "org.osmdroid:osmdroid-android:$osmdroid_version"
// implementation "androidx.compose.ui:ui:$compose_version"
// implementation "androidx.compose.ui:ui-tooling:$compose_version"
// implementation "androidx.compose.runtime:runtime:$compose_version"
// implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
// implementation "androidx.compose.compiler:compiler:$compose_compiler_version"
// implementation "androidx.compose.animation:animation:$compose_version"
// implementation "androidx.compose.foundation:foundation:$compose_foundation_version"
// implementation "androidx.activity:activity-compose:$activity_compose_version"
// implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
// implementation "androidx.compose.material:material:$compose_material_version"
// implementation "androidx.compose.material:material-icons-extended:$compose_material_version"
// implementation "com.google.android.material:compose-theme-adapter:$material_adapter_version"
// debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
debugImplementation "androidx.fragment:fragment-testing:$fragment_test_version"
debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakcanary_version"
testImplementation "junit:junit:$junit_version"
testImplementation "org.mockito:mockito-core:$mockito_version"
testImplementation "org.robolectric:robolectric:$robolectric_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_test_version"
androidTestImplementation "androidx.test:core:$androidx_test_version"
androidTestImplementation "androidx.test:runner:$androidx_test_version"
androidTestImplementation "androidx.test:rules:$androidx_test_version"
androidTestImplementation "androidx.test.ext:junit:$androidx_junit_version"
androidTestImplementation "androidx.test.ext:truth:$androidx_test_version"
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
// androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
androidTestImplementation "org.mockito:mockito-android:$mockito_version"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_test_version"
}
+36
View File
@@ -0,0 +1,36 @@
import java.io.FileInputStream
import java.util.Properties
plugins {
alias(libs.plugins.convention.applicationPlugin)
}
// Release signing credentials live in local.properties (gitignored) or
// environment variables — never hardcode passwords in VCS.
val releaseProps = Properties().apply {
val propsFile = rootProject.file("local.properties")
if (propsFile.exists()) FileInputStream(propsFile).use { load(it) }
}
fun releaseCred(name: String): String =
releaseProps.getProperty(name) ?: System.getenv(name) ?: ""
android {
namespace = libs.versions.packageName.get()
defaultConfig {
applicationId = "cn.ba7opf.look4sat"
ndk { abiFilters.add("armeabi-v7a") }
}
signingConfigs {
create("release") {
storeFile = file(System.getProperty("user.home") + "/my-release-key.jks")
storePassword = releaseCred("RELEASE_STORE_PASSWORD")
keyAlias = releaseCred("RELEASE_KEY_ALIAS").ifEmpty { "look4sat" }
keyPassword = releaseCred("RELEASE_KEY_PASSWORD")
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
}
-11
View File
@@ -1,11 +0,0 @@
# Please add these rules to your existing keep rules in order to suppress warnings.
# This is generated automatically by the Android Gradle plugin.
-dontwarn org.bouncycastle.jsse.BCSSLParameters
-dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
-dontwarn org.conscrypt.Conscrypt$Version
-dontwarn org.conscrypt.Conscrypt
-dontwarn org.conscrypt.ConscryptHostnameVerifier
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
-dontwarn org.openjsse.net.ssl.OpenJSSE
@@ -1,198 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "834981e960b712254e56b633f3772115",
"entities": [
{
"tableName": "entries",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`comment` TEXT, `name` TEXT NOT NULL, `epoch` REAL NOT NULL, `meanmo` REAL NOT NULL, `eccn` REAL NOT NULL, `incl` REAL NOT NULL, `raan` REAL NOT NULL, `argper` REAL NOT NULL, `meanan` REAL NOT NULL, `catnum` INTEGER NOT NULL, `bstar` REAL NOT NULL, `xincl` REAL NOT NULL, `xnodeo` REAL NOT NULL, `omegao` REAL NOT NULL, `xmo` REAL NOT NULL, `xno` REAL NOT NULL, `isDeepspace` INTEGER NOT NULL, PRIMARY KEY(`catnum`))",
"fields": [
{
"fieldPath": "comment",
"columnName": "comment",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "data.name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "data.epoch",
"columnName": "epoch",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.meanmo",
"columnName": "meanmo",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.eccn",
"columnName": "eccn",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.incl",
"columnName": "incl",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.raan",
"columnName": "raan",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.argper",
"columnName": "argper",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.meanan",
"columnName": "meanan",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.catnum",
"columnName": "catnum",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "data.bstar",
"columnName": "bstar",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.xincl",
"columnName": "xincl",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.xnodeo",
"columnName": "xnodeo",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.omegao",
"columnName": "omegao",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.xmo",
"columnName": "xmo",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.xno",
"columnName": "xno",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.isDeepspace",
"columnName": "isDeepspace",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"catnum"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "radios",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `info` TEXT NOT NULL, `isAlive` INTEGER NOT NULL, `downlink` INTEGER, `uplink` INTEGER, `mode` TEXT, `isInverted` INTEGER NOT NULL, `catnum` INTEGER, `comment` TEXT, PRIMARY KEY(`uuid`))",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "info",
"columnName": "info",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlive",
"columnName": "isAlive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "downlink",
"columnName": "downlink",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "uplink",
"columnName": "uplink",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mode",
"columnName": "mode",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isInverted",
"columnName": "isInverted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "catnum",
"columnName": "catnum",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "comment",
"columnName": "comment",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"uuid"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '834981e960b712254e56b633f3772115')"
]
}
}
@@ -1,204 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "f43ab966fb7b0508057c359530582ff5",
"entities": [
{
"tableName": "entries",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`comment` TEXT, `name` TEXT NOT NULL, `epoch` REAL NOT NULL, `meanmo` REAL NOT NULL, `eccn` REAL NOT NULL, `incl` REAL NOT NULL, `raan` REAL NOT NULL, `argper` REAL NOT NULL, `meanan` REAL NOT NULL, `catnum` INTEGER NOT NULL, `bstar` REAL NOT NULL, `xincl` REAL NOT NULL, `xnodeo` REAL NOT NULL, `omegao` REAL NOT NULL, `xmo` REAL NOT NULL, `xno` REAL NOT NULL, `orbitalPeriod` REAL NOT NULL, `isDeepSpace` INTEGER NOT NULL, PRIMARY KEY(`catnum`))",
"fields": [
{
"fieldPath": "comment",
"columnName": "comment",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "data.name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "data.epoch",
"columnName": "epoch",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.meanmo",
"columnName": "meanmo",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.eccn",
"columnName": "eccn",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.incl",
"columnName": "incl",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.raan",
"columnName": "raan",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.argper",
"columnName": "argper",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.meanan",
"columnName": "meanan",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.catnum",
"columnName": "catnum",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "data.bstar",
"columnName": "bstar",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.xincl",
"columnName": "xincl",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.xnodeo",
"columnName": "xnodeo",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.omegao",
"columnName": "omegao",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.xmo",
"columnName": "xmo",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.xno",
"columnName": "xno",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.orbitalPeriod",
"columnName": "orbitalPeriod",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.isDeepSpace",
"columnName": "isDeepSpace",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"catnum"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "radios",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `info` TEXT NOT NULL, `isAlive` INTEGER NOT NULL, `downlink` INTEGER, `uplink` INTEGER, `mode` TEXT, `isInverted` INTEGER NOT NULL, `catnum` INTEGER, `comment` TEXT, PRIMARY KEY(`uuid`))",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "info",
"columnName": "info",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlive",
"columnName": "isAlive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "downlink",
"columnName": "downlink",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "uplink",
"columnName": "uplink",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mode",
"columnName": "mode",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isInverted",
"columnName": "isInverted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "catnum",
"columnName": "catnum",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "comment",
"columnName": "comment",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"uuid"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f43ab966fb7b0508057c359530582ff5')"
]
}
}
@@ -1,17 +0,0 @@
package com.rtbishop.look4sat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.rtbishop.bbctestapp", appContext.packageName)
}
}
+36 -8
View File
@@ -1,32 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:name=".presentation.MainApplication"
android:name=".MainApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Look4Sat.Main">
android:roundIcon="@mipmap/ic_launcher_round">
<activity
android:name=".presentation.MainActivity"
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/Theme.Look4Sat.SplashScreen">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- <intent-filter android:autoVerify="true">-->
<!-- <action android:name="android.intent.action.VIEW" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
<!-- <data android:scheme="https" />-->
<!-- <data android:host="github.com" />-->
<!-- <data android:pathPattern="/rt-bishop/Look4Sat/passes.*" />-->
<!-- </intent-filter>-->
</activity>
</application>
</manifest>
<meta-data
android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"
android:value="com.rtbishop.look4sat" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 22 KiB

@@ -0,0 +1,83 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat
import android.content.Context
import android.content.res.Configuration
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.Paint
import android.os.Bundle
import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
import com.rtbishop.look4sat.core.presentation.MainTheme
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun attachBaseContext(newBase: Context?) {
val config = Configuration(newBase?.resources?.configuration)
applyOverrideConfiguration(config.apply { fontScale = 1.0f })
super.attachBaseContext(newBase)
}
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
enableEdgeToEdge()
super.onCreate(savedInstanceState)
observeNightFilterState()
setContent {
MainTheme(isDarkTheme = true) { NavRoot() }
}
}
private fun observeNightFilterState() {
val mainContainer = (applicationContext as IContainerProvider).getMainContainer()
lifecycleScope.launch {
mainContainer.settingsRepo.otherSettings
.map { it.stateOfNightMode }
.distinctUntilChanged()
.collect { nightMode -> applyNightFilter(nightMode) }
}
}
private fun applyNightFilter(enabled: Boolean) {
if (enabled) {
val nightMatrix = ColorMatrix(
floatArrayOf(
1f, 0f, 0f, 0f, 0f, // R → R
0f, 0f, 0f, 0f, 0f, // G → 0
0f, 0f, 0f, 0f, 0f, // B → 0
0f, 0f, 0f, 1f, 0f // A → A
)
)
window.decorView.setLayerType(View.LAYER_TYPE_HARDWARE, Paint().apply {
colorFilter = ColorMatrixColorFilter(nightMatrix)
})
} else {
window.decorView.setLayerType(View.LAYER_TYPE_NONE, null)
}
}
}
@@ -0,0 +1,82 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat
import android.app.Activity
import android.app.Application
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
import com.rtbishop.look4sat.core.data.injection.MainContainer
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class MainApplication : Application(), IContainerProvider {
private lateinit var container: IMainContainer
override fun getMainContainer(): IMainContainer = container
override fun onCreate() {
super.onCreate()
container = MainContainer(this)
clearAmSatCacheOnAppBackground()
// trigger automatic update every 48 hours
container.appScope.launch { checkAutoUpdate() }
// load satellite data on every app start
container.appScope.launch { container.satelliteRepo.initRepository() }
}
private fun clearAmSatCacheOnAppBackground() {
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
private var startedActivityCount = 0
override fun onActivityStarted(activity: Activity) {
if (startedActivityCount == 0) {
container.appScope.launch { container.amSatRepo.prefetchStatus() }
}
startedActivityCount += 1
}
override fun onActivityStopped(activity: Activity) {
startedActivityCount = (startedActivityCount - 1).coerceAtLeast(0)
if (startedActivityCount == 0) container.amSatRepo.clearStatusCache()
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
override fun onActivityResumed(activity: Activity) = Unit
override fun onActivityPaused(activity: Activity) = Unit
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
override fun onActivityDestroyed(activity: Activity) = Unit
})
}
private suspend fun checkAutoUpdate(timeNow: Long = System.currentTimeMillis()) {
if (container.settingsRepo.otherSettings.value.stateOfAutoUpdate) {
val timeDelta = timeNow - container.settingsRepo.databaseState.value.updateTimestamp
if (timeDelta > 172_800_000L) { // 48 hours in ms
val sdf = SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault())
println("Started periodic data update on ${sdf.format(Date())}")
container.databaseRepo.updateFromRemote()
}
}
}
}
@@ -0,0 +1,313 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
import com.rtbishop.look4sat.core.domain.repository.MutualPassData
import com.rtbishop.look4sat.core.presentation.DeeplinkResolver
import com.rtbishop.look4sat.core.presentation.ElevationThresholds
import com.rtbishop.look4sat.core.presentation.LocalElevationThresholds
import com.rtbishop.look4sat.core.presentation.RadarDestination
import com.rtbishop.look4sat.core.presentation.Screen
import com.rtbishop.look4sat.core.presentation.hasEnoughHeight
import com.rtbishop.look4sat.core.presentation.hasEnoughWidth
import com.rtbishop.look4sat.feature.map.MapDestination
import com.rtbishop.look4sat.feature.mutual.MutualScreen
import com.rtbishop.look4sat.feature.mutual.MutualViewModel
import com.rtbishop.look4sat.feature.passes.PassesDestination
import com.rtbishop.look4sat.feature.radar.RadarDestination
import com.rtbishop.look4sat.feature.satellites.SatellitesDestination
import com.rtbishop.look4sat.feature.settings.SettingsDestination
import com.rtbishop.look4sat.feature.status.SatStatusDestination
@Composable
fun NavRoot(deeplink: String? = null) {
val rootBackStack = rememberNavBackStack(Screen.Passes)
val deeplinkResolver = DeeplinkResolver()
var openMapRequest by remember { mutableIntStateOf(0) }
LaunchedEffect(deeplink) {
deeplink?.let { rootBackStack.add(deeplinkResolver.resolve(it)) }
}
val navigateBack: () -> Unit = { rootBackStack.removeLastOrNull() }
val navigateToRadar: () -> Unit = { rootBackStack.add(RadarDestination) }
val navigateToMap: () -> Unit = {
while (rootBackStack.size > 1) rootBackStack.removeAt(rootBackStack.size - 1)
openMapRequest += 1
}
// Incoming screen slides in from the right, outgoing drifts left at 1/3 speed (API35+ style)
val pushTransition = slideInHorizontally(tween(300)) { it } togetherWith
slideOutHorizontally(tween(300)) { -it / 3 }
// Reverse: outgoing slides out to the right, incoming drifts in from the left
val popTransition = slideInHorizontally(tween(300)) { -it / 3 } togetherWith
slideOutHorizontally(tween(300)) { it }
NavDisplay(
modifier = Modifier.fillMaxSize(),
backStack = rootBackStack,
onBack = navigateBack,
transitionSpec = { pushTransition },
popTransitionSpec = { popTransition },
predictivePopTransitionSpec = { popTransition },
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider {
entry<Screen.Passes> {
MainScreen(
navigateToRadar = navigateToRadar,
openMapRequest = openMapRequest,
onOpenMapRequestHandled = { openMapRequest = 0 }
)
}
entry<RadarDestination> {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
RadarDestination(navigateUp = navigateBack, navigateToMap = navigateToMap)
}
}
}
)
}
@Composable
fun MainScreen(
navigateToRadar: () -> Unit = {},
openMapRequest: Int = 0,
onOpenMapRequestHandled: () -> Unit = {}
) {
val backStack = rememberNavBackStack(Screen.Passes)
val currentKey = backStack.lastOrNull()
val navigateBack: () -> Unit = { backStack.removeLastOrNull() }
val navigateToMap: () -> Unit = {
while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
backStack.add(Screen.Map)
}
LaunchedEffect(openMapRequest) {
if (openMapRequest > 0) {
navigateToMap()
onOpenMapRequestHandled()
}
}
val fadeTransition = fadeIn(animationSpec = tween(350)) togetherWith fadeOut(animationSpec = tween(350))
val navItems = listOf(Screen.Satellites, Screen.Passes, Screen.AMSAT, Screen.Mutual, Screen.Settings)
val context = LocalContext.current
val container = (context.applicationContext as IContainerProvider).getMainContainer()
val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle()
// Activity-scoped so the mutual query results survive navigation to Radar and back
val mutualViewModel: MutualViewModel = viewModel(
viewModelStoreOwner = context as ViewModelStoreOwner,
factory = MutualViewModel.factory(container)
)
CompositionLocalProvider(
LocalElevationThresholds provides ElevationThresholds(
low = otherSettings.lowElevation,
high = otherSettings.highElevation
)
) {
NavigationSuiteScaffold(
navigationSuiteItems = {
navItems.forEach { screen ->
val isSelected = when (currentKey) {
is Screen.Satellites -> screen is Screen.Satellites
is Screen.Passes -> screen is Screen.Passes
is Screen.Map -> screen is Screen.Passes
is Screen.AMSAT -> screen is Screen.AMSAT
is Screen.Mutual -> screen is Screen.Mutual
is Screen.Settings -> screen is Screen.Settings
else -> false
}
item(
icon = { Icon(painterResource(screen.iconResId), stringResource(screen.titleResId)) },
label = { Text(stringResource(screen.titleResId)) },
selected = isSelected,
onClick = {
if (isSelected && !(currentKey is Screen.Map && screen is Screen.Passes)) return@item
while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
if (screen !is Screen.Passes) backStack.add(screen)
}
)
}
},
navigationSuiteColors = NavigationSuiteDefaults.colors(
navigationRailContainerColor = MaterialTheme.colorScheme.surfaceContainer
),
layoutType = when {
!hasEnoughHeight() && hasEnoughWidth() -> NavigationSuiteType.NavigationRail
!hasEnoughWidth() -> NavigationSuiteType.ShortNavigationBarCompact
else -> NavigationSuiteType.ShortNavigationBarMedium
}
) {
Column {
NavDisplay(
backStack = backStack,
modifier = Modifier.weight(1f),
onBack = navigateBack,
transitionSpec = { fadeTransition },
popTransitionSpec = { fadeTransition },
predictivePopTransitionSpec = { fadeTransition },
entryDecorators = listOf(
// Required for saving Compose state per entry
rememberSaveableStateHolderNavEntryDecorator(),
// Required for ViewModel scoping per entry
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider {
entry<Screen.Satellites> {
SatellitesDestination(navigateUp = navigateBack)
}
entry<Screen.Passes> {
PassesDestination(
navigateToRadar = { catNum, aosTime ->
container.setMutualPassData(MutualPassData())
container.satelliteRepo.selectPass(catNum, aosTime)
navigateToRadar()
},
navigateToMap = navigateToMap
)
}
entry<Screen.Map> {
MapDestination()
}
entry<Screen.Mutual> {
MutualScreen(
viewModel = mutualViewModel,
navigateToRadar = { catNum, aosTime, pass ->
container.setMutualPassData(pass ?: MutualPassData())
container.satelliteRepo.selectPass(catNum, aosTime)
navigateToRadar()
}
)
}
entry<Screen.AMSAT> { SatStatusDestination() }
entry<Screen.Settings> {
SettingsDestination()
}
}
)
// Radio tracking status banner
if (trackingState.isActive) {
val infiniteTransition = rememberInfiniteTransition(label = "trackingPulse")
val alpha by infiniteTransition.animateFloat(
initialValue = 1f, targetValue = 0.4f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
), label = "pulseAlpha"
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primaryContainer)
.clickable {
val pass = trackingState.currentPass
if (pass != null) {
container.setMutualPassData(MutualPassData())
container.satelliteRepo.selectPass(pass.catNum, pass.aosTime)
navigateToRadar()
}
}
.padding(horizontal = 12.dp, vertical = 6.dp)
) {
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(Color(0xFF4CAF50).copy(alpha = alpha))
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Tracking: ${trackingState.currentPass?.name ?: ""}",
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.weight(1f)
)
val txOk = if (trackingState.txConnected) "TX" else ""
val rxOk = if (trackingState.rxConnected) "RX" else ""
Text(
text = listOf(txOk, rxOk).filter { it.isNotBlank() }.joinToString("/"),
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
}
}
}
}
@@ -1,139 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import androidx.core.content.ContextCompat
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.ILocationManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.utility.QthConverter
import com.rtbishop.look4sat.utility.round
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LocationManager @Inject constructor(
@ApplicationContext private val context: Context,
private val manager: LocationManager,
private val settings: ISettingsManager,
) : LocationListener, ILocationManager {
private val providerDef = LocationManager.PASSIVE_PROVIDER
private val providerNet = LocationManager.NETWORK_PROVIDER
private val providerGps = LocationManager.GPS_PROVIDER
private val locationCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
private val locationFine = Manifest.permission.ACCESS_FINE_LOCATION
private val _stationPosition = MutableStateFlow<DataState<GeoPos>>(DataState.Handled)
private var currentLocator = settings.loadStationLocator()
private var currentPosition = settings.loadStationPosition()
override val stationPosition: StateFlow<DataState<GeoPos>> = _stationPosition
override fun getStationLocator(): String = currentLocator
override fun getStationPosition(): GeoPos = currentPosition
override fun setStationPosition(latitude: Double, longitude: Double) {
if (QthConverter.isValidPosition(latitude, longitude)) {
val tempLon = if (longitude > 180.0) longitude - 180 else longitude
val newLat = latitude.round(4)
val newLon = tempLon.round(4)
QthConverter.positionToQth(newLat, newLon)?.let { locator ->
currentLocator = locator
currentPosition = GeoPos(newLat, newLon)
settings.saveStationLocator(locator)
settings.saveStationPosition(currentPosition)
_stationPosition.value = DataState.Success(currentPosition)
}
} else {
_stationPosition.value =
DataState.Error(context.getString(R.string.location_manual_error))
}
}
override fun setPositionFromGps() {
val result = ContextCompat.checkSelfPermission(context, locationFine)
if (manager.isProviderEnabled(providerGps) && result == PackageManager.PERMISSION_GRANTED) {
val location = manager.getLastKnownLocation(providerDef)
if (location != null) {
setStationPosition(location.latitude, location.longitude)
} else {
_stationPosition.value = DataState.Loading
manager.requestLocationUpdates(providerGps, 0L, 0f, this)
}
} else {
_stationPosition.value = DataState.Error(context.getString(R.string.location_null))
}
}
override fun setPositionFromNet() {
val result = ContextCompat.checkSelfPermission(context, locationCoarse)
if (manager.isProviderEnabled(providerNet) && result == PackageManager.PERMISSION_GRANTED) {
val location = manager.getLastKnownLocation(providerDef)
if (location != null) {
setStationPosition(location.latitude, location.longitude)
} else {
_stationPosition.value = DataState.Loading
manager.requestLocationUpdates(providerNet, 0L, 0f, this)
}
} else {
_stationPosition.value = DataState.Error(context.getString(R.string.location_null))
}
}
override fun setPositionFromQth(locator: String) {
val position = QthConverter.qthToPosition(locator)
if (position != null) {
currentLocator = locator
currentPosition = position
settings.saveStationLocator(locator)
settings.saveStationPosition(currentPosition)
_stationPosition.value = DataState.Success(currentPosition)
} else {
_stationPosition.value = DataState.Error(context.getString(R.string.location_qth_error))
}
}
override fun setPositionHandled() {
_stationPosition.value = DataState.Handled
}
override fun onLocationChanged(location: Location) {
manager.removeUpdates(this)
setStationPosition(location.latitude, location.longitude)
}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
@Deprecated("Deprecated in Java")
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
}
@@ -1,79 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import com.rtbishop.look4sat.domain.predict.RAD2DEG
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.round
@Singleton
class OrientationManager @Inject constructor(private val sensorManager: SensorManager) :
SensorEventListener {
interface OrientationListener {
fun onOrientationChanged(azimuth: Float, pitch: Float, roll: Float)
}
private val sensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
private val rotationMatrix = FloatArray(9)
private val orientationValues = FloatArray(3)
private var sensorAccuracy: Int = SensorManager.SENSOR_STATUS_UNRELIABLE
private var orientationListener: OrientationListener? = null
fun startListening(listener: OrientationListener) {
if (sensor != null && orientationListener !== listener) {
orientationListener = listener
sensorManager.registerListener(this, sensor, 16000)
} else return
}
fun stopListening() {
sensorManager.unregisterListener(this)
orientationListener = null
}
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
sensorAccuracy = accuracy
}
override fun onSensorChanged(event: SensorEvent) {
when {
orientationListener == null -> return
sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE -> return
event.sensor == sensor -> updateOrientation(event.values)
}
}
private fun updateOrientation(rotationVector: FloatArray) {
SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector)
SensorManager.getOrientation(rotationMatrix, orientationValues)
val azimuth = (orientationValues[0] * RAD2DEG).toFloat()
val pitch = (orientationValues[1] * RAD2DEG).toFloat()
val roll = (orientationValues[2] * RAD2DEG).toFloat()
val magneticAzimuth = (azimuth + 360f) % 360f
val roundedAzimuth = round(magneticAzimuth * 10) / 10
val roundedPitch = round(pitch * 10) / 10
val roundedRoll = round(roll * 10) / 10
orientationListener?.onOrientationChanged(roundedAzimuth, roundedPitch, roundedRoll)
}
}
@@ -1,221 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework
import android.content.SharedPreferences
import androidx.core.content.edit
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.predict.GeoPos
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SettingsManager @Inject constructor(private val prefs: SharedPreferences) : ISettingsManager {
companion object {
const val keyModes = "satModes"
const val keyCompass = "compass"
const val keyRadarSweep = "radarSweep"
const val keyTimeUTC = "timeUTC"
const val keyLastUpdateTime = "lastUpdateTime"
const val keyAutoUpdateEnabled = "autoUpdateEnabled"
const val keyHoursAhead = "hoursAhead"
const val keyMinElevation = "minElevation"
const val keyRotator = "isRotatorEnabled"
const val keyRotatorAddress = "rotatorAddress"
const val keyRotatorPort = "rotatorPort"
const val keyBTEnabled = "isBTEnabled"
const val keyBTDeviceName = "BTDeviceName"
const val keyBTDeviceAddr = "BTDeviceAddr"
const val keyBTFormat = "BTFormat"
const val keyLatitude = "stationLat"
const val keyLongitude = "stationLon"
const val keyLocator = "stationQTH"
const val keySelection = "selection"
}
override fun loadStationLocator(): String {
return prefs.getString(keyLocator, null) ?: "null"
}
override fun saveStationLocator(locator: String) {
prefs.edit { putString(keyLocator, locator) }
}
override fun loadStationPosition(): GeoPos {
val defaultSP = "0.0"
val latitude = prefs.getString(keyLatitude, null) ?: defaultSP
val longitude = prefs.getString(keyLongitude, null) ?: defaultSP
return GeoPos(latitude.toDouble(), longitude.toDouble())
}
override fun saveStationPosition(position: GeoPos) {
prefs.edit {
putString(keyLatitude, position.lat.toString())
putString(keyLongitude, position.lon.toString())
}
}
override fun saveEntriesSelection(catnums: List<Int>) {
val stringList = catnums.map { catnum -> catnum.toString() }
prefs.edit { putStringSet(keySelection, stringList.toSet()) }
}
override fun loadEntriesSelection(): List<Int> {
val catnums = prefs.getStringSet(keySelection, emptySet())?.map { catnum -> catnum.toInt() }
return catnums?.sorted() ?: emptyList()
}
override fun saveSatType(type: String, catnums: List<Int>) {
val stringList = catnums.map { catnum -> catnum.toString() }
prefs.edit { putStringSet("type$type", stringList.toSet()) }
}
override fun loadSatType(type: String): List<Int> {
val catnums = prefs.getStringSet("type$type", emptySet())?.map { catnum -> catnum.toInt() }
return catnums ?: emptyList()
}
override fun getHoursAhead(): Int {
return prefs.getInt(keyHoursAhead, 24)
}
override fun setHoursAhead(hoursAhead: Int) {
prefs.edit { putInt(keyHoursAhead, hoursAhead) }
}
override fun getMinElevation(): Double {
return prefs.getDouble(keyMinElevation, 16.0)
}
override fun setMinElevation(minElevation: Double) {
prefs.edit { putDouble(keyMinElevation, minElevation) }
}
override fun getUseUTC(): Boolean {
return prefs.getBoolean(keyTimeUTC, false)
}
override fun setUseUTC(value: Boolean) {
prefs.edit { putBoolean(keyTimeUTC, value) }
}
override fun getLastUpdateTime(): Long {
return prefs.getLong(keyLastUpdateTime, 0L)
}
override fun setLastUpdateTime(updateTime: Long) {
prefs.edit { putLong(keyLastUpdateTime, updateTime) }
}
override fun getAutoUpdateEnabled(): Boolean {
return prefs.getBoolean(keyAutoUpdateEnabled, true)
}
override fun setAutoUpdateEnabled(value: Boolean) {
prefs.edit { putBoolean(keyAutoUpdateEnabled, value) }
}
override fun getUseCompass(): Boolean {
return prefs.getBoolean(keyCompass, true)
}
override fun setUseCompass(value: Boolean) {
prefs.edit { putBoolean(keyCompass, value) }
}
override fun getShowSweep(): Boolean {
return prefs.getBoolean(keyRadarSweep, true)
}
override fun setShowSweep(value: Boolean) {
prefs.edit { putBoolean(keyRadarSweep, value) }
}
override fun saveModesSelection(modes: List<String>) {
prefs.edit { putStringSet(keyModes, modes.toSet()) }
}
override fun loadModesSelection(): List<String> {
return prefs.getStringSet(keyModes, null)?.toList()?.sorted() ?: emptyList()
}
override fun getRotatorEnabled(): Boolean {
return prefs.getBoolean(keyRotator, false)
}
override fun setRotatorEnabled(value: Boolean) {
prefs.edit { putBoolean(keyRotator, value) }
}
override fun getRotatorServer(): String {
return prefs.getString(keyRotatorAddress, null) ?: "127.0.0.1"
}
override fun setRotatorServer(value: String) {
prefs.edit { putString(keyRotatorAddress, value) }
}
override fun getRotatorPort(): String {
return prefs.getString(keyRotatorPort, null) ?: "4533"
}
override fun setRotatorPort(value: String) {
prefs.edit { putString(keyRotatorPort, value) }
}
override fun getBTEnabled(): Boolean {
return prefs.getBoolean(keyBTEnabled, false)
}
override fun setBTEnabled(value: Boolean) {
prefs.edit { putBoolean(keyBTEnabled, value) }
}
override fun getBTDeviceAddr(): String {
return prefs.getString(keyBTDeviceAddr, null) ?: "00:0C:BF:13:80:5D"
}
override fun setBTDeviceAddr(value: String) {
prefs.edit { putString(keyBTDeviceAddr, value) }
}
override fun getBTDeviceName(): String {
return prefs.getString(keyBTDeviceName, null) ?: "Default"
}
override fun setBTDeviceName(value: String) {
prefs.edit { putString(keyBTDeviceName, value) }
}
override fun getBTFormat(): String {
return prefs.getString(keyBTFormat, null) ?: "W\$AZ \$EL"
}
override fun setBTFormat(value: String) {
prefs.edit { putString(keyBTFormat, value) }
}
private fun SharedPreferences.getDouble(key: String, default: Double): Double {
return Double.fromBits(getLong(key, default.toRawBits()))
}
private fun SharedPreferences.Editor.putDouble(key: String, double: Double) {
putLong(key, double.toRawBits())
}
}
@@ -1,36 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.data
import android.content.ContentResolver
import android.net.Uri
import com.rtbishop.look4sat.domain.data.IFileDataSource
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import java.io.InputStream
class FileDataSource(
private val contentResolver: ContentResolver,
private val ioDispatcher: CoroutineDispatcher
) : IFileDataSource {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun getDataStream(uri: String): InputStream? {
return withContext(ioDispatcher) { contentResolver.openInputStream(Uri.parse(uri)) }
}
}
@@ -1,44 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.data
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.rtbishop.look4sat.framework.data.dao.EntriesDao
import com.rtbishop.look4sat.framework.data.dao.RadiosDao
import com.rtbishop.look4sat.framework.model.SatEntry
import com.rtbishop.look4sat.framework.model.SatRadio
@Database(entities = [SatEntry::class, SatRadio::class], version = 2, exportSchema = true)
abstract class LocalDatabase : RoomDatabase() {
abstract fun entriesDao(): EntriesDao
abstract fun radiosDao(): RadiosDao
}
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE entries_backup (name TEXT NOT NULL, epoch REAL NOT NULL, meanmo REAL NOT NULL, eccn REAL NOT NULL, incl REAL NOT NULL, raan REAL NOT NULL, argper REAL NOT NULL, meanan REAL NOT NULL, catnum INTEGER NOT NULL, bstar REAL NOT NULL, xincl REAL NOT NULL, xnodeo REAL NOT NULL, omegao REAL NOT NULL, xmo REAL NOT NULL, xno REAL NOT NULL, orbitalPeriod REAL NOT NULL, isDeepSpace INTEGER NOT NULL, comment TEXT, PRIMARY KEY(catnum))")
database.execSQL("INSERT INTO entries_backup (name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar, xincl, xnodeo, omegao, xmo, xno, orbitalPeriod, isDeepSpace, comment) SELECT name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar, xincl, xnodeo, omegao, xmo, xno, 1440 / meanmo, 1440 / meanmo >= 225.0, comment FROM entries")
database.execSQL("DROP TABLE entries")
database.execSQL("ALTER TABLE entries_backup RENAME TO entries")
}
}
@@ -1,58 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.data
import com.rtbishop.look4sat.domain.data.ILocalEntrySource
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.framework.data.dao.EntriesDao
import com.rtbishop.look4sat.domain.model.SatEntry as DomainEntry
import com.rtbishop.look4sat.domain.model.SatItem as DomainItem
import com.rtbishop.look4sat.framework.model.SatEntry as FrameworkEntry
import com.rtbishop.look4sat.framework.model.SatItem as FrameworkItem
class LocalEntrySource(private val entriesDao: EntriesDao) : ILocalEntrySource {
override fun getEntriesTotal() = entriesDao.getEntriesTotal()
override suspend fun getEntriesWithModes(): List<DomainItem> {
return entriesDao.getEntriesWithModes().toDomainItems()
}
override suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite> {
val selectedSatellites = mutableListOf<Satellite>()
ids.chunked(999).forEach { idsPart ->
val entries = entriesDao.getEntriesWithIds(idsPart)
selectedSatellites.addAll(entries.map { entry -> entry.data.getSatellite() })
}
return selectedSatellites
}
override suspend fun insertEntries(entries: List<DomainEntry>) {
entriesDao.insertEntries(entries.toFrameworkEntries())
}
override suspend fun deleteEntries() = entriesDao.deleteEntries()
private fun DomainEntry.toFramework() = FrameworkEntry(this.data, this.comment)
private fun FrameworkItem.toDomain() = DomainItem(this.catnum, this.name, this.modes, false)
private fun List<DomainEntry>.toFrameworkEntries() = this.map { entry -> entry.toFramework() }
private fun List<FrameworkItem>.toDomainItems() = this.map { item -> item.toDomain() }
}
@@ -1,53 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.data
import com.rtbishop.look4sat.domain.model.SatRadio as DomainRadio
import com.rtbishop.look4sat.framework.model.SatRadio as FrameworkRadio
import com.rtbishop.look4sat.domain.data.ILocalRadioSource
import com.rtbishop.look4sat.framework.data.dao.RadiosDao
class LocalRadioSource(private val radiosDao: RadiosDao) : ILocalRadioSource {
override fun getRadiosTotal() = radiosDao.getRadiosTotal()
override suspend fun getRadiosWithId(id: Int): List<DomainRadio> {
return radiosDao.getRadiosWithId(id).toDomainRadios()
}
override suspend fun insertRadios(radios: List<DomainRadio>) {
radiosDao.deleteRadios()
radiosDao.insertRadios(radios.toFrameworkRadios())
}
override suspend fun deleteRadios() = radiosDao.deleteRadios()
private fun DomainRadio.toFramework() = FrameworkRadio(
this.uuid, this.info, this.isAlive, this.downlink, this.uplink,
this.mode, this.isInverted, this.catnum, this.comment
)
private fun FrameworkRadio.toDomain() = DomainRadio(
this.uuid, this.info, this.isAlive, this.downlink, this.uplink,
this.mode, this.isInverted, this.catnum, this.comment
)
private fun List<DomainRadio>.toFrameworkRadios() = this.map { radio -> radio.toFramework() }
private fun List<FrameworkRadio>.toDomainRadios() = this.map { radio -> radio.toDomain() }
}
@@ -1,39 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.data
import com.rtbishop.look4sat.domain.data.IRemoteDataSource
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.InputStream
class RemoteDataSource(
private val httpClient: OkHttpClient, private val ioDispatcher: CoroutineDispatcher
) : IRemoteDataSource {
override suspend fun getDataStream(url: String): InputStream? = withContext(ioDispatcher) {
try {
val request = Request.Builder().url(url).build()
httpClient.newCall(request).execute().body?.byteStream()
} catch (exception: Exception) {
null
}
}
}
@@ -1,44 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.data.dao
import androidx.room.*
import com.rtbishop.look4sat.framework.model.SatEntry
import com.rtbishop.look4sat.framework.model.SatItem
import kotlinx.coroutines.flow.Flow
@Dao
interface EntriesDao {
@Query("SELECT COUNT(*) FROM entries")
fun getEntriesTotal(): Flow<Int>
@Transaction
@Query("SELECT catnum, name FROM entries ORDER BY name ASC")
suspend fun getEntriesWithModes(): List<SatItem>
@Transaction
@Query("SELECT * FROM entries WHERE catnum IN (:selectedIds)")
suspend fun getEntriesWithIds(selectedIds: List<Int>): List<SatEntry>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertEntries(entries: List<SatEntry>)
@Query("DELETE FROM entries")
suspend fun deleteEntries()
}
@@ -1,39 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.data.dao
import androidx.room.*
import com.rtbishop.look4sat.framework.model.SatRadio
import kotlinx.coroutines.flow.Flow
@Dao
interface RadiosDao {
@Query("SELECT COUNT(*) FROM radios")
fun getRadiosTotal(): Flow<Int>
@Transaction
@Query("SELECT * FROM radios WHERE catnum = :id AND isAlive = 1")
suspend fun getRadiosWithId(id: Int): List<SatRadio>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRadios(radios: List<SatRadio>)
@Query("DELETE FROM radios")
suspend fun deleteRadios()
}
@@ -1,20 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.model
data class DataSource(var url: String = String())
@@ -1,25 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.model
import androidx.room.Embedded
import androidx.room.Entity
import com.rtbishop.look4sat.domain.predict.OrbitalData
@Entity(tableName = "entries", primaryKeys = ["catnum"])
data class SatEntry(@Embedded val data: OrbitalData, var comment: String? = null)
@@ -1,32 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.model
import androidx.room.Relation
data class SatItem(
val catnum: Int,
val name: String,
@Relation(
parentColumn = "catnum",
entity = SatRadio::class,
entityColumn = "catnum",
projection = ["mode"]
)
val modes: List<String>
)
@@ -1,34 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.model
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "radios")
data class SatRadio(
@PrimaryKey val uuid: String,
val info: String,
val isAlive: Boolean,
var downlink: Long?,
var uplink: Long?,
val mode: String?,
val isInverted: Boolean,
val catnum: Int?,
var comment: String? = null
)
@@ -1,59 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.injection
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.SharedPreferences
import android.hardware.SensorManager
import android.location.LocationManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideBluetoothManager(@ApplicationContext context: Context): BluetoothManager {
return context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
}
@Provides
@Singleton
fun provideLocationManager(@ApplicationContext context: Context): LocationManager {
return context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
}
@Provides
@Singleton
fun provideSensorManager(@ApplicationContext context: Context): SensorManager {
return context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
}
@Provides
@Singleton
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
return context.getSharedPreferences("default", Context.MODE_PRIVATE)
}
}
@@ -1,93 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.injection
import android.bluetooth.BluetoothManager
import android.content.Context
import androidx.room.Room
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ILocationManager
import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.data.DataRepository
import com.rtbishop.look4sat.domain.predict.SatelliteManager
import com.rtbishop.look4sat.framework.LocationManager
import com.rtbishop.look4sat.framework.SettingsManager
import com.rtbishop.look4sat.framework.data.FileDataSource
import com.rtbishop.look4sat.framework.data.LocalDatabase
import com.rtbishop.look4sat.framework.data.LocalEntrySource
import com.rtbishop.look4sat.framework.data.LocalRadioSource
import com.rtbishop.look4sat.framework.data.MIGRATION_1_2
import com.rtbishop.look4sat.framework.data.RemoteDataSource
import com.rtbishop.look4sat.presentation.radarScreen.BTReporter
import com.rtbishop.look4sat.utility.DataParser
import com.rtbishop.look4sat.utility.DataReporter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import okhttp3.OkHttpClient
@Module
@InstallIn(SingletonComponent::class)
object BaseModule {
@Provides
@Singleton
fun provideDataRepository(
@ApplicationContext context: Context,
settings: ISettingsManager
): IDataRepository {
val db = Room.databaseBuilder(context, LocalDatabase::class.java, "Look4SatDb")
.addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build()
val parser = DataParser(Dispatchers.Default)
val fileSource = FileDataSource(context.contentResolver, Dispatchers.IO)
val entries = LocalEntrySource(db.entriesDao())
val radios = LocalRadioSource(db.radiosDao())
val remoteSource = RemoteDataSource(OkHttpClient.Builder().build(), Dispatchers.IO)
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
return DataRepository(parser, fileSource, entries, radios, remoteSource, scope, settings)
}
@Provides
@Singleton
fun provideDataReporter(): DataReporter = DataReporter(CoroutineScope(Dispatchers.IO))
@Provides
@Singleton
fun provideBTReporter(manager: BluetoothManager): BTReporter {
return BTReporter(manager, CoroutineScope(Dispatchers.IO))
}
@Provides
@Singleton
fun provideLocationManager(manager: LocationManager): ILocationManager = manager
@Provides
@Singleton
fun provideSatelliteManager(): ISatelliteManager = SatelliteManager(Dispatchers.Default)
@Provides
@Singleton
fun provideSettingsManager(manager: SettingsManager): ISettingsManager = manager
}
@@ -1,84 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Bundle
import android.os.SystemClock
import android.view.View
import androidx.annotation.IdRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.navigation.fragment.findNavController
import com.rtbishop.look4sat.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
installSplashScreen()
super.onCreate(savedInstanceState)
setContentView(ActivityMainBinding.inflate(layoutInflater).root)
}
override fun attachBaseContext(newBase: Context?) {
val newConfig = Configuration(newBase?.resources?.configuration)
newConfig.fontScale = 1.0f
applyOverrideConfiguration(newConfig)
super.attachBaseContext(newBase)
}
}
fun <T> Fragment.getNavResult(@IdRes id: Int, key: String, onResult: (result: T) -> Unit) {
val backStackEntry = findNavController().getBackStackEntry(id)
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME && backStackEntry.savedStateHandle.contains(key)) {
backStackEntry.savedStateHandle.get<T>(key)?.let(onResult)
backStackEntry.savedStateHandle.remove<T>(key)
}
}
backStackEntry.lifecycle.addObserver(observer)
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) backStackEntry.lifecycle.removeObserver(observer)
})
}
fun <T> Fragment.setNavResult(key: String, value: T) {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, value)
}
fun View.clickWithDebounce(debounceTime: Long = 875L, action: () -> Unit) {
this.setOnClickListener(object : View.OnClickListener {
private var lastClickTime: Long = 0
override fun onClick(v: View) {
if (SystemClock.elapsedRealtime() - lastClickTime < debounceTime) return
else action()
lastClickTime = SystemClock.elapsedRealtime()
}
})
}
@@ -1,53 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation
import android.app.Application
import android.util.Log
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.framework.SettingsManager
import dagger.hilt.android.HiltAndroidApp
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
@HiltAndroidApp
class MainApplication : Application() {
@Inject
lateinit var repository: IDataRepository
@Inject
lateinit var settingsManager: SettingsManager
override fun onCreate() {
super.onCreate()
checkAutoUpdate()
}
private fun checkAutoUpdate() {
if (settingsManager.getAutoUpdateEnabled()) {
val timeDelta = System.currentTimeMillis() - settingsManager.getLastUpdateTime()
if (timeDelta > 172800000) { // 48 hours in ms
val sdf = SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault())
Log.d("AutoUpdate", "Started periodic data update on ${sdf.format(Date())}")
repository.updateFromWeb()
}
}
}
}
@@ -1,78 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.entriesScreen
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.databinding.ItemEntryBinding
import com.rtbishop.look4sat.domain.model.SatItem
class EntriesAdapter(private val clickListener: EntriesClickListener) :
RecyclerView.Adapter<EntriesAdapter.SatItemHolder>() {
private val diffCallback = object : DiffUtil.ItemCallback<SatItem>() {
override fun areItemsTheSame(oldItem: SatItem, newItem: SatItem): Boolean {
return oldItem.catnum == newItem.catnum
}
override fun areContentsTheSame(oldItem: SatItem, newItem: SatItem): Boolean {
return oldItem.isSelected == newItem.isSelected
}
}
private val differ = AsyncListDiffer(this, diffCallback)
fun submitList(items: List<SatItem>) {
differ.submitList(items)
}
override fun getItemCount(): Int = differ.currentList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SatItemHolder {
return SatItemHolder.from(parent)
}
override fun onBindViewHolder(holder: SatItemHolder, position: Int) {
holder.bind(differ.currentList[position], clickListener)
}
class SatItemHolder private constructor(private val binding: ItemEntryBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: SatItem, listener: EntriesClickListener) {
binding.entryCheckbox.text = item.name
binding.entryCheckbox.isChecked = item.isSelected
binding.entryCheckbox.setOnClickListener {
listener.updateSelection(listOf(item.catnum), item.isSelected.not())
}
}
companion object {
fun from(parent: ViewGroup): SatItemHolder {
val inflater = LayoutInflater.from(parent.context)
return SatItemHolder(ItemEntryBinding.inflate(inflater, parent, false))
}
}
}
interface EntriesClickListener {
fun updateSelection(catNums: List<Int>, isSelected: Boolean)
}
}
@@ -1,117 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.entriesScreen
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.FragmentEntriesBinding
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.presentation.clickWithDebounce
import com.rtbishop.look4sat.presentation.getNavResult
import com.rtbishop.look4sat.presentation.setNavResult
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class EntriesFragment : Fragment(R.layout.fragment_entries) {
private val viewModel: EntriesViewModel by viewModels()
private lateinit var binding: FragmentEntriesBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val entriesAdapter = EntriesAdapter(viewModel)
binding = FragmentEntriesBinding.bind(view).apply {
entriesRecycler.apply {
setHasFixedSize(true)
adapter = entriesAdapter
layoutManager = GridLayoutManager(requireContext(), 2)
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
addItemDecoration(DividerItemDecoration(requireContext(), 1))
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy > 0 && entriesFab.visibility == View.VISIBLE) entriesFab.hide()
else if (dy < 0 && entriesFab.visibility != View.VISIBLE) entriesFab.show()
}
})
}
entriesBtnBack.clickWithDebounce { findNavController().navigateUp() }
entriesSearch.doOnTextChanged { text, _, _, _ -> viewModel.setQuery(text.toString()) }
entriesBtnModes.clickWithDebounce {
val direction = EntriesFragmentDirections.entriesToModes()
findNavController().navigate(direction)
}
entriesBtnSelect.clickWithDebounce { viewModel.selectCurrentItems(true) }
entriesBtnClear.clickWithDebounce { viewModel.selectCurrentItems(false) }
val typeMessageFormat = requireContext().getString(R.string.types_message)
val type = if (viewModel.getSatType().isNullOrBlank()) "All" else viewModel.getSatType()
entriesTypeMessage.text = String.format(typeMessageFormat, type)
entriesTypeCard.setOnClickListener { showSelectTypeDialog() }
}
viewModel.satData.observe(viewLifecycleOwner) { satData ->
handleSatData(satData, entriesAdapter)
binding.entriesFab.clickWithDebounce {
setNavResult("selection", viewModel.saveSelection())
findNavController().popBackStack()
}
}
getNavResult<List<String>>(R.id.nav_entries, "modes") { modes ->
viewModel.saveSelectedModes(modes)
}
}
private fun handleSatData(dataState: DataState<List<SatItem>>, entriesAdapter: EntriesAdapter) {
when (dataState) {
is DataState.Success -> {
entriesAdapter.submitList(dataState.data)
binding.entriesProgress.visibility = View.INVISIBLE
}
is DataState.Loading -> {
binding.entriesProgress.visibility = View.VISIBLE
}
else -> {}
}
}
private fun showSelectTypeDialog() {
val satelliteTypes = viewModel.satTypes.toTypedArray()
val selectedValue = satelliteTypes.indexOf(viewModel.getSatType())
val typeFormat = requireContext().getString(R.string.types_message)
AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.types_title)
setSingleChoiceItems(satelliteTypes, selectedValue) { dialog, index ->
val selectedItem = satelliteTypes[index]
binding.entriesTypeMessage.text = String.format(typeFormat, selectedItem)
viewModel.setSatType(selectedItem)
dialog.dismiss()
}
create()
show()
}
}
}
@@ -1,120 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.entriesScreen
import androidx.lifecycle.*
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.model.SatItem
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import java.util.*
import javax.inject.Inject
@HiltViewModel
class EntriesViewModel @Inject constructor(
private val repository: IDataRepository,
private val settings: ISettingsManager
) : ViewModel(), EntriesAdapter.EntriesClickListener {
private val satType = MutableLiveData(String())
private val transModes = MutableLiveData(settings.loadModesSelection())
private val currentQuery = MutableLiveData(String())
private val itemsFromRepo = liveData {
delay(125)
emit(loadEntriesWithSelection())
} as MutableLiveData
private val itemsWithType = satType.switchMap { type ->
itemsFromRepo.map { items -> filterByType(items, type) }
}
private val itemsWithModes = transModes.switchMap { modes ->
itemsWithType.map { items -> filterByModes(items, modes) }
}
private val itemsWithQuery = currentQuery.switchMap { query ->
itemsWithModes.map { items -> filterByQuery(items, query) }
}
val satData = itemsWithQuery.map { items -> DataState.Success(items) }
val satTypes: List<String> = settings.sourcesMap.keys.sorted()
fun getSatType(): String? = satType.value
fun setSatType(type: String) {
satType.value = type
}
fun selectCurrentItems(selectAll: Boolean) {
itemsWithQuery.value?.let { itemsWithQuery ->
updateSelection(itemsWithQuery.map { item -> item.catnum }, selectAll)
}
}
fun saveSelectedModes(modes: List<String>) {
transModes.value = modes
settings.saveModesSelection(modes)
}
fun setQuery(query: String) {
currentQuery.value = query
}
fun saveSelection(): List<Int> {
return itemsFromRepo.value?.let { itemsAll ->
itemsAll.filter { item -> item.isSelected }.map { item -> item.catnum }
} ?: emptyList()
}
override fun updateSelection(catNums: List<Int>, isSelected: Boolean) {
itemsFromRepo.value?.let { itemsAll ->
val copiedList = itemsAll.map { item -> item.copy() }
catNums.forEach { catnum ->
copiedList.find { item -> item.catnum == catnum }?.isSelected = isSelected
}
itemsFromRepo.value = copiedList
}
}
private suspend fun loadEntriesWithSelection(): List<SatItem> {
val selectedIds = settings.loadEntriesSelection()
return repository.getEntriesWithModes().onEach { it.isSelected = it.catnum in selectedIds }
}
private fun filterByType(items: List<SatItem>, type: String): List<SatItem> {
if (type == "All") return items
val catnums = settings.loadSatType(type)
if (catnums.isEmpty()) return items
return items.filter { item -> item.catnum in catnums }
}
private fun filterByModes(items: List<SatItem>, modes: List<String>): List<SatItem> {
if (modes.isEmpty()) return items
return items.filter { item -> item.modes.any { mode -> mode in modes } }
}
private fun filterByQuery(items: List<SatItem>, query: String): List<SatItem> {
if (query.isBlank()) return items
return try {
items.filter { it.catnum == query.toInt() }
} catch (e: Exception) {
items.filter { item ->
val itemName = item.name.lowercase(Locale.getDefault())
itemName.contains(query.lowercase(Locale.getDefault()))
}
}
}
}
@@ -1,79 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.entriesScreen
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.databinding.ItemModeBinding
class ModesAdapter(private val clickListener: ModesClickListener) :
RecyclerView.Adapter<ModesAdapter.ModeHolder>() {
private val diffCallback = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
}
private val differ = AsyncListDiffer(this, diffCallback)
private var selectedModes = listOf<String>()
fun submitModes(items: List<String>, selected: List<String>) {
differ.submitList(items)
selectedModes = selected
}
override fun getItemCount(): Int = differ.currentList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ModeHolder {
return ModeHolder.from(parent)
}
override fun onBindViewHolder(holder: ModeHolder, position: Int) {
val currentItem = differ.currentList[position]
holder.bind(currentItem, selectedModes.contains(currentItem), clickListener)
}
class ModeHolder(private val binding: ItemModeBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(mode: String, isSelected: Boolean, listener: ModesClickListener) {
binding.modeCheckbox.text = mode
binding.modeCheckbox.isChecked = isSelected
binding.modeCheckbox.setOnClickListener {
listener.onModeClicked(mode, isSelected.not())
}
}
companion object {
fun from(parent: ViewGroup): ModeHolder {
val inflater = LayoutInflater.from(parent.context)
return ModeHolder(ItemModeBinding.inflate(inflater, parent, false))
}
}
}
interface ModesClickListener {
fun onModeClicked(mode: String, isSelected: Boolean)
}
}
@@ -1,89 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.entriesScreen
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.DialogModesBinding
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.presentation.setNavResult
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class ModesDialog : AppCompatDialogFragment(), ModesAdapter.ModesClickListener {
@Inject
lateinit var preferences: ISettingsManager
private lateinit var binding: DialogModesBinding
private val allModes = listOf(
"AFSK", "AFSK S-Net", "AFSK SALSAT", "AHRPT", "AM", "APT", "BPSK", "BPSK PMT-A3",
"CERTO", "CW", "DQPSK", "DSTAR", "DUV", "FFSK", "FM", "FMN", "FSK", "FSK AX.100 Mode 5",
"FSK AX.100 Mode 6", "FSK AX.25 G3RUH", "GFSK", "GFSK Rktr", "GMSK", "HRPT", "LoRa",
"LRPT", "LSB", "MFSK", "MSK", "MSK AX.100 Mode 5", "MSK AX.100 Mode 6", "OFDM", "OQPSK",
"PSK", "PSK31", "PSK63", "QPSK", "QPSK31", "QPSK63", "SSTV", "USB", "WSJT"
)
private val selectedModes = mutableListOf<String>()
override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View {
binding = DialogModesBinding.inflate(inflater, group, false)
return binding.root
}
override fun onViewCreated(view: View, state: Bundle?) {
super.onViewCreated(view, state)
dialog?.window?.setBackgroundDrawableResource(R.color.transparent)
dialog?.window?.setLayout(
(resources.displayMetrics.widthPixels * 0.94).toInt(),
(resources.displayMetrics.heightPixels * 0.76).toInt()
)
val modesAdapter = ModesAdapter(this@ModesDialog)
val layoutManager = GridLayoutManager(context, 2)
val itemDecoration = DividerItemDecoration(context, layoutManager.orientation)
selectedModes.addAll(preferences.loadModesSelection())
modesAdapter.submitModes(allModes, selectedModes)
binding.run {
modesRecycler.apply {
setHasFixedSize(true)
this.adapter = modesAdapter
this.layoutManager = layoutManager
addItemDecoration(itemDecoration)
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
modesBtnNeg.setOnClickListener { dismiss() }
modesBtnPos.setOnClickListener {
setNavResult("modes", selectedModes.toList())
dismiss()
}
}
}
override fun onModeClicked(mode: String, isSelected: Boolean) {
when {
isSelected -> selectedModes.add(mode)
selectedModes.contains(mode) -> selectedModes.remove(mode)
}
}
}
@@ -1,36 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.mapScreen
import com.rtbishop.look4sat.domain.predict.GeoPos
data class MapData(
val catNum: Int,
val name: String,
val aosTime: String,
val azimuth: Double,
val elevation: Double,
val range: Double,
val altitude: Double,
val velocity: Double,
val qthLoc: String,
val osmPos: GeoPos,
val period: Double,
val phase: Double,
val eclipsed: Boolean
)
@@ -1,240 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.mapScreen
import android.content.Context
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.View
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.FragmentMapBinding
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.presentation.clickWithDebounce
import dagger.hilt.android.AndroidEntryPoint
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.FolderOverlay
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Polyline
@AndroidEntryPoint
class MapFragment : Fragment(R.layout.fragment_map) {
private val viewModel: MapViewModel by viewModels()
private val navArgs: MapFragmentArgs by navArgs()
private val minLat = MapView.getTileSystem().minLatitude
private val maxLat = MapView.getTileSystem().maxLatitude
private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 3f
style = Paint.Style.STROKE
color = Color.RED
strokeCap = Paint.Cap.ROUND
strokeJoin = Paint.Join.ROUND
}
private val footprintPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 3f
style = Paint.Style.FILL_AND_STROKE
color = Color.parseColor("#FFE082")
}
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
textSize = 36f
style = Paint.Style.FILL
color = Color.parseColor("#CCFFFFFF")
setShadowLayer(3f, 3f, 3f, Color.BLACK)
}
private val labelRect = Rect()
private lateinit var binding: FragmentMapBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentMapBinding.bind(view)
setupComponents()
setupObservers()
}
private fun setupComponents() {
val context = requireContext()
val preferences = context.getSharedPreferences("default", Context.MODE_PRIVATE)
Configuration.getInstance().load(context, preferences)
binding.run {
mapView.apply {
setMultiTouchControls(true)
setTileSource(TileSourceFactory.WIKIMEDIA)
minZoomLevel = getMinZoom(resources.displayMetrics.heightPixels) + 0.25
maxZoomLevel = 5.75
controller.setZoom(minZoomLevel + 0.25)
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
overlayManager.tilesOverlay.loadingBackgroundColor = Color.TRANSPARENT
overlayManager.tilesOverlay.loadingLineColor = Color.TRANSPARENT
overlayManager.tilesOverlay.setColorFilter(getColorFilter())
setScrollableAreaLimitLatitude(maxLat, minLat, 0)
// add overlays: 0 - GSP, 1 - SatTrack, 2 - SatFootprint, 3 - SatIcons
overlays.addAll(Array(4) { FolderOverlay() })
}
mapBtnBack.clickWithDebounce { findNavController().navigateUp() }
mapBtnPrev.setOnClickListener { viewModel.scrollSelection(true) }
mapBtnNext.setOnClickListener { viewModel.scrollSelection(false) }
}
}
private fun setupObservers() {
viewModel.selectDefaultSatellite(navArgs.catNum)
viewModel.stationPos.observe(viewLifecycleOwner) { handleStationPos(it) }
viewModel.positions.observe(viewLifecycleOwner) { handlePositions(it) }
viewModel.track.observe(viewLifecycleOwner) { handleTrack(it) }
viewModel.footprint.observe(viewLifecycleOwner) { handleFootprint(it) }
viewModel.mapData.observe(viewLifecycleOwner) { handleMapData(it) }
}
private fun handleStationPos(stationPos: GeoPos) {
binding.apply {
Marker(mapView).apply {
setInfoWindow(null)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_position)
position = GeoPoint(stationPos.lat, stationPos.lon)
mapView.overlays[0] = this
mapView.invalidate()
}
}
}
private fun handlePositions(posMap: Map<Satellite, GeoPos>) {
binding.apply {
val markers = FolderOverlay()
posMap.entries.forEach {
Marker(mapView).apply {
setInfoWindow(null)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
icon = getCustomTextIcon(it.key.data.name)
try {
position = GeoPoint(it.value.lat, it.value.lon)
} catch (exception: IllegalArgumentException) {
println(exception.stackTraceToString())
}
setOnMarkerClickListener { _, _ ->
viewModel.selectSatellite(it.key)
return@setOnMarkerClickListener true
}
markers.add(this)
}
}
mapView.overlays[3] = markers
mapView.invalidate()
}
}
private fun getCustomTextIcon(textLabel: String): Drawable {
textPaint.getTextBounds(textLabel, 0, textLabel.length, labelRect)
val iconSize = 8f
val width = labelRect.width() + iconSize * 2f
val height = textPaint.textSize * 3f + iconSize * 2f
val bitmap = Bitmap.createBitmap(width.toInt(), height.toInt(), Bitmap.Config.ARGB_8888)
Canvas(bitmap).run {
drawCircle(width / 2f, height / 2f, iconSize, textPaint)
drawText(textLabel, iconSize / 2f, height - iconSize, textPaint)
}
return BitmapDrawable(binding.mapView.context.resources, bitmap)
}
private fun handleTrack(satTrack: List<List<GeoPos>>) {
val center = GeoPoint(satTrack[0][0].lat, satTrack[0][0].lon)
val trackOverlay = FolderOverlay()
satTrack.forEach { track ->
val trackPoints = track.map { GeoPoint(it.lat, it.lon) }
Polyline().apply {
try {
setPoints(trackPoints)
outlinePaint.set(trackPaint)
trackOverlay.add(this)
} catch (exception: IllegalArgumentException) {
println(exception.stackTraceToString())
}
}
}
binding.mapView.overlays[1] = trackOverlay
binding.mapView.controller.animateTo(center)
}
private fun handleFootprint(satPos: SatPos) {
val footprintPoints = satPos.getRangeCircle().map { GeoPoint(it.lat, it.lon) }
val footprintOverlay = Polyline().apply {
outlinePaint.set(footprintPaint)
try {
setPoints(footprintPoints)
} catch (exception: IllegalArgumentException) {
println(exception.stackTraceToString())
}
}
binding.mapView.overlays[2] = footprintOverlay
}
private fun handleMapData(mapData: MapData) {
binding.apply {
mapTimer.text = mapData.aosTime
mapDataPeriod.text = String.format(getString(R.string.map_period, mapData.period))
mapDataPhase.text = String.format(getString(R.string.map_phase), mapData.phase)
mapAzimuth.text = String.format(getString(R.string.map_azimuth), mapData.azimuth)
mapElevation.text = String.format(getString(R.string.map_elevation), mapData.elevation)
mapDataAlt.text = String.format(getString(R.string.map_altitude), mapData.altitude)
mapDataDst.text = String.format(getString(R.string.map_distance), mapData.range)
mapDataLat.text =
String.format(getString(R.string.map_latitude), mapData.osmPos.lat)
mapDataLon.text =
String.format(getString(R.string.map_longitude), mapData.osmPos.lon)
mapDataQth.text = String.format(getString(R.string.map_qth), mapData.qthLoc)
mapDataVel.text = String.format(getString(R.string.map_velocity), mapData.velocity)
if (mapData.eclipsed) {
mapDataVisibility.text = getString(R.string.map_eclipsed)
} else {
mapDataVisibility.text = getString(R.string.map_visible)
}
}
binding.mapView.invalidate()
}
private fun getColorFilter(): ColorMatrixColorFilter {
val grayScaleMatrix = ColorMatrix().apply { setSaturation(0f) }
val negativeMatrix = ColorMatrix(
floatArrayOf(
-1f, 0f, 0f, 0f, 255f,
0f, -1f, 0f, 0f, 255f,
0f, 0f, -1f, 0f, 255f,
0f, 0f, 0f, 1f, 0f
)
)
negativeMatrix.preConcat(grayScaleMatrix)
return ColorMatrixColorFilter(negativeMatrix)
}
private fun getMinZoom(screenHeight: Int): Double {
return MapView.getTileSystem().getLatitudeZoom(maxLat, minLat, screenHeight)
}
}
@@ -1,207 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.mapScreen
import androidx.lifecycle.*
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.utility.QthConverter
import com.rtbishop.look4sat.utility.toDegrees
import com.rtbishop.look4sat.utility.toTimerString
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
import java.util.*
import javax.inject.Inject
import kotlin.math.max
import kotlin.math.min
@HiltViewModel
class MapViewModel @Inject constructor(
private val satelliteManager: ISatelliteManager,
private val repository: IDataRepository,
private val settings: ISettingsManager,
) : ViewModel() {
private val stationPosition = settings.loadStationPosition()
private var allPasses = satelliteManager.getPasses()
private var allSatellites = listOf<Satellite>()
private var dataUpdateJob: Job? = null
private var dataUpdateRate = 1000L
private var selectedSatellite: Satellite? = null
val stationPos = liveData {
val osmLat = clipLat(stationPosition.lat)
val osmLon = clipLon(stationPosition.lon)
emit(GeoPos(osmLat, osmLon))
}
private val _track = MutableLiveData<List<List<GeoPos>>>()
val track: LiveData<List<List<GeoPos>>> = _track
private val _footprint = MutableLiveData<SatPos>()
val footprint: LiveData<SatPos> = _footprint
private val _mapData = MutableLiveData<MapData>()
val mapData: LiveData<MapData> = this._mapData
private val _positions = MutableLiveData<Map<Satellite, GeoPos>>()
val positions: LiveData<Map<Satellite, GeoPos>> = _positions
fun scrollSelection(decrement: Boolean) {
if (allSatellites.isNotEmpty()) {
val index = allSatellites.indexOf(selectedSatellite)
if (decrement) {
if (index > 0) selectSatellite(allSatellites[index - 1])
else selectSatellite(allSatellites[allSatellites.size - 1])
} else {
if (index < allSatellites.size - 1) selectSatellite(allSatellites[index + 1])
else selectSatellite(allSatellites[0])
}
}
}
fun selectDefaultSatellite(catnum: Int) {
viewModelScope.launch {
val selectedIds = settings.loadEntriesSelection()
repository.getEntriesWithIds(selectedIds).also { satellites ->
if (satellites.isNotEmpty()) {
allSatellites = satellites
if (catnum == -1) {
allPasses.find { pass -> pass.progress < 100 && !pass.isDeepSpace }
?.let { pass -> selectSatellite(pass.satellite) }
} else {
satellites.find { it.data.catnum == catnum }?.let { selectSatellite(it) }
}
}
}
}
}
fun selectSatellite(satellite: Satellite, updateFreq: Long = dataUpdateRate) {
selectedSatellite = satellite
viewModelScope.launch {
dataUpdateJob?.cancelAndJoin()
dataUpdateJob = launch {
val dateNow = Date()
getSatTrack(satellite, stationPosition, dateNow)
while (isActive) {
dateNow.time = System.currentTimeMillis()
getPositions(allSatellites, stationPosition, dateNow)
getSatFootprint(satellite, stationPosition, dateNow)
getSatData(satellite, stationPosition, dateNow)
delay(updateFreq)
}
}
}
}
private suspend fun getSatTrack(satellite: Satellite, pos: GeoPos, date: Date) {
val satTracks = mutableListOf<List<GeoPos>>()
val currentTrack = mutableListOf<GeoPos>()
val endDate = Date(date.time + (satellite.data.orbitalPeriod * 2.4 * 60000L).toLong())
var oldLongitude = 0.0
satelliteManager.getTrack(satellite, pos, date.time, endDate.time).forEach { satPos ->
val osmLat = clipLat(satPos.latitude.toDegrees())
val osmLon = clipLon(satPos.longitude.toDegrees())
val currentPosition = GeoPos(osmLat, osmLon)
if (oldLongitude < -170.0 && currentPosition.lon > 170.0) {
// adding left terminal position
currentTrack.add(GeoPos(osmLat, -180.0))
val finishedTrack = mutableListOf<GeoPos>().apply { addAll(currentTrack) }
satTracks.add(finishedTrack)
currentTrack.clear()
} else if (oldLongitude > 170.0 && currentPosition.lon < -170.0) {
// adding right terminal position
currentTrack.add(GeoPos(osmLat, 180.0))
val finishedTrack = mutableListOf<GeoPos>().apply { addAll(currentTrack) }
satTracks.add(finishedTrack)
currentTrack.clear()
}
oldLongitude = currentPosition.lon
currentTrack.add(currentPosition)
}
satTracks.add(currentTrack)
_track.postValue(satTracks)
}
private suspend fun getPositions(satellites: List<Satellite>, pos: GeoPos, date: Date) {
val positions = mutableMapOf<Satellite, GeoPos>()
satellites.forEach { satellite ->
val satPos = satelliteManager.getPosition(satellite, pos, date.time)
val osmLat = clipLat(satPos.latitude.toDegrees())
val osmLon = clipLon(satPos.longitude.toDegrees())
positions[satellite] = GeoPos(osmLat, osmLon)
}
_positions.postValue(positions)
}
private suspend fun getSatFootprint(satellite: Satellite, pos: GeoPos, date: Date) {
val satPos = satelliteManager.getPosition(satellite, pos, date.time)
_footprint.postValue(satPos)
}
private suspend fun getSatData(sat: Satellite, pos: GeoPos, date: Date) {
var aosTime = 0L.toTimerString()
allPasses.find { pass -> pass.catNum == sat.data.catnum && pass.progress < 100 }
?.let { satPass ->
if (!satPass.isDeepSpace) {
aosTime = if (date.time < satPass.aosTime) {
val millisBeforeStart = satPass.aosTime.minus(date.time)
millisBeforeStart.toTimerString()
} else {
val millisBeforeEnd = satPass.losTime.minus(date.time)
millisBeforeEnd.toTimerString()
}
}
}
val satPos = satelliteManager.getPosition(sat, pos, date.time)
val azimuth = satPos.azimuth.toDegrees()
val elevation = satPos.elevation.toDegrees()
val osmLat = clipLat(satPos.latitude.toDegrees())
val osmLon = clipLon(satPos.longitude.toDegrees())
val osmPos = GeoPos(osmLat, osmLon)
val qthLoc = QthConverter.positionToQth(osmPos.lat, osmPos.lon) ?: "-- --"
val velocity = satPos.getOrbitalVelocity()
val phase = satPos.phase.toDegrees()
val visibility = satPos.eclipsed
val satData = MapData(
sat.data.catnum, sat.data.name, aosTime, azimuth, elevation, satPos.distance,
satPos.altitude, velocity, qthLoc, osmPos, sat.data.orbitalPeriod, phase, visibility
)
_mapData.postValue(satData)
}
private fun clipLat(latitude: Double): Double {
return clip(latitude, -85.05, 85.05)
}
private fun clipLon(longitude: Double): Double {
var result = longitude
while (result < -180.0) result += 360.0
while (result > 180.0) result -= 360.0
return clip(result, -180.0, 180.0)
}
private fun clip(currentValue: Double, minValue: Double, maxValue: Double): Double {
return min(max(currentValue, minValue), maxValue)
}
}
@@ -1,70 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.passesScreen
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDialogFragment
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.DialogFilterBinding
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.presentation.setNavResult
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class FilterDialog : AppCompatDialogFragment() {
@Inject
lateinit var preferences: ISettingsManager
override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View? {
return inflater.inflate(R.layout.dialog_filter, group, false)
}
override fun onViewCreated(view: View, state: Bundle?) {
super.onViewCreated(view, state)
DialogFilterBinding.bind(view).apply {
dialog?.window?.setBackgroundDrawableResource(R.color.transparent)
dialog?.window?.setLayout(
(resources.displayMetrics.widthPixels * 0.94).toInt(),
WindowManager.LayoutParams.WRAP_CONTENT
)
filterHoursEdit.setText(preferences.getHoursAhead().toString())
filterElevEdit.setText(preferences.getMinElevation().toString())
filterBtnPos.setOnClickListener {
val hoursAhead = try {
filterHoursEdit.text.toString().toInt()
} catch (exception: Exception) {
24
}
val minElevation = try {
filterElevEdit.text.toString().toDouble()
} catch (exception: Exception) {
16.0
}
setNavResult("prefs", Pair(hoursAhead, minElevation))
dismiss()
}
filterBtnNeg.setOnClickListener { dismiss() }
}
}
}
@@ -1,124 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.passesScreen
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.ItemPassBinding
import com.rtbishop.look4sat.domain.predict.SatPass
import java.text.SimpleDateFormat
import java.util.*
class PassesAdapter(private val clickListener: PassesClickListener) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val diffCallback = object : DiffUtil.ItemCallback<SatPass>() {
override fun areItemsTheSame(oldItem: SatPass, newItem: SatPass): Boolean {
return oldItem.catNum == newItem.catNum && oldItem.aosTime == newItem.aosTime
}
override fun areContentsTheSame(oldItem: SatPass, newItem: SatPass): Boolean {
return oldItem.progress == newItem.progress
}
}
private val differ = AsyncListDiffer(this, diffCallback)
private var isUTC: Boolean = false
interface PassesClickListener {
fun navigateToPass(satPass: SatPass)
}
fun submitList(passes: List<SatPass>) {
differ.submitList(passes)
}
fun setUTC(isUTC: Boolean) {
this.isUTC = isUTC
}
override fun getItemCount() = differ.currentList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return PassHolder.from(parent)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as PassHolder).bind(differ.currentList[position], clickListener, isUTC)
}
class PassHolder private constructor(private val binding: ItemPassBinding) :
RecyclerView.ViewHolder(binding.root) {
private val satIdFormat = itemView.context.getString(R.string.pass_satId)
private val aosAzFormat = itemView.context.getString(R.string.pass_aosAz)
private val altFormat = itemView.context.getString(R.string.pass_altitude)
private val losAzFormat = itemView.context.getString(R.string.pass_los)
private val startTimeFormat = itemView.context.getString(R.string.pass_startTime)
private val elevFormat = itemView.context.getString(R.string.pass_elevation)
private val endTimeFormat = itemView.context.getString(R.string.pass_endTime)
private val placeholder = itemView.context.getString(R.string.pass_placeholder)
private val timeZoneUTC = TimeZone.getTimeZone("UTC")
private val startFormat = SimpleDateFormat(startTimeFormat, Locale.getDefault())
private val endFormat = SimpleDateFormat(endTimeFormat, Locale.getDefault())
fun bind(satPass: SatPass, listener: PassesClickListener, shouldUseUTC: Boolean) {
binding.apply {
if (satPass.isDeepSpace) {
passName.text = satPass.name
passId.text = String.format(satIdFormat, satPass.catNum)
passAos.text = String.format(aosAzFormat, satPass.aosAzimuth)
passAltitude.text = String.format(altFormat, satPass.altitude)
passLos.text = String.format(losAzFormat, satPass.losAzimuth)
passStart.text = placeholder
passElev.text = String.format(elevFormat, satPass.maxElevation)
passEnd.text = placeholder
passProgress.progress = 100
} else {
if (shouldUseUTC) {
startFormat.timeZone = timeZoneUTC
endFormat.timeZone = timeZoneUTC
}
passName.text = satPass.name
passId.text = String.format(satIdFormat, satPass.catNum)
passAos.text = String.format(aosAzFormat, satPass.aosAzimuth)
passAltitude.text = String.format(altFormat, satPass.altitude)
passLos.text = String.format(losAzFormat, satPass.losAzimuth)
passStart.text = startFormat.format(Date(satPass.aosTime))
passElev.text = String.format(elevFormat, satPass.maxElevation)
passEnd.text = endFormat.format(Date(satPass.losTime))
passProgress.progress = satPass.progress
}
}
itemView.setOnClickListener {
listener.navigateToPass(satPass)
}
}
companion object {
fun from(parent: ViewGroup): PassHolder {
val inflater = LayoutInflater.from(parent.context)
return PassHolder(ItemPassBinding.inflate(inflater, parent, false))
}
}
}
}
@@ -1,167 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.passesScreen
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.FragmentPassesBinding
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.presentation.clickWithDebounce
import com.rtbishop.look4sat.presentation.getNavResult
import com.rtbishop.look4sat.utility.toTimerString
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class PassesFragment : Fragment(R.layout.fragment_passes), PassesAdapter.PassesClickListener {
private val viewModel: PassesViewModel by viewModels()
private val passesAdapter = PassesAdapter(this)
private var binding: FragmentPassesBinding? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentPassesBinding.bind(view).apply {
passesAdapter.setUTC(viewModel.shouldUseUTC())
passesRecycler.apply {
setHasFixedSize(true)
adapter = passesAdapter
layoutManager = LinearLayoutManager(requireContext())
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
addItemDecoration(DividerItemDecoration(requireContext(), 1))
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy > 0 && passesFab.visibility == View.VISIBLE) passesFab.hide()
else if (dy < 0 && passesFab.visibility != View.VISIBLE) passesFab.show()
}
})
}
passesBtnRefresh.clickWithDebounce { viewModel.calculatePasses() }
passesBtnMap.clickWithDebounce {
val dir = PassesFragmentDirections.globalToMap()
findNavController().navigate(dir)
}
passesBtnFilter.clickWithDebounce {
val dir = PassesFragmentDirections.passesToFilter()
findNavController().navigate(dir)
}
passesBtnSettings.clickWithDebounce {
val dir = PassesFragmentDirections.globalToSettings()
findNavController().navigate(dir)
}
setupObservers()
}
}
override fun navigateToPass(satPass: SatPass) {
if (satPass.progress < 100) satPass.run {
val dir = PassesFragmentDirections.globalToRadar(this.catNum, this.aosTime)
findNavController().navigate(dir)
}
}
override fun onDestroyView() {
binding?.passesRecycler?.adapter = null
binding = null
super.onDestroyView()
}
private fun setupObservers() {
viewModel.entriesTotal.observe(viewLifecycleOwner) { number ->
handleEntriesTotal(number)
}
viewModel.passes.observe(viewLifecycleOwner) { passesResult ->
handleNewPasses(passesResult)
}
getNavResult<Pair<Int, Double>>(R.id.nav_passes, "prefs") { prefs ->
viewModel.calculatePasses(prefs.first, prefs.second)
}
getNavResult<List<Int>>(R.id.nav_passes, "selection") { items ->
viewModel.calculatePasses(selection = items)
}
}
private fun handleEntriesTotal(number: Int) {
binding?.run {
if (number > 0) {
passesFab.clickWithDebounce {
val direction = PassesFragmentDirections.globalToEntries()
findNavController().navigate(direction)
}
} else {
passesFab.clickWithDebounce {
val errorMessage = getString(R.string.passes_empty_db)
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
}
}
}
}
private fun handleNewPasses(state: DataState<List<SatPass>>) {
binding?.run {
when (state) {
is DataState.Success -> {
passesAdapter.submitList(state.data)
tickMainTimer(state.data)
if (state.data.isNotEmpty()) { // show new passes list
passesEmpty.visibility = View.INVISIBLE
passesProgress.visibility = View.INVISIBLE
} else { // show no passes message
passesEmpty.visibility = View.VISIBLE
passesProgress.visibility = View.INVISIBLE
}
}
is DataState.Loading -> {
passesEmpty.visibility = View.INVISIBLE
passesProgress.visibility = View.VISIBLE
passesTimer.text = 0L.toTimerString()
}
else -> {}
}
}
}
private fun tickMainTimer(passes: List<SatPass>) {
binding?.run {
if (passes.isNotEmpty()) {
val timeNow = System.currentTimeMillis()
try {
val nextPass = passes.first { it.aosTime.minus(timeNow) > 0 }
val millisBeforeStart = nextPass.aosTime.minus(timeNow)
passesTimer.text = millisBeforeStart.toTimerString()
} catch (e: NoSuchElementException) {
val lastPass = passes.last()
val millisBeforeEnd = lastPass.losTime.minus(timeNow)
passesTimer.text = millisBeforeEnd.toTimerString()
}
} else {
passesTimer.text = 0L.toTimerString()
}
passesBtnRefresh.isEnabled = true
}
}
}
@@ -1,84 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.passesScreen
import androidx.lifecycle.*
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.SatPass
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
import javax.inject.Inject
@HiltViewModel
class PassesViewModel @Inject constructor(
private val satelliteManager: ISatelliteManager,
private val repository: IDataRepository,
private val settings: ISettingsManager
) : ViewModel() {
private var passesProcessing: Job? = null
private val _passes = MutableLiveData<DataState<List<SatPass>>>(DataState.Loading)
val passes: LiveData<DataState<List<SatPass>>> = _passes
val entriesTotal: LiveData<Int> = repository.getEntriesTotal().asLiveData()
init {
viewModelScope.launch {
satelliteManager.calculatedPasses.collect { passes ->
passesProcessing?.cancelAndJoin()
passesProcessing = viewModelScope.launch {
while (isActive) {
val time = System.currentTimeMillis()
val newPasses = satelliteManager.processPasses(passes, time)
_passes.postValue(DataState.Success(newPasses))
delay(1000)
}
}
}
}
viewModelScope.launch {
repository.updateState.collect { updateState ->
if (updateState is DataState.Success) calculatePasses()
}
}
calculatePasses()
}
fun shouldUseUTC() = settings.getUseUTC()
fun calculatePasses(
hoursAhead: Int = settings.getHoursAhead(),
minElevation: Double = settings.getMinElevation(),
timeRef: Long = System.currentTimeMillis(),
selection: List<Int>? = null
) {
viewModelScope.launch {
_passes.postValue(DataState.Loading)
passesProcessing?.cancelAndJoin()
selection?.let { items -> settings.saveEntriesSelection(items) }
settings.setHoursAhead(hoursAhead)
settings.setMinElevation(minElevation)
val stationPos = settings.loadStationPosition()
val selectedIds = settings.loadEntriesSelection()
val satellites = repository.getEntriesWithIds(selectedIds)
satelliteManager.calculatePasses(satellites, stationPos, timeRef, hoursAhead, minElevation)
}
}
}
@@ -1,82 +0,0 @@
package com.rtbishop.look4sat.presentation.radarScreen
import android.bluetooth.BluetoothManager
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.OutputStream
import java.util.*
import kotlin.math.abs
class BTReporter(private val manager: BluetoothManager, private val reporterScope: CoroutineScope) {
private val tag = "BTReporter"
private val sppid: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")
private lateinit var outputStream: OutputStream
private var rotationConnectionJob: Job? = null
private var rotationReportingJob: Job? = null
private var connected = false
private var connecting = false
fun isConnected(): Boolean = connected
fun isConnecting(): Boolean = connecting
fun connectBTDevice(deviceId: String) {
if (!connected) {
rotationConnectionJob = reporterScope.launch {
try {
manager.adapter.getRemoteDevice(deviceId)?.let { device ->
device.createInsecureRfcommSocketToServiceRecord(sppid)?.let { socket ->
connecting = true
socket.connect()
outputStream = socket.outputStream
connected = true
connecting = false
Log.i(tag, "$tag: Connected!")
}
}
} catch (e: SecurityException) {
Log.e(tag, "$tag: ${e.message}")
} catch (e: Exception) {
Log.e(tag, "$tag: ${e.message}")
}
}
}
}
fun reportRotation(format: String, azimuth: Int, elevation: Int) {
if (connected) {
rotationReportingJob = reporterScope.launch {
val newElevation = if (elevation > 0) elevation else 0
try {
val azimuthString = intToStringWithLeadingZeroes(azimuth)
val elevationString = intToStringWithLeadingZeroes(newElevation)
val crChar = '\r'
val nlChar = '\n'
val tbChar = '\t'
var buffer = format.replace("\$AZ", azimuthString)
buffer = buffer.replace("\$EL", elevationString)
buffer = buffer.replace("\\r", crChar.toString())
buffer = buffer.replace("\\n", nlChar.toString())
buffer = buffer.replace("\\t", tbChar.toString())
Log.i(tag, "$tag: Sending $buffer")
if (connected) outputStream.write(buffer.toByteArray())
} catch (e: Exception) {
Log.e(tag, "$tag: ${e.message}")
connected = false
}
}
}
}
private fun intToStringWithLeadingZeroes(value: Int): String {
return if (value > 0) {
if (value < 10) "00$value" else if (value < 100) "0$value" else "$value"
} else {
val absValue = abs(value)
if (value > -10) "-00$absValue" else if (value > -100) "-0$absValue" else "-$absValue"
}
}
}
@@ -1,22 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.radarScreen
import com.rtbishop.look4sat.domain.predict.SatPos
data class RadarData(val satPos: SatPos, val satTrack: List<SatPos> = emptyList())
@@ -1,159 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.radarScreen
import android.content.Intent
import android.os.Bundle
import android.provider.CalendarContract
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.FragmentRadarBinding
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.presentation.clickWithDebounce
import com.rtbishop.look4sat.utility.toDegrees
import com.rtbishop.look4sat.utility.toTimerString
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class RadarFragment : Fragment(R.layout.fragment_radar) {
private val viewModel: RadarViewModel by viewModels()
private val navArgs: RadarFragmentArgs by navArgs()
private val radioAdapter = RadioAdapter()
private var binding: FragmentRadarBinding? = null
private var radarView: RadarView? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentRadarBinding.bind(view).apply {
radarRecycler.apply {
setHasFixedSize(true)
this.adapter = radioAdapter
this.layoutManager = LinearLayoutManager(requireContext())
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
addItemDecoration(DividerItemDecoration(requireContext(), 1))
}
setupObservers()
}
}
override fun onResume() {
super.onResume()
viewModel.enableSensor()
}
override fun onPause() {
super.onPause()
viewModel.disableSensor()
}
override fun onDestroyView() {
binding?.radarRecycler?.adapter = null
radarView = null
binding = null
super.onDestroyView()
}
private fun setupObservers() {
viewModel.getPass(navArgs.catNum, navArgs.aosTime).observe(viewLifecycleOwner) { pass ->
binding?.run {
radarView = RadarView(requireContext()).apply {
setShowAim(viewModel.getUseCompass())
setScanning(viewModel.getShowSweep())
}
radarCard.addView(radarView)
viewModel.radarData.observe(viewLifecycleOwner) { passData ->
radarView?.setPosition(passData.satPos)
radarView?.setPositions(passData.satTrack)
setPassText(pass, passData.satPos)
}
viewModel.transmitters.observe(viewLifecycleOwner) { list ->
if (list.isNotEmpty()) {
radioAdapter.submitList(list)
radarProgress.visibility = View.INVISIBLE
} else {
radarProgress.visibility = View.INVISIBLE
radarEmptyLayout.visibility = View.VISIBLE
}
radarView?.invalidate()
}
viewModel.orientation.observe(viewLifecycleOwner) { value ->
radarView?.setOrientation(value.first, value.second, value.third)
}
radarBtnBack.clickWithDebounce { findNavController().navigateUp() }
radarBtnMap.clickWithDebounce {
val direction = RadarFragmentDirections.globalToMap(pass.catNum)
findNavController().navigate(direction)
}
radarBtnNotify.clickWithDebounce {
val intent = Intent(Intent.ACTION_INSERT)
.setData(CalendarContract.Events.CONTENT_URI)
.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, pass.aosTime)
.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, pass.losTime)
.putExtra(CalendarContract.Events.TITLE, pass.name)
.putExtra(CalendarContract.Events.DESCRIPTION, "Look4Sat")
startActivity(intent)
}
radarBtnSettings.clickWithDebounce {
val direction = RadarFragmentDirections.globalToSettings()
findNavController().navigate(direction)
}
}
}
}
private fun setPassText(satPass: SatPass, satPos: SatPos) {
binding?.run {
val timeNow = System.currentTimeMillis()
val radarAzim = getString(R.string.radar_az_value)
val radarElev = getString(R.string.radar_el_value)
val radarAlt = getString(R.string.radar_alt_value)
val radarDist = getString(R.string.radar_dist_value)
radarAzValue.text = String.format(radarAzim, satPos.azimuth.toDegrees())
radarElValue.text = String.format(radarElev, satPos.elevation.toDegrees())
radarAltValue.text = String.format(radarAlt, satPos.altitude)
radarDstValue.text = String.format(radarDist, satPos.distance)
if (satPos.eclipsed) {
radarVisibility.text = getText(R.string.radar_eclipsed)
} else {
radarVisibility.text = getText(R.string.radar_visible)
}
if (!satPass.isDeepSpace) {
if (timeNow < satPass.aosTime) {
val millisBeforeStart = satPass.aosTime.minus(timeNow)
radarTimer.text = millisBeforeStart.toTimerString()
} else {
val millisBeforeEnd = satPass.losTime.minus(timeNow)
radarTimer.text = millisBeforeEnd.toTimerString()
if (timeNow > satPass.losTime) {
radarTimer.text = 0L.toTimerString()
findNavController().navigateUp()
}
}
} else radarTimer.text = 0L.toTimerString()
}
}
}
@@ -1,265 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.radarScreen
import android.content.Context
import android.graphics.*
import android.view.View
import androidx.core.content.ContextCompat
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.predict.PI_2
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.TWO_PI
import com.rtbishop.look4sat.utility.toRadians
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.sin
class RadarView(context: Context) : View(context) {
private val defaultColor = ContextCompat.getColor(context, R.color.accent)
private val scale = resources.displayMetrics.density
private val strokeSize = scale * 2f
private var position: SatPos? = null
private var positions: List<SatPos> = emptyList()
private var radarColor = ContextCompat.getColor(context, R.color.textMain)
private var radarCircleNum = 3
private var radarPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = radarColor
style = Paint.Style.STROKE
strokeWidth = strokeSize
}
private var shouldShowSweep = true
private var sweepColor = defaultColor
private var sweepDegrees = 0f
private var sweepSpeed = 15.0f
private var sweepPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = sweepColor
}
private var shouldShowBeacons = true
private var beaconColor = defaultColor
private var beaconSize = scale * 7f
private var beaconPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = beaconColor
style = Paint.Style.FILL
}
private var radarTextSize = scale * 16f
private var radarTextColor = defaultColor
private var radarTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = radarTextColor
textSize = radarTextSize
}
private var isTrackCreated = false
private val trackPath: Path = Path()
private var trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.RED
style = Paint.Style.STROKE
strokeWidth = strokeSize
}
private val arrowPath = Path()
private var arrowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = ContextCompat.getColor(context, R.color.accent)
style = Paint.Style.FILL
strokeWidth = strokeSize
}
private var shouldShowAim = true
private var aimColor = Color.RED
private var aimPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = aimColor
style = Paint.Style.STROKE
strokeWidth = strokeSize
}
private var shouldRotateRadar = true
private var azimuth: Float = 0f
private var pitch: Float = 0f
private var roll: Float = 0f
fun setScanning(isScanning: Boolean) {
shouldShowSweep = isScanning
}
fun setPosition(position: SatPos) {
this.position = position
}
fun setPositions(positions: List<SatPos>) {
this.positions = positions
}
fun setShowAim(showAim: Boolean) {
shouldShowAim = showAim
}
fun setOrientation(azimuth: Float, pitch: Float, roll: Float) {
this.azimuth = azimuth
this.pitch = pitch
this.roll = roll
if (shouldRotateRadar) {
this.rotation = -azimuth
}
}
override fun onDraw(canvas: Canvas) {
val radarWidth = width - paddingLeft - paddingRight
val radarHeight = height - paddingTop - paddingBottom
val radarRadius = min(width, height) * 0.48f
val cx = paddingLeft + radarWidth / 2f
val cy = paddingTop + radarHeight / 2f
if (positions.isNotEmpty() && !isTrackCreated) {
createPassTrajectory(radarRadius)
createPassTrajectoryArrow()
isTrackCreated = true
}
canvas.drawColor(ContextCompat.getColor(context, R.color.cardRegular))
drawRadarCircle(canvas, cx, cy, radarRadius)
drawRadarCross(canvas, cx, cy, radarRadius)
drawRadarText(canvas, cx, radarRadius)
canvas.translate(cx, cy)
if (positions.isNotEmpty()) {
canvas.drawPath(trackPath, trackPaint)
canvas.drawPath(trackPath, arrowPaint)
}
if (shouldShowBeacons) {
drawSatellite(canvas, radarRadius)
}
if (shouldShowAim) {
drawCrosshair(canvas, azimuth, pitch, radarRadius)
}
if (shouldShowSweep) {
canvas.translate(-cx, -cy)
drawRadarSweep(canvas, cx, cy, radarRadius)
sweepDegrees = (sweepDegrees + 360 / sweepSpeed / 60) % 360
}
invalidate()
}
private fun drawRadarCircle(canvas: Canvas, cx: Float, cy: Float, radius: Float) {
for (i in 0 until radarCircleNum) {
canvas.drawCircle(cx, cy, radius - radius / radarCircleNum * i, radarPaint)
}
}
private fun drawRadarCross(canvas: Canvas, cx: Float, cy: Float, radius: Float) {
canvas.drawLine(cx - radius, cy, cx + radius, cy, radarPaint)
canvas.drawLine(cx, cy - radius, cx, cy + radius, radarPaint)
}
private fun drawRadarText(canvas: Canvas, cx: Float, radius: Float) {
for (i in 0 until radarCircleNum) {
val degText = " ${(90 / radarCircleNum) * (radarCircleNum - i)}°"
canvas.drawText(degText, cx, radius - radius / radarCircleNum * i, radarTextPaint)
}
}
private fun drawSatellite(canvas: Canvas, radarRadius: Float) {
position?.let { satPos ->
if (satPos.elevation > 0) {
val satX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
val satY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
canvas.drawCircle(satX, -satY, beaconSize, beaconPaint)
}
}
}
private fun drawCrosshair(canvas: Canvas, azimuth: Float, pitch: Float, radarRadius: Float) {
val azimuthRad = azimuth.toDouble().toRadians()
val tmpElevation = pitch.toDouble().toRadians()
val elevationRad = if (tmpElevation > 0.0) 0.0 else tmpElevation
val crossX = sph2CartX(azimuthRad, -elevationRad, radarRadius.toDouble())
val crossY = sph2CartY(azimuthRad, -elevationRad, radarRadius.toDouble())
canvas.drawLine(crossX - radarTextSize, -crossY, crossX + radarTextSize, -crossY, aimPaint)
canvas.drawLine(crossX, -crossY - radarTextSize, crossX, -crossY + radarTextSize, aimPaint)
canvas.drawCircle(crossX, -crossY, radarTextSize / 2, aimPaint)
}
private fun sph2CartX(azimuth: Double, elevation: Double, r: Double): Float {
val radius = r * (PI_2 - elevation) / PI_2
return (radius * cos(PI_2 - azimuth)).toFloat()
}
private fun sph2CartY(azimuth: Double, elevation: Double, r: Double): Float {
val radius = r * (PI_2 - elevation) / PI_2
return (radius * sin(PI_2 - azimuth)).toFloat()
}
private fun createPassTrajectory(radarRadius: Float) {
positions.forEachIndexed { index, satPos ->
val passX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
val passY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
if (index == 0) {
trackPath.moveTo(passX, -passY)
} else {
trackPath.lineTo(passX, -passY)
}
}
}
private fun createPassTrajectoryArrow() {
val radius = beaconSize
val sides = 3
val angle = TWO_PI / sides
arrowPath.moveTo((radius * cos(angle)).toFloat(), (radius * sin(angle)).toFloat())
for (i in 1 until sides) {
val x = (radius * cos(angle - angle * i)).toFloat()
val y = (radius * sin(angle - angle * i)).toFloat()
arrowPath.lineTo(x, y)
}
arrowPath.close()
val trackLength = PathMeasure(trackPath, false).length
val quarter = trackLength / 4f
val center = trackLength / 2f
val effect = PathDashPathEffect(arrowPath, center, quarter, PathDashPathEffect.Style.ROTATE)
arrowPaint.pathEffect = effect
}
private fun drawRadarSweep(canvas: Canvas, cx: Float, cy: Float, radius: Float) {
val sweepGradient = SweepGradient(
cx,
cy,
intArrayOf(
Color.TRANSPARENT,
changeAlpha(sweepColor, 0),
changeAlpha(sweepColor, 164),
changeAlpha(sweepColor, 255),
changeAlpha(sweepColor, 255)
),
floatArrayOf(0.0f, 0.55f, 0.996f, 0.999f, 1f)
)
sweepPaint.shader = sweepGradient
canvas.rotate(-90 + sweepDegrees, cx, cy)
canvas.drawCircle(cx, cy, radius, sweepPaint)
}
private fun changeAlpha(color: Int, alpha: Int): Int {
val red = Color.red(color)
val green = Color.green(color)
val blue = Color.blue(color)
return Color.argb(alpha, red, green, blue)
}
}
@@ -1,146 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.radarScreen
import android.hardware.GeomagneticField
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.framework.OrientationManager
import com.rtbishop.look4sat.utility.DataReporter
import com.rtbishop.look4sat.utility.round
import com.rtbishop.look4sat.utility.toDegrees
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@HiltViewModel
class RadarViewModel @Inject constructor(
private val orientationManager: OrientationManager,
private val reporter: DataReporter,
private val btReporter: BTReporter,
private val satManager: ISatelliteManager,
private val repository: IDataRepository,
private val settings: ISettingsManager
) : ViewModel(), OrientationManager.OrientationListener {
private val stationPos = settings.loadStationPosition()
private val _passData = MutableLiveData<RadarData>()
private val _transmitters = MutableLiveData<List<SatRadio>>()
private val _orientation = MutableLiveData<Triple<Float, Float, Float>>()
val radarData: LiveData<RadarData> = _passData
val transmitters: LiveData<List<SatRadio>> = _transmitters
val orientation: LiveData<Triple<Float, Float, Float>> = _orientation
fun getPass(catNum: Int, aosTime: Long) = liveData {
satManager.calculatedPasses.collect { passes ->
val pass = passes.find { pass -> pass.catNum == catNum && pass.aosTime == aosTime }
pass?.let { satPass ->
emit(satPass)
val transmitters = repository.getRadiosWithId(satPass.catNum)
viewModelScope.launch {
while (isActive) {
val timeNow = System.currentTimeMillis()
val satPos = satManager.getPosition(satPass.satellite, stationPos, timeNow)
sendPassData(satPass, satPos, satPass.satellite)
sendPassDataBT(satPos)
processRadios(transmitters, satPass.satellite, timeNow)
delay(1000)
}
}
}
}
}
fun enableSensor() {
if (settings.getUseCompass()) orientationManager.startListening(this)
}
fun disableSensor() {
if (settings.getUseCompass()) orientationManager.stopListening()
}
fun getUseCompass(): Boolean = settings.getUseCompass()
fun getShowSweep(): Boolean = settings.getShowSweep()
override fun onOrientationChanged(azimuth: Float, pitch: Float, roll: Float) {
_orientation.value = Triple(azimuth + getMagDeclination(stationPos), pitch, roll)
}
private fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float {
val latitude = geoPos.lat.toFloat()
val longitude = geoPos.lon.toFloat()
return GeomagneticField(latitude, longitude, 0f, time).declination
}
private fun sendPassData(satPass: SatPass, satPos: SatPos, satellite: Satellite) {
viewModelScope.launch {
var track: List<SatPos> = emptyList()
if (!satPass.isDeepSpace) {
track = satManager.getTrack(satellite, stationPos, satPass.aosTime, satPass.losTime)
}
if (settings.getRotatorEnabled()) {
val server = settings.getRotatorServer()
val port = settings.getRotatorPort().toInt()
val azimuth = satPos.azimuth.toDegrees().round(2)
val elevation = satPos.elevation.toDegrees().round(2)
reporter.reportRotation(server, port, azimuth, elevation)
}
_passData.postValue(RadarData(satPos, track))
}
}
private fun sendPassDataBT(satPos: SatPos) {
viewModelScope.launch {
if (settings.getBTEnabled()) {
val btDevice = settings.getBTDeviceAddr()
if (btReporter.isConnected()) {
val format = settings.getBTFormat()
val azimuth = satPos.azimuth.toDegrees().round(0).toInt()
val elevation = satPos.elevation.toDegrees().round(0).toInt()
btReporter.reportRotation(format, azimuth, elevation)
} else if (!btReporter.isConnecting()) {
Log.i("BTReporter", "BTReporter: Attempting to connect...")
btReporter.connectBTDevice(btDevice)
}
}
}
}
private fun processRadios(radios: List<SatRadio>, satellite: Satellite, time: Long) {
viewModelScope.launch {
delay(125)
val list = satManager.processRadios(satellite, stationPos, radios, time)
_transmitters.postValue(list)
}
}
}
@@ -1,103 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.radarScreen
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.ItemRadioBinding
import com.rtbishop.look4sat.domain.model.SatRadio
import java.util.*
class RadioAdapter : RecyclerView.Adapter<RadioAdapter.TransHolder>() {
private val diffCallback = object : DiffUtil.ItemCallback<SatRadio>() {
override fun areItemsTheSame(oldItem: SatRadio, newItem: SatRadio): Boolean {
return oldItem.uuid == newItem.uuid
}
override fun areContentsTheSame(oldItem: SatRadio, newItem: SatRadio): Boolean {
return oldItem.downlink == newItem.downlink
}
}
private val differ = AsyncListDiffer(this, diffCallback)
fun submitList(items: List<SatRadio>) = differ.submitList(items)
override fun getItemCount() = differ.currentList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransHolder {
return TransHolder.from(parent)
}
override fun onBindViewHolder(holder: TransHolder, position: Int) {
holder.bind(differ.currentList[position])
}
class TransHolder private constructor(private val binding: ItemRadioBinding) :
RecyclerView.ViewHolder(binding.root) {
private val divider = 1000000f
private val stringYes = itemView.context.getString(R.string.radio_string_yes)
private val stringNo = itemView.context.getString(R.string.radio_string_no)
private val link = itemView.context.getString(R.string.radio_link_low)
private val linkNull = itemView.context.getString(R.string.radio_no_link)
private val mode = itemView.context.getString(R.string.radio_mode)
private val inverted = itemView.context.getString(R.string.radio_inverted)
fun bind(radio: SatRadio) {
binding.run {
radioInfo.text = radio.info
radio.downlink.let { downlink ->
if (downlink != null) {
radioDownlink.text = String.format(Locale.ENGLISH, link, downlink / divider)
} else {
radioDownlink.text = linkNull
}
}
radio.uplink.let { uplink ->
if (uplink != null) {
radioUplink.text = String.format(Locale.ENGLISH, link, uplink / divider)
} else {
radioUplink.text = linkNull
}
}
if (radio.mode != null) {
radioMode.text = String.format(mode, radio.mode)
} else {
radioMode.text = String.format(mode, stringNo)
}
if (radio.isInverted) {
radioInverted.text = String.format(inverted, stringYes)
} else {
radioInverted.text = String.format(inverted, stringNo)
}
}
}
companion object {
fun from(parent: ViewGroup): TransHolder {
val inflater = LayoutInflater.from(parent.context)
return TransHolder(ItemRadioBinding.inflate(inflater, parent, false))
}
}
}
}
@@ -1,59 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.settingsScreen
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDialogFragment
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.DialogLocatorBinding
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.presentation.setNavResult
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class LocatorDialog : AppCompatDialogFragment() {
@Inject
lateinit var preferences: ISettingsManager
override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View? {
return inflater.inflate(R.layout.dialog_locator, group, false)
}
override fun onViewCreated(view: View, state: Bundle?) {
super.onViewCreated(view, state)
DialogLocatorBinding.bind(view).run {
dialog?.window?.setBackgroundDrawableResource(R.color.transparent)
dialog?.window?.setLayout(
(resources.displayMetrics.widthPixels * 0.94).toInt(),
WindowManager.LayoutParams.WRAP_CONTENT
)
locatorEdit.setText(preferences.loadStationLocator())
locatorBtnPos.setOnClickListener {
setNavResult("locator", locatorEdit.text.toString())
dismiss()
}
locatorBtnNeg.setOnClickListener { dismiss() }
}
}
}
@@ -1,71 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.settingsScreen
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDialogFragment
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.DialogPositionBinding
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.presentation.setNavResult
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class PositionDialog : AppCompatDialogFragment() {
@Inject
lateinit var preferences: ISettingsManager
override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View? {
return inflater.inflate(R.layout.dialog_position, group, false)
}
override fun onViewCreated(view: View, state: Bundle?) {
super.onViewCreated(view, state)
DialogPositionBinding.bind(view).run {
dialog?.window?.setBackgroundDrawableResource(R.color.transparent)
dialog?.window?.setLayout(
(resources.displayMetrics.widthPixels * 0.94).toInt(),
WindowManager.LayoutParams.WRAP_CONTENT
)
val location = preferences.loadStationPosition()
positionLatEdit.setText(location.lat.toString())
positionLonEdit.setText(location.lon.toString())
positionBtnPos.setOnClickListener {
val latitude = try {
positionLatEdit.text.toString().toDouble()
} catch (exception: Exception) {
180.0
}
val longitude = try {
positionLonEdit.text.toString().toDouble()
} catch (exception: Exception) {
400.0
}
setNavResult("position", Pair(latitude, longitude))
dismiss()
}
positionBtnNeg.setOnClickListener { dismiss() }
}
}
}
@@ -1,307 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.settingsScreen
import android.Manifest
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.widget.NestedScrollView
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.asLiveData
import androidx.navigation.fragment.findNavController
import com.rtbishop.look4sat.BuildConfig
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.FragmentSettingsBinding
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.presentation.clickWithDebounce
import com.rtbishop.look4sat.presentation.getNavResult
import com.rtbishop.look4sat.utility.isValidIPv4
import com.rtbishop.look4sat.utility.isValidPort
import dagger.hilt.android.AndroidEntryPoint
import java.text.SimpleDateFormat
import java.util.*
@AndroidEntryPoint
class SettingsFragment : Fragment(R.layout.fragment_settings) {
private val viewModel: SettingsViewModel by viewModels()
private val bluetooth = when {
Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH
else -> Manifest.permission.BLUETOOTH_CONNECT
}
private val bluetoothContract = ActivityResultContracts.RequestPermission()
private val bluetoothRequest = registerForActivityResult(bluetoothContract) { isGranted ->
if (!isGranted) {
showToast(getString(R.string.BTremote_perm_error))
toggleBTstate(isGranted)
}
}
private val locationFine = Manifest.permission.ACCESS_FINE_LOCATION
private val locationCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
private val locationContract = ActivityResultContracts.RequestMultiplePermissions()
private val locationRequest = registerForActivityResult(locationContract) { permissions ->
when {
permissions[locationFine] == true -> viewModel.setPositionFromGps()
permissions[locationCoarse] == true -> viewModel.setPositionFromNet()
else -> showToast(getString(R.string.location_gps_error))
}
}
private val contentContract = ActivityResultContracts.GetContent()
private val contentRequest = registerForActivityResult(contentContract) { uri ->
uri?.let { viewModel.updateFromFile(uri.toString()) }
}
private lateinit var binding: FragmentSettingsBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentSettingsBinding.bind(view).apply {
settingsBtnBack.clickWithDebounce { findNavController().navigateUp() }
settingsScroll.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, y, _, newY ->
if (y > newY) settingsFab.hide() else settingsFab.show()
})
settingsAbout.aboutVersion.text =
String.format(getString(R.string.app_version), BuildConfig.VERSION_NAME)
settingsBtnGithub.clickWithDebounce {
gotoUrl("https://github.com/rt-bishop/Look4Sat/")
}
settingsFab.clickWithDebounce {
gotoUrl("https://ko-fi.com/rt_bishop")
}
settingsBtnFdroid.clickWithDebounce {
gotoUrl("https://f-droid.org/en/packages/com.rtbishop.look4sat/")
}
}
setupLocationCard()
setupDataCard()
setupRemoteCard()
setupBTCard()
setupOtherCard()
setupOutroCard()
viewModel.stationPosition.asLiveData().observe(viewLifecycleOwner) { stationPos ->
stationPos?.let { handleStationPosition(stationPos) }
}
viewModel.getUpdateState().asLiveData().observe(viewLifecycleOwner) { updateState ->
updateState?.let { handleSatState(updateState) }
}
}
private fun setupLocationCard() {
binding.run {
setPositionText(viewModel.getStationPosition())
settingsLocation.locationBtnGps.clickWithDebounce {
locationRequest.launch(arrayOf(locationFine, locationCoarse))
}
settingsLocation.locationBtnManual.clickWithDebounce {
val action = SettingsFragmentDirections.globalToPosition()
findNavController().navigate(action)
}
settingsLocation.locationBtnQth.clickWithDebounce {
val action = SettingsFragmentDirections.globalToLocator()
findNavController().navigate(action)
}
getNavResult<Pair<Double, Double>>(R.id.nav_settings, "position") { position ->
viewModel.setStationPosition(position.first, position.second)
}
getNavResult<String>(R.id.nav_settings, "locator") { locator ->
viewModel.setPositionFromQth(locator)
}
}
}
private fun setupDataCard() {
binding.run {
setUpdateTime(viewModel.getLastUpdateTime())
settingsData.dataBtnWeb.clickWithDebounce { viewModel.updateFromWeb() }
settingsData.dataBtnFile.clickWithDebounce { contentRequest.launch("*/*") }
settingsData.dataBtnClear.clickWithDebounce { viewModel.clearAllData() }
viewModel.entriesTotal.observe(viewLifecycleOwner) { number ->
val entriesFormat = getString(R.string.data_entries)
settingsData.dataEntries.text = String.format(entriesFormat, number)
}
viewModel.radiosTotal.observe(viewLifecycleOwner) { number ->
val radiosFormat = getString(R.string.data_radios)
settingsData.dataRadios.text = String.format(radiosFormat, number)
}
getNavResult<List<String>>(R.id.nav_settings, "sources") {
viewModel.updateFromWeb()
}
}
}
private fun setupRemoteCard() {
binding.run {
settingsRemote.remoteSwitch.apply {
isChecked = viewModel.getRotatorEnabled()
settingsRemote.remoteIp.isEnabled = isChecked
settingsRemote.remoteIpEdit.setText(viewModel.getRotatorServer())
settingsRemote.remotePort.isEnabled = isChecked
settingsRemote.remotePortEdit.setText(viewModel.getRotatorPort())
setOnCheckedChangeListener { _, isChecked ->
viewModel.setRotatorEnabled(isChecked)
settingsRemote.remoteIp.isEnabled = isChecked
settingsRemote.remotePort.isEnabled = isChecked
}
}
settingsRemote.remoteIpEdit.doOnTextChanged { text, _, _, _ ->
if (text.toString().isValidIPv4()) viewModel.setRotatorServer(text.toString())
}
settingsRemote.remotePortEdit.doOnTextChanged { text, _, _, _ ->
if (text.toString().isValidPort()) viewModel.setRotatorPort(text.toString())
}
}
}
private fun setupBTCard() {
binding.run {
settingsBtremote.BTremoteSwitch.apply {
isChecked = viewModel.getBTEnabled()
settingsBtremote.BTremoteAddress.isEnabled = isChecked
settingsBtremote.BTAddressEdit.setText(viewModel.getBTDeviceAddr())
settingsBtremote.BTremoteFormat.isEnabled = isChecked
settingsBtremote.BTFormatEdit.setText(viewModel.getBTFormat())
setOnCheckedChangeListener { _, isChecked ->
toggleBTstate(isChecked)
bluetoothRequest.launch(bluetooth)
}
}
settingsBtremote.BTAddressEdit.doOnTextChanged { text, _, _, _ ->
viewModel.setBTDeviceAddr(text.toString())
}
settingsBtremote.BTFormatEdit.doOnTextChanged { text, _, _, _ ->
viewModel.setBTFormat(text.toString())
}
}
}
private fun toggleBTstate(value: Boolean) {
binding.run {
viewModel.setBTEnabled(value)
settingsBtremote.BTremoteSwitch.isChecked = value
settingsBtremote.BTremoteAddress.isEnabled = value
settingsBtremote.BTremoteFormat.isEnabled = value
}
}
private fun setupOtherCard() {
binding.run {
settingsOther.otherSwitchUtc.apply {
isChecked = viewModel.getUseUTC()
setOnCheckedChangeListener { _, isChecked -> viewModel.setUseUTC(isChecked) }
}
settingsOther.otherSwitchUpdate.apply {
isChecked = viewModel.getAutoUpdateEnabled()
setOnCheckedChangeListener { _, isChecked -> viewModel.setAutoUpdateEnabled(isChecked) }
}
settingsOther.otherSwitchSweep.apply {
isChecked = viewModel.getShowSweep()
setOnCheckedChangeListener { _, isChecked -> viewModel.setShowSweep(isChecked) }
}
settingsOther.otherSwitchSensors.apply {
isChecked = viewModel.getUseCompass()
setOnCheckedChangeListener { _, isChecked -> viewModel.setUseCompass(isChecked) }
}
}
}
private fun setupOutroCard() {
binding.run {
settingsOutro.outroThanks.movementMethod = LinkMovementMethod.getInstance()
settingsOutro.outroLicense.movementMethod = LinkMovementMethod.getInstance()
}
}
private fun handleStationPosition(pos: DataState<GeoPos>) {
when (pos) {
is DataState.Success -> {
setPositionText(pos.data)
binding.settingsLocation.locationProgress.isIndeterminate = false
viewModel.setPositionHandled()
showToast(getString(R.string.location_success))
}
is DataState.Error -> {
binding.settingsLocation.locationProgress.isIndeterminate = false
viewModel.setPositionHandled()
showToast(pos.message.toString())
}
DataState.Loading -> {
binding.settingsLocation.locationProgress.isIndeterminate = true
}
DataState.Handled -> {}
}
}
private fun setPositionText(geoPos: GeoPos) {
binding.run {
val latFormat = getString(R.string.location_lat)
val lonFormat = getString(R.string.location_lon)
settingsLocation.locationLat.text = String.format(latFormat, geoPos.lat)
settingsLocation.locationLon.text = String.format(lonFormat, geoPos.lon)
}
}
private fun handleSatState(state: DataState<Long>) {
when (state) {
is DataState.Success -> {
binding.settingsData.dataProgress.isIndeterminate = false
setUpdateTime(state.data)
viewModel.setUpdateHandled()
if (state.data == 0L) {
showToast(getString(R.string.data_clear_success))
} else {
showToast(getString(R.string.data_success))
}
}
is DataState.Error -> {
binding.settingsData.dataProgress.isIndeterminate = false
viewModel.setUpdateHandled()
showToast(getString(R.string.data_error))
}
is DataState.Loading -> {
binding.settingsData.dataProgress.isIndeterminate = true
}
is DataState.Handled -> {}
}
}
private fun setUpdateTime(updateTime: Long) {
val updatePattern = getString(R.string.data_update)
val updateDate = if (updateTime == 0L) {
getString(R.string.pass_placeholder)
} else {
SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault()).format(Date(updateTime))
}
binding.settingsData.dataUpdate.text = String.format(updatePattern, updateDate)
}
private fun showToast(message: String) {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
}
private fun gotoUrl(url: String) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
}
@@ -1,110 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.settingsScreen
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ILocationManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.GeoPos
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharedFlow
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val locationManager: ILocationManager,
private val repository: IDataRepository,
private val settings: ISettingsManager
) : ViewModel() {
val entriesTotal = repository.getEntriesTotal().asLiveData()
val radiosTotal = repository.getRadiosTotal().asLiveData()
fun updateFromFile(uri: String) = repository.updateFromFile(uri)
fun updateFromWeb() = repository.updateFromWeb()
fun clearAllData() = repository.clearAllData()
fun getUseUTC(): Boolean = settings.getUseUTC()
fun setUseUTC(value: Boolean) = settings.setUseUTC(value)
fun getLastUpdateTime(): Long = settings.getLastUpdateTime()
fun getAutoUpdateEnabled(): Boolean = settings.getAutoUpdateEnabled()
fun setAutoUpdateEnabled(value: Boolean) = settings.setAutoUpdateEnabled(value)
fun getUseCompass(): Boolean = settings.getUseCompass()
fun setUseCompass(value: Boolean) = settings.setUseCompass(value)
fun getShowSweep(): Boolean = settings.getShowSweep()
fun setShowSweep(value: Boolean) = settings.setShowSweep(value)
fun getRotatorEnabled(): Boolean = settings.getRotatorEnabled()
fun setRotatorEnabled(value: Boolean) = settings.setRotatorEnabled(value)
fun getRotatorServer(): String = settings.getRotatorServer()
fun setRotatorServer(value: String) = settings.setRotatorServer(value)
fun getRotatorPort(): String = settings.getRotatorPort()
fun setRotatorPort(value: String) = settings.setRotatorPort(value)
fun getBTEnabled(): Boolean = settings.getBTEnabled()
fun setBTEnabled(value: Boolean) = settings.setBTEnabled(value)
fun getBTFormat(): String = settings.getBTFormat()
fun setBTFormat(value: String) = settings.setBTFormat(value)
// fun getBTDeviceName(): String = settings.getBTDeviceName()
// fun setBTDeviceName(value: String) = settings.setBTDeviceName(value)
fun getBTDeviceAddr(): String = settings.getBTDeviceAddr()
fun setBTDeviceAddr(value: String) = settings.setBTDeviceAddr(value)
fun getUpdateState() = repository.updateState
fun setUpdateHandled() = repository.setUpdateStateHandled()
val stationPosition: SharedFlow<DataState<GeoPos>> = locationManager.stationPosition
fun getStationPosition(): GeoPos = locationManager.getStationPosition()
fun setStationPosition(lat: Double, lon: Double) = locationManager.setStationPosition(lat, lon)
fun setPositionFromGps() = locationManager.setPositionFromGps()
fun setPositionFromNet() = locationManager.setPositionFromNet()
fun setPositionFromQth(qthString: String) = locationManager.setPositionFromQth(qthString)
fun setPositionHandled() = locationManager.setPositionHandled()
}
-11
View File
@@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
</vector>
-11
View File
@@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
</vector>
-11
View File
@@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zM22.24,5.59L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z" />
</vector>
@@ -1,4 +0,0 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/background" />
</shape>
@@ -1,21 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:autoMirrored="true"
android:viewportWidth="271.2439"
android:viewportHeight="271.2439">
<group
android:translateX="1.3562195"
android:translateY="1.3562195">
<group
android:translateX="70.26573"
android:translateY="70.26573">
<path
android:fillColor="@color/accent"
android:pathData="m96.642,31.504c7.911,7.997 17.143,13.584 25.365,15.581l1.754,-3.997C123.78,43.048 123.786,43.005 123.802,42.965 125.834,38.995 127,34.495 127,29.715 127,13.881 114.379,1 98.867,1c-5.125,0 -9.92,1.429 -14.064,3.884l-3.398,1.598c0.08,0.306 0.133,0.599 0.221,0.91 2.32,8.104 7.652,16.667 15.016,24.112z" />
<path
android:fillColor="@color/accent"
android:pathData="M93.876,34.3C86.053,26.391 80.369,17.225 77.87,8.49 77.84,8.385 77.818,8.285 77.788,8.181L2.629,43.513c-1.404,0.66 -2.019,2.358 -1.372,3.791 0.472,1.046 1.485,1.663 2.545,1.663 0.392,0 0.79,-0.084 1.169,-0.263L74.045,16.235c-1.873,3.578 -2.997,7.611 -3.228,11.885L35.566,51.732c-1.294,0.866 -1.653,2.638 -0.805,3.957 0.538,0.836 1.431,1.29 2.344,1.29 0.526,0 1.06,-0.151 1.533,-0.469L71.189,34.707c1.4,8.11 6.135,15.044 12.715,19.285l-54.945,68.337c-0.981,1.22 -0.808,3.021 0.388,4.023 0.521,0.437 1.149,0.649 1.775,0.649 0.809,0 1.611,-0.357 2.166,-1.045L89.056,56.595c3.06,1.168 6.357,1.838 9.811,1.838 4.932,0 9.566,-1.312 13.601,-3.597L84.602,118.323c-0.633,1.44 0,3.131 1.41,3.777 0.372,0.17 0.76,0.25 1.144,0.25 1.07,0 2.092,-0.629 2.556,-1.691L120.402,50.739C111.642,48.416 102.042,42.555 93.876,34.3Z" />
</group>
</group>
</vector>
-11
View File
@@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M2,21h19v-3H2v3zM20,8H3c-0.55,0 -1,0.45 -1,1v6c0,0.55 0.45,1 1,1h17c0.55,0 1,-0.45 1,-1V9c0,-0.55 -0.45,-1 -1,-1zM2,3v3h19V3H2z" />
</vector>
-11
View File
@@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z" />
</vector>
-23
View File
@@ -1,23 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:fillColor="@android:color/white"
android:pathData="m105.138,114.121a2.864,2.864 90,0 0,1.701 -0.85l19.334,-19.331a2.864,2.864 90,0 0,-0 -4.027l-26.584,-26.625a2.864,2.864 90,0 0,-1.835 -0.805l-7.922,-0.626 5.773,-5.772a2.864,2.864 90,0 0,-0 -4.027l-19.602,-19.644a2.864,2.864 90,0 0,-4.073 -0l-5.773,5.772 -0.627,-7.92a2.864,2.864 90,0 0,-0.805 -1.835l-26.629,-26.58a2.864,2.864 90,0 0,-4.073 -0l-19.289,19.286a2.864,2.864 90,0 0,-0 4.072l26.584,26.625a2.864,2.864 90,0 0,1.835 0.805l7.922,0.626 -5.773,5.772a2.864,2.864 90,0 0,-0 4.072l19.603,19.599a2.864,2.864 90,0 0,4.073 -0l5.773,-5.772 0.626,7.92a2.864,2.864 90,0 0,0.806 1.835l26.629,26.58a2.864,2.864 90,0 0,2.059 0.85,2.864 2.864,90 0,0 0.269,-0zM104.825,107.23 L80.97,83.38 80.165,72.685 85.58,67.271 96.276,68.121 120.13,91.926zM55.326,47.85 L44.629,47 20.775,23.194 36.081,7.891 59.891,31.741 60.741,42.436z" />
<path
android:fillColor="@android:color/white"
android:pathData="m62.653,115.806a2.864,2.864 90,1 0,-0 -5.727c-11.457,-0 -22.876,-4.351 -31.634,-13.107 -8.741,-8.74 -13.095,-20.174 -13.095,-31.614a2.864,2.864 90,1 0,-5.728 -0c-0,12.897 4.935,25.829 14.773,35.664 9.85,9.848 22.772,14.785 35.684,14.785z" />
<path
android:fillColor="@android:color/white"
android:pathData="m62.653,127a2.864,2.864 90,1 0,-0 -5.727c-14.325,-0 -28.612,-5.441 -39.547,-16.375 -10.935,-10.933 -16.378,-25.233 -16.378,-39.541a2.864,2.864 90,1 0,-5.728 -0c-0,15.763 6.025,31.562 18.055,43.59 12.029,12.028 27.818,18.052 43.597,18.052z" />
<path
android:fillColor="@android:color/white"
android:pathData="m62.653,104.698a2.864,2.864 90,1 0,-0 -5.727c-8.62,-0 -17.201,-3.263 -23.778,-9.839 -6.577,-6.576 -9.855,-15.174 -9.855,-23.774a2.864,2.864 90,1 0,-5.728 -0c-0,10.061 3.861,20.153 11.533,27.824 7.672,7.671 17.756,11.517 27.829,11.517z" />
<path
android:fillColor="@android:color/white"
android:pathData="m62.653,93.475a2.864,2.864 90,1 0,-0 -5.727c-5.737,-0 -11.453,-2.174 -15.836,-6.557 -4.384,-4.383 -6.558,-10.098 -6.558,-15.834a2.864,2.864 90,1 0,-5.728 -0c-0,7.192 2.757,14.406 8.236,19.883 5.479,5.478 12.693,8.234 19.887,8.234z" />
</vector>
-11
View File
@@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M8,6v14l11,-7z" />
</vector>
-10
View File
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/accent"
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z" />
</vector>
-11
View File
@@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M16,6v14l-11,-7z" />
</vector>
-11
View File
@@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M19,8l-4,4h3c0,3.31 -2.69,6 -6,6 -1.01,0 -1.97,-0.25 -2.8,-0.7l-1.46,1.46C8.97,19.54 10.43,20 12,20c4.42,0 8,-3.58 8,-8h3l-4,-4zM6,12c0,-3.31 2.69,-6 6,-6 1.01,0 1.97,0.25 2.8,0.7l1.46,-1.46C15.03,4.46 13.57,4 12,4c-4.42,0 -8,3.58 -8,8H1l4,4 4,-4H6z" />
</vector>
@@ -1,14 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:fillColor="@android:color/white"
android:pathData="m96.642,31.504c7.911,7.997 17.143,13.584 25.365,15.581l1.754,-3.997C123.78,43.048 123.786,43.005 123.802,42.965 125.834,38.995 127,34.495 127,29.715 127,13.881 114.379,1 98.867,1c-5.125,0 -9.92,1.429 -14.064,3.884l-3.398,1.598c0.08,0.306 0.133,0.599 0.221,0.91 2.32,8.104 7.652,16.667 15.016,24.112z" />
<path
android:fillColor="@android:color/white"
android:pathData="M93.876,34.3C86.053,26.391 80.369,17.225 77.87,8.49 77.84,8.385 77.818,8.285 77.788,8.181L2.629,43.513c-1.404,0.66 -2.019,2.358 -1.372,3.791 0.472,1.046 1.485,1.663 2.545,1.663 0.392,0 0.79,-0.084 1.169,-0.263L74.045,16.235c-1.873,3.578 -2.997,7.611 -3.228,11.885L35.566,51.732c-1.294,0.866 -1.653,2.638 -0.805,3.957 0.538,0.836 1.431,1.29 2.344,1.29 0.526,0 1.06,-0.151 1.533,-0.469L71.189,34.707c1.4,8.11 6.135,15.044 12.715,19.285l-54.945,68.337c-0.981,1.22 -0.808,3.021 0.388,4.023 0.521,0.437 1.149,0.649 1.775,0.649 0.809,0 1.611,-0.357 2.166,-1.045L89.056,56.595c3.06,1.168 6.357,1.838 9.811,1.838 4.932,0 9.566,-1.312 13.601,-3.597L84.602,118.323c-0.633,1.44 0,3.131 1.41,3.777 0.372,0.17 0.76,0.25 1.144,0.25 1.07,0 2.092,-0.629 2.556,-1.691L120.402,50.739C111.642,48.416 102.042,42.555 93.876,34.3Z" />
</vector>
-13
View File
@@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="192dp"
android:height="200dp"
android:viewportWidth="210"
android:viewportHeight="220">
<group
android:translateX="52"
android:translateY="44">
<path
android:fillColor="@color/accent"
android:pathData="m104.005,111.936q0.504,0 0.958,-0.063 0.467,-0.076 0.87,-0.176l0,1.674q-0.202,0.101 -0.555,0.214 -0.34,0.113 -0.807,0.201 -0.454,0.101 -1.034,0.164 -0.58,0.063 -1.261,0.063 -0.87,0 -1.576,-0.214 -0.706,-0.201 -1.198,-0.68 -0.492,-0.491 -0.757,-1.259 -0.265,-0.78 -0.265,-1.913l0,-8.244l-1.942,0l0,-1.322q0.832,0 1.488,-0.327 0.656,-0.327 1.047,-0.755 0.794,-0.831 1.185,-2.555l2.144,0l0,3.197l3.304,0l0,1.762l-3.304,0l0,7.98q0,1.171 0.391,1.712 0.404,0.541 1.311,0.541zM85.595,109.922q0,1.108 0.353,1.661 0.353,0.554 1.11,0.554 0.555,0 0.996,-0.227 0.454,-0.239 0.769,-0.667 0.328,-0.428 0.492,-1.032 0.177,-0.617 0.177,-1.372l0,-1.938l-1.16,0.076q-0.769,0.038 -1.299,0.252 -0.53,0.201 -0.845,0.566 -0.315,0.365 -0.454,0.894 -0.139,0.529 -0.139,1.233zM87.89,101.188q-0.492,0 -0.832,0.214 -0.328,0.201 -0.53,0.579 -0.202,0.365 -0.29,0.881 -0.076,0.503 -0.076,1.108 -1.74,0 -2.623,-0.39 -0.87,-0.39 -0.87,-1.334 0,-0.705 0.416,-1.208 0.429,-0.503 1.147,-0.818 0.731,-0.315 1.69,-0.453 0.971,-0.151 2.068,-0.151 1.362,0 2.371,0.239 1.021,0.227 1.69,0.743 0.681,0.516 1.021,1.359 0.34,0.831 0.34,2.039l0,6.57q0,0.529 0.076,0.881 0.088,0.352 0.265,0.566 0.177,0.201 0.479,0.289 0.303,0.088 0.731,0.088l0.101,0l0,1.372l-4.804,0l-0.542,-1.775l-0.227,0q-0.429,0.516 -0.807,0.906 -0.378,0.378 -0.807,0.629 -0.429,0.252 -0.971,0.365 -0.53,0.126 -1.261,0.126 -0.857,0 -1.601,-0.252 -0.731,-0.252 -1.286,-0.768 -0.542,-0.529 -0.857,-1.309 -0.315,-0.793 -0.315,-1.863 0,-2.102 1.438,-3.109 1.45,-1.007 4.35,-1.095l2.118,-0.076l0,-1.422q0,-0.642 -0.05,-1.183 -0.05,-0.541 -0.214,-0.931 -0.164,-0.39 -0.492,-0.604 -0.315,-0.214 -0.845,-0.214zM72.115,114.013q-1.74,0 -2.9,-0.327 -1.16,-0.327 -1.866,-0.843 -0.694,-0.516 -0.996,-1.158 -0.29,-0.642 -0.29,-1.271 0,-0.667 0.24,-1.12 0.252,-0.453 0.656,-0.743 0.416,-0.289 0.933,-0.415 0.53,-0.126 1.097,-0.126 0,1.108 0.265,1.951 0.265,0.831 0.731,1.397 0.467,0.554 1.11,0.843 0.656,0.277 1.425,0.277 0.757,0 1.349,-0.214 0.593,-0.214 1.009,-0.592 0.416,-0.378 0.63,-0.881 0.214,-0.516 0.214,-1.108 0,-0.654 -0.265,-1.158 -0.265,-0.516 -0.807,-0.944 -0.53,-0.441 -1.349,-0.831 -0.807,-0.403 -1.891,-0.856 -1.349,-0.554 -2.282,-1.133 -0.921,-0.592 -1.488,-1.271 -0.567,-0.68 -0.82,-1.473 -0.24,-0.793 -0.24,-1.737 0,-1.183 0.479,-2.14 0.479,-0.957 1.324,-1.636 0.845,-0.68 2.005,-1.045 1.16,-0.365 2.522,-0.365 1.425,0 2.459,0.239 1.034,0.227 1.702,0.629 0.681,0.403 1.009,0.957 0.328,0.541 0.328,1.171 0,0.453 -0.202,0.843 -0.189,0.39 -0.593,0.68 -0.391,0.289 -0.996,0.453 -0.593,0.164 -1.387,0.164 0,-0.541 -0.151,-1.158 -0.139,-0.629 -0.467,-1.158 -0.328,-0.541 -0.857,-0.894 -0.517,-0.352 -1.261,-0.352 -0.517,0 -0.984,0.151 -0.467,0.151 -0.82,0.453 -0.34,0.302 -0.542,0.755 -0.202,0.441 -0.202,1.019 0,0.541 0.177,1.019 0.177,0.478 0.643,0.944 0.479,0.453 1.299,0.919 0.832,0.453 2.144,0.982 1.324,0.541 2.257,1.108 0.933,0.566 1.526,1.221 0.605,0.642 0.87,1.422 0.277,0.768 0.277,1.712 0,1.246 -0.492,2.291 -0.492,1.032 -1.412,1.775 -0.921,0.743 -2.219,1.158 -1.286,0.415 -2.9,0.415zM62.115,108.966l0,1.573q0,0.604 0.164,0.969 0.177,0.365 0.454,0.566 0.29,0.189 0.681,0.252 0.391,0.063 0.82,0.063l0.328,0l0,1.372l-9.054,0l0,-1.372l0.719,0q0.441,0 0.82,-0.063 0.391,-0.063 0.668,-0.252 0.29,-0.201 0.454,-0.566 0.177,-0.365 0.177,-0.969l0,-1.573l-7.15,0l0,-1.573l7.326,-12.032l3.594,0l0,11.906l2.686,0l0,1.699zM58.345,102.761q0,-1.133 0.038,-2.442 0.038,-1.309 0.139,-2.517 -0.126,0.302 -0.404,0.831 -0.277,0.529 -0.593,1.095 -0.315,0.554 -0.605,1.057 -0.29,0.491 -0.441,0.73l-3.48,5.752l5.347,0zM50.855,113.761l-0.366,0q-1.412,0 -2.371,-0.063 -0.958,-0.063 -1.652,-0.289 -0.694,-0.227 -1.198,-0.68 -0.504,-0.466 -1.009,-1.259l-2.295,-3.612 -1.034,0.768l0,2.089q0,0.554 0.177,0.881 0.189,0.327 0.454,0.503 0.277,0.176 0.605,0.239 0.328,0.05 0.605,0.05l0.303,0l0,1.372L34.866,113.761l0,-1.372l0.303,0q0.29,0 0.605,-0.05 0.328,-0.063 0.593,-0.239 0.277,-0.176 0.454,-0.503 0.189,-0.327 0.189,-0.881l0,-13.606q0,-0.516 -0.189,-0.818 -0.177,-0.315 -0.454,-0.478 -0.277,-0.176 -0.605,-0.227 -0.315,-0.05 -0.593,-0.05l-0.303,0l0,-1.359l6.065,0l0,9.943q0,0.352 -0.025,0.818 -0.013,0.453 -0.038,0.868 -0.038,0.491 -0.063,0.994l3.253,-3.637q0.429,-0.478 0.555,-0.755 0.139,-0.289 0.139,-0.478 0,-0.327 -0.353,-0.503 -0.34,-0.189 -0.984,-0.189l0,-1.296l6.507,0l0,1.296q-0.895,0 -1.765,0.529 -0.857,0.516 -1.879,1.661l-1.627,1.85 3.43,5.11q0.681,1.019 1.374,1.51 0.694,0.491 1.311,0.491l0.088,0zM33.517,106.31q0,1.007 -0.29,1.901 -0.29,0.894 -0.82,1.573 -0.517,0.667 -1.261,1.057 -0.731,0.39 -1.627,0.39 -1.198,0 -2.333,-0.73 -1.122,-0.743 -2.055,-2.417 -0.794,1.523 -1.828,2.303 -1.034,0.768 -2.181,0.768 -0.883,0 -1.627,-0.34 -0.744,-0.352 -1.286,-0.982 -0.542,-0.642 -0.845,-1.548 -0.29,-0.919 -0.29,-2.052 0,-1.032 0.29,-1.926 0.29,-0.906 0.82,-1.561 0.53,-0.667 1.274,-1.045 0.744,-0.39 1.664,-0.39 0.593,0 1.173,0.176 0.593,0.164 1.147,0.541 0.567,0.365 1.072,0.944 0.517,0.579 0.946,1.41 0.794,-1.498 1.791,-2.24 0.996,-0.755 2.295,-0.755Line truncated
</group>
</vector>
-11
View File
@@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M14,21c1.93,0 3.62,-1.17 4,-3l-1.75,-0.88C16,18.21 15.33,19 14,19l-4.9,0c0.83,-1 1.5,-2.34 1.5,-4c0,-0.35 -0.03,-0.69 -0.08,-1L14,14v-2l-4.18,0C9,10.42 8,9.6 8,8c0,-1.93 1.57,-3.5 3.5,-3.5c1.5,0 2.79,0.95 3.28,2.28L16.63,6c-0.8,-2.05 -2.79,-3.5 -5.13,-3.5C8.46,2.5 6,4.96 6,8c0,1.78 0.79,2.9 1.49,4L6,12v2l2.47,0c0.08,0.31 0.13,0.64 0.13,1c0,2.7 -2.6,4 -2.6,4v2H14z" />
</vector>
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size
android:width="1dp"
android:height="1dp" />
<solid android:color="@color/background" />
</shape>
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
-73
View File
@@ -1,73 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/SurfaceCard">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/about_img"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:contentDescription="@string/app_name"
android:src="@drawable/ic_satellite"
app:layout_constraintBottom_toBottomOf="@+id/about_version"
app:layout_constraintDimensionRatio="h,1:1"
app:layout_constraintEnd_toStartOf="@+id/about_title"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/about_title"
app:tint="@color/accent" />
<TextView
android:id="@+id/about_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="8dp"
android:includeFontPadding="false"
android:text="@string/app_name"
android:textColor="@color/accent"
android:textSize="@dimen/text_size_app_title"
app:layout_constraintBottom_toTopOf="@+id/about_version"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/about_img"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/about_version"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/app_version"
android:textColor="@color/textMain"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toTopOf="@+id/about_subtitle"
app:layout_constraintEnd_toEndOf="@+id/about_title"
app:layout_constraintStart_toStartOf="@+id/about_title"
app:layout_constraintTop_toBottomOf="@+id/about_title" />
<TextView
android:id="@+id/about_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="12dp"
android:text="@string/app_subtitle"
android:textColor="@color/textMain"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/about_version" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
-74
View File
@@ -1,74 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/SurfaceCard">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/BTremote_title"
style="@style/SettingsTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="12dp"
android:text="@string/BTremote_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/BTremote_switch"
style="@style/SettingsText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="42dp"
android:text="@string/BTremote_switch"
app:layout_constraintEnd_toEndOf="@+id/BTremote_title"
app:layout_constraintStart_toStartOf="@+id/BTremote_title"
app:layout_constraintTop_toBottomOf="@+id/BTremote_title"
app:trackTint="@color/textDisabled" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/BTremote_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="@+id/BTremote_switch"
app:layout_constraintStart_toStartOf="@+id/BTremote_switch"
app:layout_constraintTop_toBottomOf="@id/BTremote_switch">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/BT_address_edit"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/BTremote_device_hint"
android:textColorHint="@color/textMain" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/BTremote_format"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginBottom="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/BTremote_address"
app:layout_constraintStart_toStartOf="@+id/BTremote_address"
app:layout_constraintTop_toBottomOf="@id/BTremote_address">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/BT_format_edit"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/BTremote_output_hint"
android:textColorHint="@color/textMain" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
-107
View File
@@ -1,107 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/SurfaceCard">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/data_title"
style="@style/SettingsTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="10dp"
android:text="@string/data_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/data_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="12dp"
app:layout_constraintBottom_toBottomOf="@+id/data_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/data_title"
app:layout_constraintTop_toTopOf="@+id/data_title" />
<TextView
android:id="@+id/data_update"
style="@style/SettingsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/data_update"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="@+id/data_title"
app:layout_constraintTop_toBottomOf="@+id/data_title" />
<TextView
android:id="@+id/data_entries"
style="@style/SettingsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/data_entries"
app:layout_constraintEnd_toStartOf="@+id/data_radios"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="@+id/data_update"
app:layout_constraintTop_toBottomOf="@+id/data_update" />
<TextView
android:id="@+id/data_radios"
style="@style/SettingsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/data_radios"
app:layout_constraintBaseline_toBaselineOf="@+id/data_entries"
app:layout_constraintEnd_toEndOf="@+id/data_progress"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/data_entries" />
<Button
android:id="@+id/data_btn_web"
style="@style/NormalButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
android:text="@string/btn_web"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/data_btn_file"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/data_entries" />
<Button
android:id="@+id/data_btn_file"
style="@style/NormalButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:text="@string/btn_import"
app:layout_constraintBaseline_toBaselineOf="@+id/data_btn_web"
app:layout_constraintEnd_toStartOf="@+id/data_btn_clear"
app:layout_constraintStart_toEndOf="@+id/data_btn_web" />
<Button
android:id="@+id/data_btn_clear"
style="@style/NormalButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:text="@string/btn_clear"
app:layout_constraintBaseline_toBaselineOf="@+id/data_btn_web"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/data_btn_file" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
-99
View File
@@ -1,99 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/SurfaceCard">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/location_title"
style="@style/SettingsTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="10dp"
android:text="@string/location_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/location_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="12dp"
app:layout_constraintBottom_toBottomOf="@+id/location_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/location_title"
app:layout_constraintTop_toTopOf="@+id/location_title" />
<TextView
android:id="@+id/location_lat"
style="@style/SettingsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/location_lat"
app:layout_constraintEnd_toStartOf="@+id/location_lon"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="@+id/location_title"
app:layout_constraintTop_toBottomOf="@+id/location_title" />
<TextView
android:id="@+id/location_lon"
style="@style/SettingsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/location_lon"
app:layout_constraintBaseline_toBaselineOf="@+id/location_lat"
app:layout_constraintEnd_toEndOf="@+id/location_progress"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/location_lat" />
<Button
android:id="@+id/location_btn_gps"
style="@style/NormalButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
android:text="@string/btn_gps"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/location_btn_manual"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/location_lat" />
<Button
android:id="@+id/location_btn_manual"
style="@style/NormalButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:text="@string/btn_manual"
app:layout_constraintBaseline_toBaselineOf="@+id/location_btn_gps"
app:layout_constraintEnd_toStartOf="@+id/location_btn_qth"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/location_btn_gps" />
<Button
android:id="@+id/location_btn_qth"
style="@style/NormalButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:text="@string/btn_qth"
app:layout_constraintBaseline_toBaselineOf="@+id/location_btn_gps"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/location_btn_manual" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
-81
View File
@@ -1,81 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/SurfaceCard">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/other_title"
style="@style/SettingsTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="12dp"
android:text="@string/other_title"
app:layout_constraintBottom_toTopOf="@+id/other_switch_utc"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/other_switch_utc"
style="@style/SettingsText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:minHeight="42dp"
android:text="@string/other_switch_utc"
app:layout_constraintBottom_toTopOf="@+id/other_switch_update"
app:layout_constraintEnd_toEndOf="@+id/other_title"
app:layout_constraintStart_toStartOf="@+id/other_title"
app:layout_constraintTop_toBottomOf="@+id/other_title"
app:trackTint="@color/textDisabled" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/other_switch_update"
style="@style/SettingsText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="42dp"
android:text="@string/other_switch_update"
app:layout_constraintBottom_toTopOf="@+id/other_switch_sweep"
app:layout_constraintEnd_toEndOf="@+id/other_switch_utc"
app:layout_constraintStart_toStartOf="@+id/other_switch_utc"
app:layout_constraintTop_toBottomOf="@+id/other_switch_utc"
app:trackTint="@color/textDisabled" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/other_switch_sweep"
style="@style/SettingsText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="42dp"
android:text="@string/other_switch_sweep"
app:layout_constraintBottom_toTopOf="@+id/other_switch_sensors"
app:layout_constraintEnd_toEndOf="@+id/other_switch_update"
app:layout_constraintStart_toStartOf="@+id/other_switch_update"
app:layout_constraintTop_toBottomOf="@+id/other_switch_update"
app:trackTint="@color/textDisabled" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/other_switch_sensors"
style="@style/SettingsText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:minHeight="42dp"
android:text="@string/other_switch_sensors"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/other_switch_sweep"
app:layout_constraintStart_toStartOf="@+id/other_switch_sweep"
app:layout_constraintTop_toBottomOf="@+id/other_switch_sweep"
app:trackTint="@color/textDisabled" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
-53
View File
@@ -1,53 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/SurfaceCard">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/outro_title"
style="@style/SettingsTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="12dp"
android:text="@string/outro_title"
app:layout_constraintBottom_toTopOf="@+id/outro_thanks"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/outro_thanks"
style="@style/SettingsText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/outro_thanks"
app:layout_constraintBottom_toTopOf="@+id/outro_license"
app:layout_constraintEnd_toEndOf="@+id/outro_title"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/outro_title"
app:layout_constraintTop_toBottomOf="@+id/outro_title" />
<TextView
android:id="@+id/outro_license"
style="@style/SettingsText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="10dp"
android:text="@string/outro_license"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/outro_title"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/outro_title"
app:layout_constraintTop_toBottomOf="@+id/outro_thanks" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
-79
View File
@@ -1,79 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/SurfaceCard">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/remote_title"
style="@style/SettingsTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="12dp"
android:text="@string/remote_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/remote_switch"
style="@style/SettingsText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="42dp"
android:text="@string/remote_switch"
app:layout_constraintEnd_toEndOf="@+id/remote_title"
app:layout_constraintStart_toStartOf="@+id/remote_title"
app:layout_constraintTop_toBottomOf="@+id/remote_title"
app:trackTint="@color/textDisabled" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/remote_ip"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginBottom="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/remote_port"
app:layout_constraintStart_toStartOf="@+id/remote_switch"
app:layout_constraintTop_toBottomOf="@+id/remote_switch"
app:layout_constraintWidth_percent="0.64">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/remote_ip_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:digits="0123456789."
android:hint="@string/remote_ip_hint"
android:inputType="number"
android:textColorHint="@color/textMain" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/remote_port"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/remote_ip"
app:layout_constraintEnd_toEndOf="@+id/remote_switch"
app:layout_constraintStart_toEndOf="@+id/remote_ip"
app:layout_constraintTop_toTopOf="@+id/remote_ip">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/remote_port_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/remote_port_hint"
android:inputType="number"
android:textColorHint="@color/textMain" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
-131
View File
@@ -1,131 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/SurfaceCard"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/filter_title"
style="@style/DialogTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:minHeight="48dp"
android:text="@string/filter_title"
app:layout_constraintBottom_toTopOf="@+id/filter_hours_text"
app:layout_constraintEnd_toEndOf="@+id/filter_btn_pos"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/filter_btn_neg"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/filter_hours_text"
style="@style/DialogText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="@string/filter_hours_text"
app:layout_constraintBottom_toTopOf="@+id/filter_hours_layout"
app:layout_constraintEnd_toEndOf="@+id/filter_title"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/filter_title"
app:layout_constraintTop_toBottomOf="@+id/filter_title" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/filter_hours_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/filter_hours_hint"
android:importantForAutofill="no"
android:inputType="number"
android:minHeight="48dp"
app:endIconMode="clear_text"
app:layout_constraintBottom_toTopOf="@+id/filter_elev_text"
app:layout_constraintEnd_toEndOf="@+id/filter_hours_text"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/filter_hours_text"
app:layout_constraintTop_toBottomOf="@+id/filter_hours_text">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/filter_hours_edit"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/filter_elev_text"
style="@style/DialogText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/filter_elev_text"
app:layout_constraintBottom_toTopOf="@+id/filter_elev_layout"
app:layout_constraintEnd_toEndOf="@+id/filter_hours_text"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/filter_hours_text"
app:layout_constraintTop_toBottomOf="@+id/filter_hours_layout" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/filter_elev_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/filter_elev_hint"
android:importantForAutofill="no"
android:inputType="number|numberDecimal"
android:minHeight="48dp"
app:endIconMode="clear_text"
app:layout_constraintBottom_toTopOf="@+id/filter_btn_neg"
app:layout_constraintEnd_toEndOf="@+id/filter_hours_layout"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/filter_hours_layout"
app:layout_constraintTop_toBottomOf="@+id/filter_elev_text">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/filter_elev_edit"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/filter_btn_neg"
style="@style/NormalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="6dp"
android:text="@string/btn_cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/filter_btn_pos"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/filter_elev_layout" />
<Button
android:id="@+id/filter_btn_pos"
style="@style/NormalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:text="@string/btn_accept"
app:layout_constraintBaseline_toBaselineOf="@+id/filter_btn_neg"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/filter_btn_neg" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
@@ -1,94 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/SurfaceCard"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/locator_title"
style="@style/DialogTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:minHeight="48dp"
android:text="@string/locator_title"
app:layout_constraintBottom_toTopOf="@+id/locator_text"
app:layout_constraintEnd_toEndOf="@+id/locator_btn_pos"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/locator_btn_neg"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/locator_text"
style="@style/DialogText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="@string/locator_text"
app:layout_constraintBottom_toTopOf="@+id/locator_layout"
app:layout_constraintEnd_toEndOf="@+id/locator_title"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/locator_title"
app:layout_constraintTop_toBottomOf="@+id/locator_title" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/locator_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/locator_hint"
android:importantForAutofill="no"
android:inputType="number"
android:minHeight="48dp"
app:endIconMode="clear_text"
app:layout_constraintBottom_toTopOf="@+id/locator_btn_neg"
app:layout_constraintEnd_toEndOf="@+id/locator_text"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/locator_text"
app:layout_constraintTop_toBottomOf="@+id/locator_text">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/locator_edit"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/locator_btn_neg"
style="@style/NormalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="6dp"
android:text="@string/btn_cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/locator_btn_pos"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/locator_layout" />
<Button
android:id="@+id/locator_btn_pos"
style="@style/NormalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:text="@string/btn_accept"
app:layout_constraintBaseline_toBaselineOf="@+id/locator_btn_neg"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/locator_btn_neg" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
-66
View File
@@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/SurfaceCard"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/modes_title"
style="@style/DialogTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/modes_title"
android:minHeight="48dp"
app:layout_constraintEnd_toEndOf="@+id/modes_btn_pos"
app:layout_constraintStart_toStartOf="@+id/modes_btn_neg"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/modes_recycler"
style="@style/Recycler"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="4dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@+id/modes_btn_neg"
app:layout_constraintEnd_toEndOf="@+id/modes_btn_pos"
app:layout_constraintStart_toStartOf="@+id/modes_btn_neg"
app:layout_constraintTop_toBottomOf="@+id/modes_title" />
<Button
android:id="@+id/modes_btn_neg"
style="@style/NormalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginBottom="6dp"
android:text="@string/btn_cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/modes_btn_pos"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/modes_btn_pos"
style="@style/NormalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:text="@string/btn_accept"
app:layout_constraintBaseline_toBaselineOf="@+id/modes_btn_neg"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/modes_btn_neg" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
-133
View File
@@ -1,133 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/SurfaceCard"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/position_title"
style="@style/DialogTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:minHeight="48dp"
android:text="@string/position_title"
app:layout_constraintBottom_toTopOf="@+id/position_lat_text"
app:layout_constraintEnd_toEndOf="@+id/position_btn_pos"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/position_btn_neg"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/position_lat_text"
style="@style/DialogText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="@string/position_lat_text"
app:layout_constraintBottom_toTopOf="@+id/position_lat_layout"
app:layout_constraintEnd_toEndOf="@+id/position_title"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/position_title"
app:layout_constraintTop_toBottomOf="@+id/position_title" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/position_lat_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/position_lat_hint"
android:importantForAutofill="no"
android:inputType="number"
android:minHeight="48dp"
app:endIconMode="clear_text"
app:layout_constraintBottom_toTopOf="@+id/position_lon_text"
app:layout_constraintEnd_toEndOf="@+id/position_lat_text"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/position_lat_text"
app:layout_constraintTop_toBottomOf="@+id/position_lat_text">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/position_lat_edit"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:digits="0123456789.-"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/position_lon_text"
style="@style/DialogText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/position_lon_text"
app:layout_constraintBottom_toTopOf="@+id/position_lon_layout"
app:layout_constraintEnd_toEndOf="@+id/position_lat_text"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/position_lat_text"
app:layout_constraintTop_toBottomOf="@+id/position_lat_layout" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/position_lon_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/position_lon_hint"
android:importantForAutofill="no"
android:inputType="number|numberDecimal"
android:minHeight="48dp"
app:endIconMode="clear_text"
app:layout_constraintBottom_toTopOf="@+id/position_btn_neg"
app:layout_constraintEnd_toEndOf="@+id/position_lat_layout"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="@+id/position_lat_layout"
app:layout_constraintTop_toBottomOf="@+id/position_lon_text">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/position_lon_edit"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:digits="0123456789.-"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/position_btn_neg"
style="@style/NormalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="6dp"
android:text="@string/btn_cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/position_btn_pos"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/position_lon_layout" />
<Button
android:id="@+id/position_btn_pos"
style="@style/NormalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:text="@string/btn_accept"
app:layout_constraintBaseline_toBaselineOf="@+id/position_btn_neg"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/position_btn_neg" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
@@ -1,84 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/SurfaceCard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/sources_title"
style="@style/DialogTitle"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="22dp"
android:layout_marginEnd="8dp"
android:text="@string/sources_title"
android:textSize="18sp"
app:layout_constraintBaseline_toBaselineOf="@+id/sources_btn_add"
app:layout_constraintEnd_toStartOf="@+id/sources_btn_add"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="@+id/sources_btn_neg" />
<Button
android:id="@+id/sources_btn_add"
style="@style/NormalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/btn_add"
app:layout_constraintEnd_toEndOf="@+id/sources_btn_pos"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/sources_title"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/sources_recycler"
style="@style/Recycler"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="2dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="2dp"
android:layout_marginBottom="2dp"
app:layout_constraintBottom_toTopOf="@+id/sources_btn_pos"
app:layout_constraintDimensionRatio="w,1:1"
app:layout_constraintEnd_toEndOf="@+id/sources_btn_pos"
app:layout_constraintStart_toStartOf="@+id/sources_btn_neg"
app:layout_constraintTop_toBottomOf="@+id/sources_btn_add" />
<Button
android:id="@+id/sources_btn_neg"
style="@style/NormalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginBottom="6dp"
android:text="@string/btn_cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/sources_btn_pos"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/sources_btn_pos"
style="@style/NormalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:text="@string/btn_accept"
app:layout_constraintBaseline_toBaselineOf="@+id/sources_btn_neg"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/sources_btn_neg" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
@@ -1,126 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:transitionGroup="true">
<androidx.cardview.widget.CardView
android:id="@+id/entries_toolbar"
style="@style/Toolbar">
<ImageButton
android:id="@+id/entries_btn_back"
style="@style/ToolbarButton"
android:layout_gravity="start|center_vertical"
android:contentDescription="@string/btn_back"
android:src="@drawable/ic_back" />
<EditText
android:id="@+id/entries_search"
style="@style/ToolbarTitle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="52dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="52dp"
android:layout_marginBottom="4dp"
android:hint="@string/entries_search_hint"
android:importantForAutofill="no"
android:inputType="text" />
<ImageButton
android:id="@+id/entries_btn_modes"
style="@style/ToolbarButton"
android:layout_gravity="end|center_vertical"
android:contentDescription="@string/btn_modes"
android:src="@drawable/ic_modes" />
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/entries_type_card"
style="@style/SurfaceCard"
android:layout_width="match_parent"
android:layout_height="42dp"
android:layout_marginStart="@dimen/view_default_margin"
android:layout_marginTop="60dp"
android:layout_marginEnd="@dimen/view_default_margin">
<TextView
android:id="@+id/entries_type_message"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start|center_vertical"
android:gravity="center"
android:includeFontPadding="false"
android:paddingStart="14dp"
android:paddingEnd="14dp"
android:text="@string/types_title"
android:textColor="@color/accent"
android:textSize="@dimen/text_size_mediumLarge" />
<ImageView
android:id="@+id/entries_type_img"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end|center_vertical"
android:contentDescription="@string/types_title"
android:paddingStart="14dp"
android:paddingEnd="14dp"
android:src="@drawable/ic_next"
app:tint="@color/accent" />
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
style="@style/SurfaceCard"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/view_default_margin"
android:layout_marginTop="108dp"
android:layout_marginEnd="@dimen/view_default_margin"
android:layout_marginBottom="@dimen/surface_margin_bot">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/entries_recycler"
style="@style/Recycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
android:id="@+id/entries_progress"
style="@style/RecyclerProgress" />
</androidx.cardview.widget.CardView>
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/entries_bottom_bar"
style="@style/BottomBar">
<Button
android:id="@+id/entries_btn_select"
style="@style/NormalButton"
android:layout_width="@dimen/button_width_max"
android:layout_gravity="start|center_vertical"
android:layout_marginStart="@dimen/button_margin_side"
android:text="@string/btn_select" />
<Button
android:id="@+id/entries_btn_clear"
style="@style/NormalButton"
android:layout_width="@dimen/button_width_max"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="@dimen/button_margin_side"
android:text="@string/btn_clear_all" />
</com.google.android.material.bottomappbar.BottomAppBar>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/entries_fab"
style="@style/FloatingActionButton"
android:contentDescription="@string/btn_done"
android:src="@drawable/ic_done"
app:layout_anchor="@id/entries_bottom_bar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
-242
View File
@@ -1,242 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:transitionGroup="true"
android:keepScreenOn="true">
<androidx.cardview.widget.CardView
android:id="@+id/map_toolbar"
style="@style/Toolbar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/map_btn_back"
style="@style/ToolbarButton"
android:layout_gravity="start|center_vertical"
android:contentDescription="@string/btn_back"
android:src="@drawable/ic_back" />
<TextView
android:id="@+id/map_timer"
style="@style/ToolbarTimer" />
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/map_card"
style="@style/SurfaceCard"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="@dimen/view_default_margin"
android:layout_marginTop="@dimen/view_default_margin"
android:layout_marginEnd="@dimen/view_default_margin"
android:layout_marginBottom="@dimen/view_default_margin"
app:layout_constraintBottom_toTopOf="@+id/map_card_info"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/map_toolbar">
<org.osmdroid.views.MapView
android:id="@+id/map_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/map_copyright"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="2dp"
android:ellipsize="end"
android:gravity="center"
android:includeFontPadding="false"
android:maxLines="1"
android:text="@string/map_copyright"
android:textColor="@color/textMain"
android:textSize="@dimen/text_size_copyright"
app:layout_constraintEnd_toEndOf="@+id/map_card"
app:layout_constraintStart_toStartOf="@+id/map_card"
app:layout_constraintTop_toTopOf="@+id/map_card" />
<androidx.cardview.widget.CardView
android:id="@+id/map_card_info"
style="@style/SurfaceCard"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/view_default_margin"
android:layout_marginEnd="@dimen/view_default_margin"
android:layout_marginBottom="@dimen/view_default_margin"
app:layout_constraintBottom_toTopOf="@+id/map_coordinator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/map_azimuth"
style="@style/WorldMapText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/map_azimuth"
app:layout_constraintBottom_toTopOf="@+id/map_data_alt"
app:layout_constraintStart_toStartOf="@+id/map_data_period"
app:layout_constraintTop_toBottomOf="@+id/map_data_period" />
<TextView
android:id="@+id/map_data_lat"
style="@style/WorldMapText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/map_latitude"
app:layout_constraintBottom_toTopOf="@+id/map_data_qth"
app:layout_constraintStart_toStartOf="@+id/map_data_alt"
app:layout_constraintTop_toBottomOf="@+id/map_data_alt" />
<TextView
android:id="@+id/map_data_lon"
style="@style/WorldMapText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/map_longitude"
app:layout_constraintBaseline_toBaselineOf="@+id/map_data_lat"
app:layout_constraintEnd_toEndOf="@+id/map_data_dst" />
<TextView
android:id="@+id/map_data_alt"
style="@style/WorldMapText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/map_altitude"
app:layout_constraintBottom_toTopOf="@+id/map_data_lat"
app:layout_constraintStart_toStartOf="@+id/map_azimuth"
app:layout_constraintTop_toBottomOf="@+id/map_azimuth" />
<TextView
android:id="@+id/map_data_dst"
style="@style/WorldMapText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/map_distance"
app:layout_constraintBaseline_toBaselineOf="@+id/map_data_alt"
app:layout_constraintEnd_toEndOf="@+id/map_elevation" />
<TextView
android:id="@+id/map_elevation"
style="@style/WorldMapText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/map_elevation"
app:layout_constraintBaseline_toBaselineOf="@+id/map_azimuth"
app:layout_constraintEnd_toEndOf="@+id/map_data_phase" />
<TextView
android:id="@+id/map_data_period"
style="@style/WorldMapText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="6dp"
android:text="@string/map_period"
app:layout_constraintBottom_toTopOf="@+id/map_azimuth"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/map_data_phase"
style="@style/WorldMapText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="@string/map_phase"
app:layout_constraintBaseline_toBaselineOf="@+id/map_data_period"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/map_data_qth"
style="@style/WorldMapText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="8dp"
android:text="@string/map_qth"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/map_data_lat"
app:layout_constraintTop_toBottomOf="@+id/map_data_lat" />
<TextView
android:id="@+id/map_data_vel"
style="@style/WorldMapText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/map_velocity"
app:layout_constraintBaseline_toBaselineOf="@+id/map_data_qth"
app:layout_constraintEnd_toEndOf="@+id/map_data_lon" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/map_coordinator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/map_bottom_bar"
style="@style/BottomBar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<Button
android:id="@+id/map_btn_prev"
style="@style/NormalButton"
android:layout_width="@dimen/button_width_max"
android:layout_marginStart="@dimen/button_margin_side"
android:text="@string/btn_prev"
app:icon="@drawable/ic_prev"
app:layout_constraintBottom_toBottomOf="@+id/map_coordinator"
app:layout_constraintStart_toStartOf="@+id/map_coordinator"
app:layout_constraintTop_toTopOf="@+id/map_coordinator" />
<TextView
android:id="@+id/map_data_visibility"
style="@style/WorldMapText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:ellipsize="end"
android:gravity="center_horizontal"
android:maxLines="1"
android:text="@string/map_visible"
app:layout_constraintBaseline_toBaselineOf="@+id/map_btn_prev"
app:layout_constraintEnd_toStartOf="@+id/map_btn_next"
app:layout_constraintStart_toEndOf="@+id/map_btn_prev" />
<Button
android:id="@+id/map_btn_next"
style="@style/NormalButton"
android:layout_width="@dimen/button_width_max"
android:layout_marginEnd="@dimen/button_margin_side"
android:text="@string/btn_next"
app:icon="@drawable/ic_next"
app:iconGravity="end"
app:layout_constraintBaseline_toBaselineOf="@+id/map_btn_prev"
app:layout_constraintEnd_toEndOf="@+id/map_coordinator" />
</androidx.constraintlayout.widget.ConstraintLayout>
-116
View File
@@ -1,116 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:transitionGroup="true">
<androidx.cardview.widget.CardView
android:id="@+id/passes_toolbar"
style="@style/Toolbar">
<ImageButton
android:id="@+id/passes_btn_refresh"
style="@style/ToolbarButton"
android:rotation="45"
android:layout_gravity="start|center_vertical"
android:contentDescription="@string/btn_refresh"
android:src="@drawable/ic_refresh" />
<TextView
android:id="@+id/passes_timer"
style="@style/ToolbarTimer" />
<ImageButton
android:id="@+id/passes_btn_map"
style="@style/ToolbarButton"
android:layout_gravity="end|center_vertical"
android:contentDescription="@string/btn_map"
android:src="@drawable/ic_map" />
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
style="@style/SurfaceCard"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/view_default_margin"
android:layout_marginTop="@dimen/surface_margin_top"
android:layout_marginEnd="@dimen/view_default_margin"
android:layout_marginBottom="@dimen/surface_margin_bot">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/passes_recycler"
style="@style/Recycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
android:id="@+id/passes_progress"
style="@style/RecyclerProgress" />
<LinearLayout
android:id="@+id/passes_empty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="48dp"
android:layout_marginEnd="48dp"
android:layout_marginBottom="32dp"
android:orientation="vertical"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/passes_empty_img"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="32dp"
android:contentDescription="@string/passes_error"
android:src="@drawable/ic_list"
app:tint="@color/textDisabled" />
<TextView
android:id="@+id/passes_empty_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:text="@string/passes_error"
android:textSize="@dimen/text_size_mediumLarge" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/passes_bottomBar"
style="@style/BottomBar">
<Button
android:id="@+id/passes_btn_filter"
style="@style/NormalButton"
android:layout_width="@dimen/button_width_max"
android:layout_marginStart="@dimen/button_margin_side"
android:layout_gravity="start|center_vertical"
android:text="@string/btn_filter" />
<Button
android:id="@+id/passes_btn_settings"
style="@style/NormalButton"
android:layout_width="@dimen/button_width_max"
android:layout_marginEnd="@dimen/button_margin_side"
android:layout_gravity="end|center_vertical"
android:text="@string/btn_settings" />
</com.google.android.material.bottomappbar.BottomAppBar>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/passes_fab"
style="@style/FloatingActionButton"
android:src="@drawable/ic_satellite"
android:contentDescription="@string/btn_satellites"
app:layout_anchor="@id/passes_bottomBar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
-235
View File
@@ -1,235 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:transitionGroup="true"
android:keepScreenOn="true">
<androidx.cardview.widget.CardView
android:id="@+id/radar_toolbar"
style="@style/Toolbar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/radar_btn_back"
style="@style/ToolbarButton"
android:layout_gravity="start|center_vertical"
android:contentDescription="@string/btn_back"
android:src="@drawable/ic_back" />
<TextView
android:id="@+id/radar_timer"
style="@style/ToolbarTimer" />
<ImageButton
android:id="@+id/radar_btn_map"
style="@style/ToolbarButton"
android:layout_gravity="end|center_vertical"
android:contentDescription="@string/btn_map"
android:src="@drawable/ic_map" />
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/radar_card"
style="@style/SurfaceCard"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="@dimen/view_default_margin"
android:layout_marginTop="@dimen/view_default_margin"
android:layout_marginEnd="@dimen/view_default_margin"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/radar_toolbar" />
<TextView
android:id="@+id/radar_az_value"
style="@style/RadarValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:text="@string/radar_az_value"
app:layout_constraintStart_toStartOf="@+id/radar_card"
app:layout_constraintTop_toTopOf="@+id/radar_card" />
<TextView
android:id="@+id/radar_az"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/radar_az_text"
android:textSize="@dimen/text_size_small"
app:layout_constraintStart_toStartOf="@+id/radar_az_value"
app:layout_constraintTop_toBottomOf="@+id/radar_az_value" />
<TextView
android:id="@+id/radar_el_value"
style="@style/RadarValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/radar_el_value"
app:layout_constraintBaseline_toBaselineOf="@+id/radar_az_value"
app:layout_constraintEnd_toEndOf="@+id/radar_card" />
<TextView
android:id="@+id/radar_el"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/radar_el_text"
android:textSize="@dimen/text_size_small"
app:layout_constraintBaseline_toBaselineOf="@+id/radar_az"
app:layout_constraintEnd_toEndOf="@+id/radar_el_value" />
<TextView
android:id="@+id/radar_alt_value"
style="@style/RadarValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="@string/radar_alt_value"
app:layout_constraintBottom_toBottomOf="@+id/radar_card"
app:layout_constraintStart_toStartOf="@+id/radar_az_value" />
<TextView
android:id="@+id/radar_alt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/radar_alt_text"
android:textSize="@dimen/text_size_small"
app:layout_constraintBottom_toTopOf="@+id/radar_alt_value"
app:layout_constraintStart_toStartOf="@+id/radar_alt_value" />
<TextView
android:id="@+id/radar_dst_value"
style="@style/RadarValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/radar_dist_value"
app:layout_constraintBaseline_toBaselineOf="@+id/radar_alt_value"
app:layout_constraintEnd_toEndOf="@+id/radar_el_value" />
<TextView
android:id="@+id/radar_dst"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/radar_dist_text"
android:textSize="@dimen/text_size_small"
app:layout_constraintBaseline_toBaselineOf="@+id/radar_alt"
app:layout_constraintEnd_toEndOf="@+id/radar_dst_value" />
<androidx.cardview.widget.CardView
android:id="@+id/radar_list_card"
style="@style/SurfaceCard"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="@dimen/view_default_margin"
android:layout_marginTop="@dimen/view_default_margin"
android:layout_marginEnd="@dimen/view_default_margin"
android:layout_marginBottom="@dimen/view_default_margin"
app:layout_constraintBottom_toTopOf="@+id/radar_coordinator"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/radar_card">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/radar_recycler"
style="@style/Recycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
android:id="@+id/radar_progress"
style="@style/RecyclerProgress" />
<LinearLayout
android:id="@+id/radar_empty_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:visibility="invisible"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/radar_empty_img"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
android:layout_marginBottom="24dp"
android:contentDescription="@string/radio_no_data"
android:src="@drawable/ic_modes" />
<TextView
android:id="@+id/radar_empty_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:includeFontPadding="false"
android:text="@string/radio_no_data"
android:textSize="@dimen/text_size_mediumLarge" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/radar_coordinator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/radar_bottom_bar"
style="@style/BottomBar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<Button
android:id="@+id/radar_btn_notify"
style="@style/NormalButton"
android:layout_width="0dp"
android:layout_marginStart="@dimen/button_margin_side"
android:text="@string/btn_notify"
app:layout_constraintBottom_toBottomOf="@+id/radar_coordinator"
app:layout_constraintEnd_toStartOf="@+id/radar_visibility"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/radar_coordinator"
app:layout_constraintStart_toStartOf="@+id/radar_coordinator"
app:layout_constraintTop_toTopOf="@+id/radar_coordinator" />
<TextView
android:id="@+id/radar_visibility"
style="@style/WorldMapText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:ellipsize="end"
android:gravity="center_horizontal"
android:maxLines="1"
android:text="@string/radar_visible"
app:layout_constraintBaseline_toBaselineOf="@+id/radar_btn_notify"
app:layout_constraintEnd_toStartOf="@+id/radar_btn_settings"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/radar_btn_notify" />
<Button
android:id="@+id/radar_btn_settings"
style="@style/NormalButton"
android:layout_width="0dp"
android:layout_marginEnd="@dimen/button_margin_side"
android:text="@string/btn_settings"
app:layout_constraintBaseline_toBaselineOf="@+id/radar_btn_notify"
app:layout_constraintEnd_toEndOf="@+id/radar_coordinator"
app:layout_constraintStart_toEndOf="@+id/radar_visibility" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,151 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:transitionGroup="true">
<androidx.cardview.widget.CardView
android:id="@+id/settings_toolbar"
style="@style/Toolbar">
<ImageButton
android:id="@+id/settings_btn_back"
style="@style/ToolbarButton"
android:layout_gravity="start|center_vertical"
android:contentDescription="@string/btn_back"
android:src="@drawable/ic_back" />
<TextView
android:id="@+id/settings_title"
style="@style/ToolbarTitle"
android:text="@string/btn_settings" />
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
style="@style/Toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/view_default_margin"
android:layout_marginTop="@dimen/surface_margin_top"
android:layout_marginEnd="@dimen/view_default_margin"
android:layout_marginBottom="@dimen/surface_margin_bot"
android:backgroundTint="@color/background">
<androidx.core.widget.NestedScrollView
android:id="@+id/settings_scroll"
style="@style/Recycler"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
android:id="@+id/settings_about"
layout="@layout/card_about"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include
android:id="@+id/settings_location"
layout="@layout/card_location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/view_default_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/settings_about" />
<include
android:id="@+id/settings_data"
layout="@layout/card_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/view_default_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/settings_location" />
<include
android:id="@+id/settings_remote"
layout="@layout/card_remote"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/view_default_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/settings_data" />
<include
android:id="@+id/settings_btremote"
layout="@layout/card_btremote"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/view_default_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/settings_remote" />
<include
android:id="@+id/settings_other"
layout="@layout/card_other"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/view_default_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/settings_btremote" />
<include
android:id="@+id/settings_outro"
layout="@layout/card_outro"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/view_default_margin"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/settings_other" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.cardview.widget.CardView>
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/settings_bottom_bar"
style="@style/BottomBar">
<Button
android:id="@+id/settings_btn_github"
style="@style/NormalButton"
android:layout_width="@dimen/button_width_max"
android:layout_marginStart="@dimen/button_margin_side"
android:layout_gravity="start|center_vertical"
android:text="@string/btn_github" />
<Button
android:id="@+id/settings_btn_fdroid"
style="@style/NormalButton"
android:layout_width="@dimen/button_width_max"
android:layout_marginEnd="@dimen/button_margin_side"
android:layout_gravity="end|center_vertical"
android:text="@string/btn_fdroid" />
</com.google.android.material.bottomappbar.BottomAppBar>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/settings_fab"
style="@style/FloatingActionButton"
android:src="@drawable/ic_support"
android:contentDescription="@string/btn_support"
app:layout_anchor="@id/settings_bottom_bar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/RecyclerItemCard">
<CheckBox
android:id="@+id/entry_checkbox"
android:layout_width="match_parent"
android:layout_height="41dp"
android:layout_marginStart="6dp"
android:background="@color/cardRegular"
android:ellipsize="end"
android:lines="1"
android:padding="4dp"
android:textColor="@color/textMain"
android:textSize="@dimen/text_size_medium" />
</androidx.cardview.widget.CardView>
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/RecyclerItemCard">
<CheckBox
android:id="@+id/mode_checkbox"
android:layout_width="match_parent"
android:layout_height="41dp"
android:layout_marginStart="6dp"
android:background="@color/cardRegular"
android:ellipsize="end"
android:lines="1"
android:padding="4dp"
android:textColor="@color/textMain"
android:textSize="@dimen/text_size_medium" />
</androidx.cardview.widget.CardView>
-115
View File
@@ -1,115 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/RecyclerItemCard">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/pass_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/pass_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:text="@string/pass_satName"
android:textColor="@color/accent"
android:textSize="@dimen/text_size_small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/pass_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/pass_satId"
android:textColor="@color/accent"
android:textSize="@dimen/text_size_small"
app:layout_constraintBaseline_toBaselineOf="@+id/pass_name"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/pass_aos"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="@string/pass_aosAz"
android:textSize="@dimen/text_size_small"
app:layout_constraintEnd_toStartOf="@+id/pass_los"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="@+id/pass_name"
app:layout_constraintTop_toBottomOf="@+id/pass_name" />
<TextView
android:id="@+id/pass_altitude"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pass_altitude"
android:textSize="@dimen/text_size_small"
app:layout_constraintBaseline_toBaselineOf="@+id/pass_aos"
app:layout_constraintEnd_toEndOf="@+id/pass_los"
app:layout_constraintStart_toStartOf="@+id/pass_aos" />
<TextView
android:id="@+id/pass_los"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pass_los"
android:textSize="@dimen/text_size_small"
app:layout_constraintBaseline_toBaselineOf="@+id/pass_aos"
app:layout_constraintEnd_toEndOf="@+id/pass_id"
app:layout_constraintStart_toEndOf="@+id/pass_aos" />
<TextView
android:id="@+id/pass_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="@string/pass_startTime"
android:textSize="@dimen/text_size_small"
app:layout_constraintEnd_toStartOf="@+id/pass_end"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="@+id/pass_aos"
app:layout_constraintTop_toBottomOf="@+id/pass_aos" />
<TextView
android:id="@+id/pass_elev"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pass_elevation"
android:textSize="@dimen/text_size_small"
app:layout_constraintBaseline_toBaselineOf="@+id/pass_start"
app:layout_constraintEnd_toEndOf="@+id/pass_end"
app:layout_constraintStart_toStartOf="@+id/pass_start" />
<TextView
android:id="@+id/pass_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/pass_endTime"
android:textSize="@dimen/text_size_small"
app:layout_constraintBaseline_toBaselineOf="@+id/pass_start"
app:layout_constraintEnd_toEndOf="@+id/pass_los"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/pass_start" />
<ProgressBar
android:id="@+id/pass_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="1dp"
android:layout_marginBottom="2dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/pass_end"
app:layout_constraintStart_toStartOf="@+id/pass_start"
app:layout_constraintTop_toBottomOf="@+id/pass_elev" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
-103
View File
@@ -1,103 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/RecyclerItemCard">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/radio_guide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<TextView
android:id="@+id/radio_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="2dp"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:text="@string/radio_info"
android:textSize="@dimen/text_size_small"
app:layout_constraintEnd_toStartOf="@+id/radio_up_img"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toEndOf="@+id/radio_down_img"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/radio_down_img"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="6dp"
android:contentDescription="@string/radio_downlink"
android:rotation="90"
app:layout_constraintBottom_toBottomOf="@+id/radio_info"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/radio_info"
app:srcCompat="@drawable/ic_next" />
<TextView
android:id="@+id/radio_downlink"
style="@style/RadioFreq"
android:layout_marginStart="4dp"
android:layout_marginTop="3dp"
app:layout_constraintEnd_toStartOf="@+id/radio_guide"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/radio_info" />
<ImageView
android:id="@+id/radio_up_img"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginEnd="6dp"
android:contentDescription="@string/radio_uplink"
android:rotation="270"
app:layout_constraintBottom_toBottomOf="@+id/radio_info"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/radio_info"
app:srcCompat="@drawable/ic_next" />
<TextView
android:id="@+id/radio_uplink"
style="@style/RadioFreq"
android:layout_marginEnd="4dp"
app:layout_constraintBaseline_toBaselineOf="@+id/radio_downlink"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/radio_guide" />
<TextView
android:id="@+id/radio_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="4dp"
android:text="@string/radio_mode"
android:textSize="@dimen/text_size_small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/radio_inverted"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/radio_downlink" />
<TextView
android:id="@+id/radio_inverted"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/radio_inverted"
android:textSize="@dimen/text_size_small"
app:layout_constraintBaseline_toBaselineOf="@+id/radio_mode"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/radio_mode" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
-21
View File
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.textfield.TextInputLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/source_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
android:hint="@string/sources_hint"
app:endIconDrawable="@drawable/ic_delete"
app:endIconMode="custom">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/source_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_bg" />
<foreground android:drawable="@drawable/ic_launcher_fg" />
</adaptive-icon>
Loaded 100 of 5930 files, more files were not shown because too many files have changed in this diff. Show more