Compare commits

..
Author SHA1 Message Date
mckero c19209655b fix(roaming,settings): theme colors for night mode; unclamp cards; brand title and APK name
Three detail fixes plus branding, per user review:

1. Night mode visibility (roaming page): hardcoded reference blues
   (#01DDFF/#0BACF1) collapse to black under the red ColorMatrix
   filter (R channel only), making the page unreadable. Replace ALL
   colors with MaterialTheme.colorScheme (background/surface/
   surfaceVariant/onSurface/onSurfaceVariant/primary/error). GPS
   status dots become themed circles (primary/error) so they survive
   the red filter; red marker keeps the original pnt drawable.

2. Settings cards unclamped:
   - OtherCard: fixed height(268.dp) squeezed the last toggle row
     (spacing 42/42/42/15px on device); drop the fixed height.
   - CardCredits: same 268dp + SpaceBetween overflowed, gluing the
     last thanks entry to the warranty line; drop fixed height, use
     spacedBy(8.dp) and insert 24dp before the warranty text.

3. Branding:
   - Settings top title: "Look4Sat v%s" -> "Look4Sat Pro v%s" (en/tr);
     title Text no longer marquees and wraps instead (heightIn(min=48)).
   - APK asset name: look4sat-<ver>.apk -> Look4Sat-Pro-<ver>.apk.
   - What's-new dialog (pass_whatsnew_message) rewritten in all three
     locales (en/zh/tr) with this release's changes; zh gets its own
     localized title "Look4Sat Pro 更新内容".

Verified: roaming/settings/passes/app compile, 11 unit tests pass.
2026-08-03 15:46:21 +00:00
mckero d456bdf4b5 fix(roaming): stretch side cells to 80dp like the reference RelativeLayout
The user spotted two white "beams" (20dp gaps) between the center
cell and the side cells. Root cause: the reference app's RelativeLayout
ignores the fixed 60dp width when a child has BOTH a left rule
(alignParentLeft) and a right rule (toLeftOf=center cell) - the width
is stretched to right-rule minus left-rule, i.e. 80dp on a 360dp
screen. The three cells therefore sit flush with only 2dp margins
between them, no gaps.

Port that behavior: side cells 60dp -> 80dp (6 places), center cell
stays 200dp centered. Verified against the reference screenshot pixel
measurements (side cells ~76.5dp incl. margins, center ~198dp).

Verified: feature + app compile, 11 unit tests pass. Marker lookup,
grid math and GPS logic untouched.
2026-08-03 15:22:08 +00:00
mckero f06b17a194 fix(roaming): align grid columns like reference and lift footer
Two remaining proportion issues on the user's device:

1. Right-side gap: the three grid rows used a continuous Row
   (60+200+60dp), leaving ~84px of blank space on the right of the
   screen. The reference app uses a RelativeLayout where the right
   column is pinned to the screen edge and the 20dp gaps sit on both
   sides of the centered middle column. Convert each row to a Box:
   left cell align(CenterStart), middle cell align(Center), right
   cell align(CenterEnd) - pixel-identical to the reference.

2. Footer legibility: the credit line ("制作:US1PM  汉化:BA7LCE")
   had a fixed 15dp height with no bottom margin, so it sat directly
   against the navigation bar and part of the text was hard to read.
   Drop the fixed height and add 10dp bottom padding so the text
   renders fully with breathing room above the nav bar.

Verified: feature + app compile, 11 unit tests pass. Marker lookup,
grid math and GPS logic untouched.
2026-08-03 15:09:45 +00:00
mckero 675194075e fix(roaming): inset page content from system bars
The ported page rendered edge-to-edge: the GPS bar started right at
the top of the screen and the footer sat against the navigation bar,
so system UI overlapped the content (user: "顶头"). The reference app
is not edge-to-edge and keeps its content inside the safe area.

Add windowInsetsPadding(WindowInsets.systemBars) on the page root
column, shrinking the top and bottom by the status/navigation bar
height exactly as the user requested ("上方和下方往里面缩一点点").
Grid columns (60/200/60dp), rows, marker lookup and all logic are
untouched - verified pixel-identical column ratios vs the reference
screenshot (147:395:147 vs 146:395:148).

Verified: feature + app compile, 11 unit tests pass.
2026-08-03 14:55:46 +00:00
mckero 0ecccc4746 feat(roaming): port QTH定位器 2.0 page and logic verbatim
The roaming page was repeatedly rebuilt by hand and the red marker
still rendered at the wrong spot on the user's device. Per the user's
explicit instruction the whole page is now a faithful, line-by-line
port of the reference app (QTH定位器 2.0, com.us1pm.gridsquarelocator)
with zero UI or logic changes.

UI (res/layout/main.xml, byte-verified via aapt2 dump):
- 25dp holo-blue GPS bar: "GPS" 14sp, green/red status dot 15dp
  (original mipmaps copied as drawables), centered date, right time,
  translucent-yellow "设置启用GPS" button that opens location settings
- Latitude/longitude rows: 16sp black labels, right-aligned decimal
  values, DMS label format "纬度  22° 18' 50" N" exactly as reference
- 43sp bold black locator, centered, with progress spinner + notice
- 3x3 continuous grid: 60/200/60dp columns, middle row fixed 205dp,
  edge cells #0BACF1, center cell 200x200dp holo-blue (#01DDFF as
  shown on the user's device), center label 100x80dp 30sp bold white
  (textColorHighlight) centered, bottom "制作:US1PM  汉化:BA7LCE"
- Red marker: original pnt.png (red square with white outline),
  10x10dp, absolutely positioned by the reference lookup tables
  (lon 3rd pair a..x -> leftMargin -2..190dp, lat 3rd pair a..x ->
  topMargin 190..-2dp, screen-Y inverted)

Logic (MainActivity.java showLocation/checkEnabled/onResume):
- 8-char locator via the reference range-lookup tables
- 3x3 neighbor grid via parseInt3 five-branch logic incl. all four
  corner-carry tables (00/09/99/90)
- Live GPS + network updates 10s/10m while the page is shown,
  provider filtered to gps/network, checkEnabled three-state
  (green dot / red dot + settings button) exactly like the reference
- Time = cached hour prefix + fix minutes; date "dd MMM yyyy"

Dropped only the Play-store ad banner (conflicts with GPL project).

Verified: 11 unit tests pass (locator, marker lookup, grid, DMS,
time, all edge branches); feature + app modules compile.

RoamingState.kt removed - state and math now live in RoamingScreen.kt.
2026-08-03 14:29:44 +00:00
mckero 88d1498eac fix(roaming): red marker tracks fix in BOTH axes via 3rd-pair lookup
The marker's vertical position was hardcoded (10dp below the label) — markerY never participated, so the dot could only move horizontally and could not reflect where the GPS fix sits inside the 4-char square.

Now the marker is positioned in both axes from the cell's top-left corner:
- x = markerX * cellWidth, y = markerY * cellHeight (screen Y)
- markerX/markerY come from the 3rd character pair via the reference lookup tables (lon a=-2..x=190 as leftMargin; lat a=190..x=-2 as topMargin, i.e. latitude inverted on screen)
- Verified numerically against the decompiled tables: a=0/1.0, i=0.333/0.667, x=0.958/0.042 — matching within 3% (the reference table is slightly non-uniform)

Label stays at upper-middle (28sp bold); marker is only drawn when a valid locator exists.
2026-08-03 12:55:37 +00:00
mckero 31ae04329d fix(roaming): clock ticker moves the header time; GPS mirrors station pos
Two bugs from the last release:

1. The header clock was frozen: Date() was only evaluated during recomposition, and with no state changes the time never moved. Added a 1s LaunchedEffect ticker that updates a now-state, so the date/time text re-renders and actually advances — matching the reference app, which refreshes the clock on every location callback.

2. GPS indicator: replaced the 10-minute freshness check with a direct 'has a real fix' check (timestamp > 0 and coords non-zero). The page mirrors the station position (站位) from the shared StateFlow, so the GPS dot is green whenever the station has a fix — GPS shows exactly what the station GPS says, nothing more.
2026-08-03 12:48:17 +00:00
mckero eae6304124 feat(roaming): faithful continuous-table grid layout, not card-based
Reworks the Roaming grid to structurally match the reference app instead of a card-style panel:

- Removed the outer ElevatedCard, rounded cells, cell gaps and inner padding: the 3x3 grid is now one continuous table that fills the panel, cells connected edge-to-edge.
- Cells separated by 2dp divider lines that run the full width/height of each row/column, crossing at right angles like a real coordinate grid (the reference app's continuous separator lines).
- Cells are square-cornered (no rounded corners), background fills each cell fully.
- Column widths 21.4% : 56.2% : 21.4%, row heights 31.5% : 35.9% : 31.7% retained.
- Center cell: OL42 is larger (28sp bold) and placed at upper-middle; the red marker sits BELOW the text with a 10dp gap, horizontally offset by the 3rd-pair fraction — never overlapping the label, matching the reference 'text above, marker below' layout.
- Surrounding labels bumped to 16sp Medium (larger/stronger than before).
- Info header also switched from an ElevatedCard to a flat continuous block so the page reads as one continuous surface, like the reference.

The grid proportions, locator algorithm, marker mapping and boundary logic were already faithful; this change makes the visual structure faithful too.
2026-08-03 12:32:45 +00:00
mckero 97c6d609ac fix(roaming): drop auto GPS polling, show station position directly
Reverts the auto-update machinery after review — the page now simply mirrors the station position (站位) from the shared settingsRepo.stationPosition StateFlow, exactly what the Settings page shows:

- RoamingScreen: removed the LocationManager listeners, the 30s re-request loop, the provider filter and the location-disabled hint. No polling, no auto-updates; coordinates are whatever the station GPS says.
- Settings: removed the '漫游位置实时更新' toggle (stateOfRoamingLive, key, action, strings en/zh/tr) that caused the 'Other' card to overflow — the sixth unlabeled switch clipped past the card's rounded bottom edge was that row overflowing a fixed-height card. Card height back to 268.dp, five rows fit again.

Verified: core:domain tests, roaming/settings/app compile clean; zero references to RoamingLive remain.
2026-08-03 12:24:07 +00:00
mckero 44c442ddd3 feat(roaming): port live GPS tracking and location-disabled hint
Completes the port of the QTH定位器 app's location logic (read from the decompiled MainActivity):

1. Live location updates: a LocationListener registers GPS+NETWORK providers (10s / 10m, matching the reference onResume) while the Roaming page is visible and removes itself on dispose. Every fix is pushed through settingsRepo.setStationPosition, so the shared stationPosition StateFlow updates the Settings page and the map in lockstep — the page now refreshes in real time instead of only showing stale cached coordinates.

2. Provider filtering: only gps/network fixes are accepted, mirroring the reference showLocation() guard that rejects passive fixes.

3. Location-disabled hint: when GPS is off or permission is missing, the header shows a tappable '定位未开启,点击前往系统设置' row that opens ACTION_LOCATION_SOURCE_SETTINGS — the port of the reference btnLocationSettings button. The GPS status dot now has three states: fresh fix (primary), provider on but stale (error), provider off (outline).

4. Periodic recovery: a 30s re-request loop (honoring the 漫游位置实时更新 toggle) re-arms the location request after the chip falls idle.

Not ported (conflict, deliberate): the reference app's Play-store ad banner, 'New! Grid Square with map' promo and GP_IN preferences — commercial advertising does not belong in a GPL satellite tracker.
2026-08-03 11:59:31 +00:00
mckero f9250ec1f1 fix(roaming): live coords from shared flow, marker by locator pair, versioned apk
Addresses three review findings:

1. Coordinates now come straight from settingsRepo.stationPosition in the screen (collectAsStateWithLifecycle) — the exact same StateFlow the Settings page shows. Previously a separate ViewModel re-derived them, and it could lag behind the Settings page (user: '设置页更新了站位但漫游页死活不更新'). With the shared source the two pages can never disagree. RoamingViewModel removed; state derivation moved to RoamingState.fromPosition().

2. Red marker placement ported faithfully from the QTH定位器 app: it is driven by the 3rd character pair of the 8-char locator (the 'ih' in OL42ih45), mapped to a 0..1 fraction (lon a=west..x=east, lat inverted a=south..x=north), then scaled to the actual center-cell size. The grid now uses the reference proportions (columns 21.4/56.2/21.4, rows 31.5/35.9/31.7) and fills the screen, so the marker lands accurately on any device.

3. Workflow now uploads a versioned APK (look4sat-<version>.apk instead of look4sat.apk).

Also: Settings 'Other' card rows got vertical spacing (Arrangement.spacedBy) so the new roaming toggle is not glued to the night-mode row.
2026-08-03 11:49:46 +00:00
mckero bff3e1af09 chore: bump version to 4.4.7
Next release: versionCode 446 -> 447, versionName 4.4.6 -> 4.4.7. Includes the reworked Roaming page (faithful QTH定位器 port, live-update toggle, night-mode-safe theme colors).
2026-08-03 11:27:25 +00:00
mckero 6dbe30b3f6 feat(roaming): faithful QTH定位器 port, live-update toggle, night-mode safe
Rework the Roaming page after user review. It is now a faithful port of the QTH定位器 location panel, not a loose re-skin:

UI (matching the reference layout):
- GPS status dot (real: green when a fresh fix exists, outline when stale/missing) + date + time header
- Lat/Lon rows with DMS and 5-decimal display, big 8-char locator centered below
- 3x3 grid of neighboring 4-char squares with the reference proportions: center column ~2.6x wider (21.4% : 56.2% : 21.4%) and center row the tallest (31.5% : 35.9% : 31.7%); red position marker now placed at the fractional position of the fix inside the center cell (was fixed center)
- No oversized GPS button: live updating is now a Settings toggle '漫游位置实时更新' (stateOfRoamingLive, default on) that drives periodic location refresh

Style & night-mode safety:
- All colors come from MaterialTheme.colorScheme (surfaceVariant/secondaryContainer/error) — no hardcoded cyan/blue from the original app. The red night filter (ColorMatrix keeping only the R channel) blanked the old hardcoded palette; theme colors survive it.
- Chinese nav label '漫游' added to values-zh (was missing, showing English 'Roaming')

Settings:
- OtherSettings.stateOfRoamingLive persisted (default true), toggle row in Other card, height adjusted

Verified: feature:roaming, feature:settings, app compile clean.
2026-08-03 11:18:35 +00:00
mckero 8a920acead build: revert gradle heap cap, keep CI builds unconstrained
The 768m heap cap broke GitHub Actions (KSP OutOfMemoryError: Metaspace) because CI runners read the same gradle.properties. Restore -Xmx6g for CI; the 2GB local server should pass a one-off -Dorg.gradle.jvmargs instead.
2026-08-03 10:49:59 +00:00
mckero 929f652b8e chore: bump version to 4.4.6
Next release: versionCode 445 -> 446, versionName 4.4.5 -> 4.4.6. Adds the Roaming page (QTH定位器-style 3x3 Maidenhead grid) between Match and Settings, credits BG7NTA & the original author in the thanks title, and caps local Gradle heap for the 2GB build server.
2026-08-03 10:47:39 +00:00
mckero 610768501e build: add roaming module gradle file, cap local JVM heap
Include feature:roaming/build.gradle.kts (missed from the module commit) and keep the reduced Gradle heap (-Xmx768m) so local light builds don't freeze the 2GB server.
2026-08-03 10:46:32 +00:00
mckero 427e6e1862 i18n: credit BG7NTA and original author in thanks title
The settings outro title now reads 'BG7NTA & the original author would like to thank' (en) / 'BG7NTA 与原作者感谢' (zh) / 'BG7NTA ve orijinal yazar teşekkür eder' (tr), alongside the BA7OPF/BG7NTA entries added to the credits list.
2026-08-03 10:46:20 +00:00
mckero bfa5e5f474 feat(roaming): add Roaming page with 3x3 Maidenhead grid
Ports the QTH定位器 (com.us1pm.gridsquarelocator) location panel into Look4Sat as a new 'Roaming' page, restyled with the app's own look.

UI (top to bottom):
- Info header: GPS status dot, big 8-char locator, Lat/Lon rows with DMS + 5-decimal display, and a GPS 定位 button
- 3x3 grid panel: the current 4-char Maidenhead square (e.g. OL42) centered, surrounded by its 8 neighbors (OL33..OL51), with a red position marker in the center cell

Logic:
- QthConverter gains qthNeighbors(square) building the 3x3 grid with field/square carry at boundaries (AA00 wraps to RR99, IO91 crosses into J field), and qthToSquare(locator) extracting the 4-char square
- Verified against the decompiled app algorithm and the reference screenshot (OL42 grid matches exactly); 9 unit tests cover normal, boundary and field-wrap cases

Navigation:
- New bottom-nav item 'Roaming' between Match and Settings, with a crosshair icon
- New feature:roaming module (ViewModel + Compose screen) registered in the app

Build config:
- Lowered Gradle JVM heap from -Xmx6g to 768m: the 2GB build server froze on the old value; heavy release builds stay on GitHub Actions
2026-08-03 10:46:14 +00:00
mckero 213425379c merge: merge atsunatsu/main (mutual radar overlay arrows)
Merge upstream commit 2298d8ee 'feat: add mutual radar overlay arrows'. Clean auto-merge: upstream changed RadarView.kt arrow drawing, our sweep optimization stayed intact. No conflicts.

First v4.4.5 was built from c3444aed; this release rebuilds from the merged tree.
2026-08-03 10:04:09 +00:00
mckero c3444aed24 chore: bump version to 4.4.5
Next release: versionCode 444 -> 445, versionName 4.4.4 -> 4.4.5, following the upstream scheme without suffix. Includes the station panel layout fix, 5-decimal coordinates, and credits additions already merged on main.
2026-08-03 09:43:10 +00:00
mckero 368bd199e5 fix: station panel layout wraps QTH to its own line
With 5-decimal coordinates the Lat/Lon/Qth row overflowed, pushing the QTH value to a wrapped line. Split into two rows: Lat + Lon on the first line, Qth on the second.

Also append BA7OPF (pass matching feature) and BG7NTA to the credits list in en/zh/tr string resources.
2026-08-03 09:33:40 +00:00
mckero 7bb0bbbb46 build: load release signing config from local keystore.properties
Conditionally applies the BG7NTA signingConfig when keystore.properties exists (gitignored). CI signs via apksigner with GitHub Secrets, so this only affects local builds.
2026-08-03 09:25:53 +00:00
mckero deec5581ae feat: rebrand to Look4Sat Pro, bump version to 4.4.4
App display name changed from Look4Sat to Look4Sat Pro (fork identity). Version follows upstream scheme without suffix: 4.4.3 -> 4.4.4, versionCode 443 -> 444.
2026-08-03 09:20:41 +00:00
mckero 1de5632428 feat: increase station coordinate precision to 5 decimals
Station lat/lon was stored rounded to 4 decimals (~11 m). Bump to 5 decimals (~1.1 m), slightly better than GPS hardware accuracy without showing noise.

- SettingsRepo.setStationPosition: round(4) -> round(5)
- QthConverter.qthToPosition: round(4) -> round(6) so 10-char locators fully roundtrip
- Update QthConverterTest expected values to 6-decimal precision
2026-08-03 09:14:13 +00:00
mckero 2dfd76825b fix: fork under own applicationId com.rtbishop.look4sat.bg7nta
The official app signs com.rtbishop.look4sat with its own certificate. A fork sharing that applicationId cannot be installed over the official build (signature mismatch) and users saw overwrite/install failures.

- Add applicationId version catalog entry; namespace stays com.rtbishop.look4sat so source imports are untouched, applicationId becomes com.rtbishop.look4sat.bg7nta.
- Update PROPERTY_SATELLITE_DATA_OPTIMIZED meta-data to the fork id.
- Document the fork-applicationId requirement in the version catalog.
2026-08-03 08:38:21 +00:00
mckero 06fc8bc2fb ci: set GH_TOKEN for release creation step
gh CLI in Actions requires GH_TOKEN to authenticate. Use the built-in github.token.
2026-08-03 08:15:20 +00:00
mckero 75322390b8 ci: allow specifying release tag on manual dispatch
workflow_dispatch defaults TAG_NAME to the branch name (main), which breaks gh release create. Accept an optional tag_name input and fall back to github.ref_name for tag-triggered runs.
2026-08-03 08:12:20 +00:00
mckero d1312f1f09 ci: add manual workflow_dispatch trigger
Allow triggering the release build from the Actions tab in case tag events are not picked up.
2026-08-03 08:06:19 +00:00
mckero 75acbbd633 ci: simplify release workflow for fork builds
Remove Google Play upload (needs SERVICE_ACCOUNT_JSON we don't have) and AAB signing. Use built-in GITHUB_TOKEN instead of RELEASE_TOKEN secret. Pin actions to stable versions (checkout@v4, setup-java@v4, setup-gradle@v4). Builds assembleRelease, signs APK via apksigner with KEY_STORE secrets, creates GitHub release with APK. Triggered by v** tags.
2026-08-03 08:03:16 +00:00
mckero a37fb8f773 feat(domain): support 6/8/10-char Maidenhead grid square conversion
Port the 8-char (4-pair) Maidenhead grid algorithm from the
"QTH定位器 2.0" app (com.us1pm.gridsquarelocator) into QthConverter
so locator precision matches common grid tools instead of being
truncated to 6 chars.

Previously positionToQth() emitted only 6-char locators and
qthToPosition() discarded everything past the 6th character via
take(6), losing the finer 30" x 15" resolution carried by 8-char
grid squares.

What changed:
- positionToQth(lat, lon, precision = 8) now emits 8-char locators
  by default; precision = 6 / 10 available for backwards
  compatibility and maximum resolution (1.25" x 0.625").
- qthToPosition() parses 6/8/10-char locators and returns the center
  of the finest encoded cell (30" x 15" for 8-char, 1.25" x 0.625"
  for 10-char) instead of the 6-char cell center.
- Locator validation regex tightened: 6/8/10 chars accepted,
  4-char strings like "JN58" are now rejected as invalid.
- Boundary clamping added so lat = 90 / lon = 180 no longer overflow
  the A-R / 0-9 / a-x alphabet (previously produced invalid chars).

Also fixed a pre-existing compile error in RadarView.kt: a delegated
property was assigned after declaration ("by" on an already declared
val). Converted the sweep angle to an if/else expression.

Verification:
- QthConverterTest extended to 5 test cases covering 6/8/10-char
  roundtrips, invalid input, boundary coordinates and roundtrip
  stability.
- Cross-checked against a Python reference model of the decompiled
  APK algorithm: 20k random roundtrips at 8 and 10 chars, 0 failures.
- :core:domain:test green; :app:compileDebugKotlin passes.
2026-08-03 07:25:31 +00:00
mckero 9d0d971500 refine: AOS binary-search refinement, getRadios signature cleanup
Refine AOS crossing to ~500ms via binary search instead of linear stepping; simplify getRadios to take satPos; tidy radar view/viewmodel.
2026-08-03 06:47:49 +00:00
102 changed files with 1616 additions and 13258 deletions

No files matched your search

+19 -40
View File
@@ -4,10 +4,15 @@ on:
push:
tags:
- v**
workflow_dispatch:
inputs:
tag_name:
description: 'Release tag name (e.g. v4.4.3)'
required: false
default: ''
env:
TAG_NAME: ${{ github.ref_name }}
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
TAG_NAME: ${{ github.event.inputs.tag_name || github.ref_name }}
jobs:
release:
@@ -16,66 +21,40 @@ jobs:
contents: write
steps:
- name: Checkout Repository
uses: actions/checkout@v7
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
uses: gradle/actions/setup-gradle@v4
- name: Assemble Artifacts
run: ./gradlew assembleRelease bundleRelease
- name: Assemble APK
run: ./gradlew assembleRelease
- name: Sign APK
run: |
echo "${{ secrets.KEY_STORE }}" | base64 -d > keystore.jks
APK=$(find app/build/outputs/apk/release -name "*.apk" | head -1)
VERSION=${TAG_NAME#v}
SIGNED_APK="app/build/outputs/apk/release/Look4Sat-Pro-${VERSION}.apk"
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 \
--out "$SIGNED_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
echo "SIGNED_APK=$SIGNED_APK" >> "$GITHUB_ENV"
- name: Create Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create $TAG_NAME --title=$TAG_NAME --generate-notes
gh release upload $TAG_NAME app/build/outputs/apk/release/look4sat.apk
gh release upload $TAG_NAME "$SIGNED_APK"
+1 -3
View File
@@ -18,7 +18,6 @@ out/
# Gradle files
.gradle/
build/
.cxx/
# Local configuration file (sdk path, etc)
local.properties
@@ -40,8 +39,7 @@ captures/
.idea/
# Keystore files
# Uncomment the following line if you do not want to check your keystore files in.
#*.jks
*.jks
/*.properties
/keystore.properties
/app/keystore.jks
+36 -17
View File
@@ -1,26 +1,45 @@
# Look4Sat-BA7OPF
# Look4Sat: Satellite tracker
[![Look4Sat CI](https://github.com/atsunatsu/Look4Sat/actions/workflows/release.yml/badge.svg)](https://github.com/atsunatsu/Look4Sat/actions/workflows/release.yml)
[![Look4Sat CI](https://github.com/rt-bishop/Look4Sat/actions/workflows/release.yml/badge.svg)](https://github.com/rt-bishop/Look4Sat/actions/workflows/release.yml)
**BA7OPF 定制版** — 基于 [rt-bishop/Look4Sat](https://github.com/rt-bishop/Look4Sat) 的业余无线电卫星追踪器,增加了线性卫星频率计算器等功能。
[<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/)
## 本仓库特色功能
### Radio satellite tracker and pass predictor for Android, inspired by Gpredict
- **线性卫星转发器频率计算器** — 在雷达页的 Calculator 标签页中,支持 TX/RX 双向多普勒频率计算,以及下行频率偏移(offset)输入,方便操作带偏移的线性卫星
- **CW 解码器** — 集成 Morse Expert 解码引擎,支持瀑布图、实时解码文本
- **Passband 模式** — 支持通过位置滑块(Passband)自动计算 TX/RX 频率,避免切换时跳变
- **中文界面优化** — 翻译修正、UI 布局调整
<p float="left">
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" width="192"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" width="192"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3.png" width="192"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4.png" width="192">
</p>
## 上游仓库
### Track satellite passes with ease!
本仓库是 [rt-bishop/Look4Sat](https://github.com/rt-bishop/Look4Sat) 的分支,上游仓库的原始功能包括:
Thanks to [Celestrak](https://celestrak.com/) and [SatNOGS](https://satnogs.org/) you have access to over 9000 active satellites.\
You can search the entire database by NORAD Catalog Number or the satellite's name.
- 基于 Celestrak / SatNOGS 数据的 9000+ 活跃卫星追踪
- SGP4/SDP4 轨道预测,10 天过境预报
- 极坐标雷达图、地面轨迹图
- SSTV 图像解码
- 无广告、无跟踪、完全离线
Orbital positions and passes are calculated relative to your location.\
To get reliable data make sure to set the station position via the app Settings.
## 许可证
The application is built using Kotlin, Coroutines, Jetpack Compose and Navigation.\
It is now and always will be completely ad-free and open-source.
GNU General Public License v3.0。详见 [LICENSE](LICENSE)。
## Main features:
* Predicting satellite positions and passes for up to 10 days
* 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 the map
* Custom TLE satellite data import is available via Three Line Element .txt files
* Offline first: calculations are made offline. Weekly TLE data update is recommended.
## Star History
<a href="https://www.star-history.com/?repos=rt-bishop%2FLook4Sat&type=timeline&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=rt-bishop/Look4Sat&type=timeline&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=rt-bishop/Look4Sat&type=timeline&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=rt-bishop/Look4Sat&type=timeline&legend=top-left" />
</picture>
</a>
+17 -12
View File
@@ -1,24 +1,29 @@
import java.util.Properties
plugins {
alias(libs.plugins.convention.applicationPlugin)
}
// Load signing config from keystore.properties (gitignored, never commit credentials)
val keystoreProperties = Properties().apply {
val propsFile = rootProject.file("keystore.properties")
if (propsFile.exists()) propsFile.inputStream().use { load(it) }
}
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 = "look4sat123"
keyAlias = "look4sat"
keyPassword = "look4sat123"
if (keystoreProperties["storeFile"] != null) {
create("release") {
storeFile = rootProject.file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
}
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
signingConfig = signingConfigs.findByName("release")
}
}
}
}
+1 -1
View File
@@ -43,7 +43,7 @@
<meta-data
android:name="android.telephony.PROPERTY_SATELLITE_DATA_OPTIMIZED"
android:value="com.rtbishop.look4sat" />
android:value="com.rtbishop.look4sat.bg7nta" />
</application>
</manifest>
@@ -85,6 +85,7 @@ 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.roaming.RoamingScreen
import com.rtbishop.look4sat.feature.satellites.SatellitesDestination
import com.rtbishop.look4sat.feature.settings.SettingsDestination
@@ -130,7 +131,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
val currentKey = backStack.lastOrNull()
val navigateBack: () -> Unit = { backStack.removeLastOrNull() }
val fadeTransition = fadeIn(animationSpec = tween(350)) togetherWith fadeOut(animationSpec = tween(350))
val navItems = listOf(Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Mutual, Screen.Map, Screen.Settings)
val navItems = listOf(Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Mutual, Screen.Roaming, Screen.Map, Screen.Settings)
val context = LocalContext.current
val container = (context.applicationContext as IContainerProvider).getMainContainer()
@@ -224,6 +225,9 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
}
)
}
entry<Screen.Roaming> {
RoamingScreen()
}
entry<Screen.Settings> {
SettingsDestination()
}
@@ -35,6 +35,7 @@ internal class ApplicationPlugin : Plugin<Project> {
implementation(project(":feature:mutual"))
implementation(project(":feature:passes"))
implementation(project(":feature:radar"))
implementation(project(":feature:roaming"))
implementation(project(":feature:satellites"))
implementation(project(":feature:settings"))
implementation(libs.androidx.core.splashscreen)
@@ -57,7 +57,7 @@ internal fun Project.setupAndroidApp() {
namespace = libs.versions.packageName.get()
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
applicationId = libs.versions.packageName.get()
applicationId = libs.versions.applicationId.get()
minSdk = libs.versions.minSdk.get().toInt()
versionCode = libs.versions.appVersionCode.get().toInt()
versionName = libs.versions.appVersionName.get()
@@ -95,14 +95,8 @@ class SatelliteRepo(
}
}
override suspend fun getRadios(
sat: OrbitalObject,
pos: GeoPos,
radios: List<SatRadio>,
time: Long
): List<SatRadio> {
override suspend fun getRadios(satPos: OrbitalPos, radios: List<SatRadio>): List<SatRadio> {
return withContext(dispatcher) {
val satPos = sat.getPosition(pos, time)
radios.map { transmitter ->
transmitter.copy(
downlinkLow = transmitter.downlinkLow?.let { satPos.getDownlinkFreq(it) },
@@ -242,13 +236,16 @@ class SatelliteRepo(
if (elevation > maxElevation) maxElevation = elevation
} while (elevation < 0.0)
// refine AOS to ~500ms precision
calendarTimeMillis -= 60L * 1000L
do {
calendarTimeMillis += 500L
elevation = sat.getElevation(pos, calendarTimeMillis)
if (elevation > maxElevation) maxElevation = elevation
} while (elevation < 0.0)
// refine AOS to ~500ms precision via binary search.
// Elevation is monotonic across the horizon crossing, so binary search
// finds the crossing in ~8 SGP4 calls instead of up to 120 linear steps.
var aosLo = calendarTimeMillis - 60L * 1000L // elevation < 0 (below horizon)
var aosHi = calendarTimeMillis // elevation >= 0 (above horizon)
while (aosHi - aosLo > 500L) {
val mid = (aosLo + aosHi) / 2
if (sat.getElevation(pos, mid) < 0.0) aosLo = mid else aosHi = mid
}
calendarTimeMillis = aosHi
// Get full position for AOS data (azimuth, altitude)
val aosPos = sat.getFullPosition(pos, calendarTimeMillis)
@@ -262,13 +259,14 @@ class SatelliteRepo(
if (elevation > maxElevation) maxElevation = elevation
} while (elevation > 0.0)
// refine LOS to ~500ms precision
calendarTimeMillis -= 30L * 1000L
do {
calendarTimeMillis += 500L
elevation = sat.getElevation(pos, calendarTimeMillis)
if (elevation > maxElevation) maxElevation = elevation
} while (elevation > 0.0)
// refine LOS to ~500ms precision via binary search (same monotonic argument)
var losLo = calendarTimeMillis - 30L * 1000L // elevation > 0 (above horizon)
var losHi = calendarTimeMillis // elevation <= 0 (below horizon)
while (losHi - losLo > 500L) {
val mid = (losLo + losHi) / 2
if (sat.getElevation(pos, mid) > 0.0) losLo = mid else losHi = mid
}
calendarTimeMillis = losHi
// Get full position for LOS data (azimuth, altitude)
val losPos = sat.getFullPosition(pos, calendarTimeMillis)
@@ -213,8 +213,8 @@ class SettingsRepo(
}
private fun setStationPosition(latitude: Double, longitude: Double, altitude: Double, locator: String) {
val newLat = latitude.round(4)
val newLon = longitude.round(4)
val newLat = latitude.round(5)
val newLon = longitude.round(5)
val newAlt = altitude.round(1)
val timestamp = System.currentTimeMillis()
println("Received new Position($newLat, $newLon, $newAlt) & Locator $locator")
@@ -61,7 +61,7 @@ interface ISatelliteRepo {
suspend fun getTrack(sat: OrbitalObject, pos: GeoPos, start: Long, end: Long): List<OrbitalPos>
/** Get Doppler-shifted radio frequencies for a satellite at the given time. */
suspend fun getRadios(sat: OrbitalObject, pos: GeoPos, radios: List<SatRadio>, time: Long): List<SatRadio>
suspend fun getRadios(satPos: OrbitalPos, radios: List<SatRadio>): List<SatRadio>
/** Fetch radio transceivers for a satellite by its catalog number. */
suspend fun getRadiosWithId(id: Int): List<SatRadio>
@@ -1,4 +1,4 @@
/*
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
@@ -11,33 +11,24 @@ package com.rtbishop.look4sat.core.domain.utility
import com.rtbishop.look4sat.core.domain.model.SatRadio
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
import java.util.Locale
/**
* Computes Doppler-corrected reciprocal frequencies for linear transponders.
*
* The full physical path:
* For a linear (passband) transponder, uplink and downlink frequencies are
* related by a fixed passband offset. When the satellite moves, both are
* Doppler-shifted. Given one, we compute the other:
*
* TX→RX (uplink → downlink):
* ① 地面发射 f_tx
* ② 卫星收到 f_tx × (c - v) / c (上行多普勒)
* ③ 卫星转发 = passband映射(②) (在卫星上做映射)
* ④ 地面听到 ③ × (c - v) / c (下行多普勒)
*
* RX→TX (downlink → uplink):
* ④ 地面听到 f_rx
* ③ 卫星转发 = f_rx × (c + v) / c (逆下行多普勒)
* ② 卫星收到 = 逆passband映射(③)
* ① 地面应发射 = ② × (c + v) / c (逆上行多普勒)
* downlink → uplink: mapDownlinkToUplink (passband) → getUplinkFreq (Doppler)
* uplink → downlink: mapUplinkToDownlink (passband) → getDownlinkFreq (Doppler)
*
* Addresses GitHub issue #91 (Custom frequency Doppler correction).
*/
object DopplerFrequencyCalculator {
/**
* Given a downlink frequency (what the user hears), compute the
* uplink frequency the user should transmit.
* Full path: ④→③→②→①
* Given a downlink frequency, compute the Doppler-corrected uplink frequency.
* Returns null if the transponder is not a linear passband type.
*/
fun computeUplinkFromDownlink(
downlinkHz: Long,
@@ -45,22 +36,14 @@ object DopplerFrequencyCalculator {
orbitalPos: OrbitalPos
): Long? {
if (!isLinearTransponder(transponder)) return null
// ④→③ 逆下行多普勒:卫星转发的频率
val satTx = orbitalPos.getUplinkFreq(downlinkHz)
// ③→② 逆 passband 映射
val satRx = TransponderMapper.mapDownlinkToUplink(satTx, transponder) ?: return null
// ②→① 逆上行多普勒:地面应发射的频率
return orbitalPos.getUplinkFreq(satRx)
val baseUplink = TransponderMapper.mapDownlinkToUplink(downlinkHz, transponder) ?: return null
return orbitalPos.getUplinkFreq(baseUplink)
}
/**
* Given a downlink frequency (what the user hears), compute the
* uplink frequency the user should transmit, with an offset applied
* to the downlink (in Hz).
* Full path: ④→③→②→①
*
* The user-entered downlink frequency already includes the offset, so subtract
* it before the inverse downlink Doppler.
* Given a downlink frequency, compute the Doppler-corrected uplink frequency
* with an offset applied to the downlink (in Hz).
* Returns null if the transponder is not a linear passband type.
*/
fun computeUplinkFromDownlinkWithOffset(
downlinkHz: Long,
@@ -69,20 +52,13 @@ object DopplerFrequencyCalculator {
offsetHz: Long
): Long? {
if (!isLinearTransponder(transponder)) return null
// ④→③ 逆下行多普勒:卫星转发的频率(含 offset)
val satTxWithOffset = orbitalPos.getUplinkFreq(downlinkHz)
// ③ 去掉 offset(offset 在卫星本地频率域)
val satTx = satTxWithOffset - offsetHz
// ③→② 逆 passband 映射
val satRx = TransponderMapper.mapDownlinkToUplink(satTx, transponder) ?: return null
// ②→① 逆上行多普勒:地面应发射的频率
return orbitalPos.getUplinkFreq(satRx)
val baseUplink = TransponderMapper.mapDownlinkToUplink(downlinkHz + offsetHz, transponder) ?: return null
return orbitalPos.getUplinkFreq(baseUplink)
}
/**
* Given an uplink frequency (what the user transmits), compute the
* downlink frequency the user will hear.
* Full path: ①→②→③→④
* Given an uplink frequency, compute the Doppler-corrected downlink frequency.
* Returns null if the transponder is not a linear passband type.
*/
fun computeDownlinkFromUplink(
uplinkHz: Long,
@@ -90,19 +66,14 @@ object DopplerFrequencyCalculator {
orbitalPos: OrbitalPos
): Long? {
if (!isLinearTransponder(transponder)) return null
// ①→② 上行多普勒:卫星收到的频率
val satRx = orbitalPos.getDownlinkFreq(uplinkHz)
// ②→③ passband 映射
val satTx = TransponderMapper.mapUplinkToDownlink(satRx, transponder) ?: return null
// ③→④ 下行多普勒:地面听到的
return orbitalPos.getDownlinkFreq(satTx)
val baseDownlink = TransponderMapper.mapUplinkToDownlink(uplinkHz, transponder) ?: return null
return orbitalPos.getDownlinkFreq(baseDownlink)
}
/**
* Given an uplink frequency (what the user transmits), compute the
* downlink frequency the user will hear, with an offset applied
* to the downlink (in Hz).
* Full path: ①→②→③→④
* Given an uplink frequency, compute the Doppler-corrected downlink frequency
* with an offset applied to the downlink (in Hz).
* Returns null if the transponder is not a linear passband type.
*/
fun computeDownlinkFromUplinkWithOffset(
uplinkHz: Long,
@@ -111,13 +82,8 @@ object DopplerFrequencyCalculator {
offsetHz: Long
): Long? {
if (!isLinearTransponder(transponder)) return null
// ①→② 上行多普勒:卫星收到的频率
val satRx = orbitalPos.getDownlinkFreq(uplinkHz)
// ②→③ passband 映射
val satTx = TransponderMapper.mapUplinkToDownlink(satRx, transponder) ?: return null
// ③ 加上 offset(offset 在卫星本地频率域)
// ③→④ 下行多普勒:地面听到的
return orbitalPos.getDownlinkFreq(satTx + offsetHz)
val baseDownlink = TransponderMapper.mapUplinkToDownlink(uplinkHz, transponder) ?: return null
return orbitalPos.getDownlinkFreq(baseDownlink + offsetHz)
}
/** True if this transponder supports linear passband mapping. */
@@ -129,26 +95,4 @@ object DopplerFrequencyCalculator {
return upLow != null && upHigh != null && downLow != null && downHigh != null
&& upLow != upHigh && downLow != downHigh
}
/**
* True for the radio entry that should drive the standalone Calculator page.
*
* A frequency range alone is not enough: some non-user-facing or drifting data entries
* can also have low/high frequencies. The calculator is meant for the named linear
* transponder entry, e.g. "Linear Transponder", "Linear Transp.", "SSB Transponder".
*/
fun isNamedLinearTransponder(transponder: SatRadio): Boolean {
if (!isLinearTransponder(transponder)) return false
val info = transponder.info.lowercase(Locale.ENGLISH)
val modes = listOfNotNull(transponder.downlinkMode, transponder.uplinkMode)
.joinToString(separator = " ")
.lowercase(Locale.ENGLISH)
val hasLinearName = info.contains("linear")
val hasTransponderName = info.contains("transponder") || info.contains("transp") ||
info.contains("xponder") || info.contains("xpdr")
val hasLinearMode = listOf("ssb", "usb", "lsb", "cw").any { modes.contains(it) }
return (hasLinearName && hasTransponderName) || (hasTransponderName && hasLinearMode)
}
}
@@ -19,31 +19,78 @@ package com.rtbishop.look4sat.core.domain.utility
import com.rtbishop.look4sat.core.domain.predict.GeoPos
/**
* Converts a Maidenhead locator (QTH grid square) to a GeoPos.
* Supports 6-char (3 pair), 8-char (4 pair) and 10-char (5 pair) locators.
* The returned position is the center of the finest cell encoded by the locator:
* - 6 char: 5' lon x 2.5' lat cell center
* - 8 char: 30" lon x 15" lat cell center
* - 10 char: 1.25" lon x 0.625" lat cell center
*/
fun qthToPosition(locator: String): GeoPos? {
val trimmedQth = locator.take(6)
val trimmedQth = locator.trim().uppercase()
if (!isValidLocator(trimmedQth)) return null
val lonFirst = (trimmedQth[0].uppercaseChar().code - 65) * 20
val latFirst = (trimmedQth[1].uppercaseChar().code - 65) * 10
val lonFirst = (trimmedQth[0].code - 65) * 20
val latFirst = (trimmedQth[1].code - 65) * 10
val lonSecond = trimmedQth[2].toString().toInt() * 2
val latSecond = trimmedQth[3].toString().toInt()
val lonThird = (((trimmedQth[4].lowercaseChar().code - 97) / 12.0) + (1.0 / 24.0)) - 180
val latThird = (((trimmedQth[5].lowercaseChar().code - 97) / 24.0) + (1.0 / 48.0)) - 90
val longitude = (lonFirst + lonSecond + lonThird).round(4)
val latitude = (latFirst + latSecond + latThird).round(4)
return GeoPos(latitude, longitude)
val lonThird = (trimmedQth[4].lowercaseChar().code - 97) / 12.0
val latThird = (trimmedQth[5].lowercaseChar().code - 97) / 24.0
var longitude = lonFirst + lonSecond + lonThird - 180
var latitude = latFirst + latSecond + latThird - 90
// 8-char extension: 4th pair, digits, 30" lon x 15" lat cells
if (trimmedQth.length >= 8) {
longitude += trimmedQth[6].toString().toInt() / 120.0
latitude += trimmedQth[7].toString().toInt() / 240.0
}
// 10-char extension: 5th pair, letters, 1.25" lon x 0.625" lat cells
if (trimmedQth.length >= 10) {
longitude += (trimmedQth[8].lowercaseChar().code - 97) / 2880.0
latitude += (trimmedQth[9].lowercaseChar().code - 97) / 5760.0
}
// Offset to the center of the finest encoded cell
when (trimmedQth.length) {
8 -> {
longitude += 1.0 / 240.0
latitude += 1.0 / 480.0
}
10 -> {
longitude += 1.0 / 5760.0
latitude += 1.0 / 11520.0
}
else -> {
longitude += 1.0 / 24.0
latitude += 1.0 / 48.0
}
}
return GeoPos(latitude.round(6), longitude.round(6))
}
fun positionToQth(latitude: Double, longitude: Double): String? {
/**
* Converts a GeoPos to a Maidenhead locator (QTH grid square).
* Default precision is 8 characters (4 pairs) giving 30" lon x 15" lat resolution,
* matching common 8-char grid square tools. Pass precision = 6 for the classic
* 5' x 2.5' resolution, or precision = 10 for the finest 1.25" x 0.625" resolution.
*/
fun positionToQth(latitude: Double, longitude: Double, precision: Int = 8): String? {
if (!isValidPosition(latitude, longitude)) return null
val newLongitude = if (longitude > 180.0) longitude else longitude + 180
val newLongitude = longitude + 180
val newLatitude = latitude + 90
val lonFirst = (65 + (newLongitude / 20)).toInt().toChar()
val latFirst = (65 + (newLatitude / 10)).toInt().toChar()
val lonSecond = ((newLongitude / 2) % 10).toInt()
val lonFirst = (65 + (newLongitude / 20).toInt().coerceIn(0, 17)).toChar()
val latFirst = (65 + (newLatitude / 10).toInt().coerceIn(0, 17)).toChar()
val lonSecond = ((newLongitude % 20) / 2).toInt()
val latSecond = (newLatitude % 10).toInt()
val lonThird = (65 + (newLongitude % 2) * 12).toInt().toChar().lowercaseChar()
val latThird = (65 + (newLatitude % 1) * 24).toInt().toChar().lowercaseChar()
return "$lonFirst$latFirst$lonSecond$latSecond$lonThird$latThird"
val qth = "$lonFirst$latFirst$lonSecond$latSecond$lonThird$latThird"
if (precision < 8) return qth
val lonFourth = ((newLongitude % (1.0 / 12.0)) * 120).toInt()
val latFourth = ((newLatitude % (1.0 / 24.0)) * 240).toInt()
val qth8 = "$qth$lonFourth$latFourth"
if (precision < 10) return qth8
val lonFifth = (65 + (newLongitude % (1.0 / 120.0)) * 2880).toInt().toChar().lowercaseChar()
val latFifth = (65 + (newLatitude % (1.0 / 240.0)) * 5760).toInt().toChar().lowercaseChar()
return "$qth8$lonFifth$latFifth"
}
private fun isValidPosition(lat: Double, lon: Double): Boolean {
@@ -51,5 +98,49 @@ private fun isValidPosition(lat: Double, lon: Double): Boolean {
}
private fun isValidLocator(locator: String): Boolean {
return locator.matches("[a-xA-X][a-xA-X]\\d\\d[a-xA-X][a-xA-X]".toRegex())
return locator.matches("[a-xA-X]{2}\\d{2}[a-xA-X]{2}(?:\\d{2}(?:[a-xA-X]{2})?)?".toRegex())
}
/**
* Returns the 4-char field+square part of a locator, e.g. "OL42ih45" -> "OL42".
* A 4-char input is returned as-is when valid.
*/
fun qthToSquare(locator: String): String {
val upper = locator.trim().uppercase()
return when {
upper.length >= 4 && isValidLocator(upper) -> upper.take(4)
upper.length == 4 && upper.matches("[a-xA-X]{2}\\d{2}".toRegex()) -> upper
else -> "----"
}
}
/**
* Builds the 3x3 grid of 4-char squares surrounding [square] (e.g. "OL42").
* Row 0 = north (lat +1), col 0 = west (lon -1). Handles field/square carry
* at boundaries (e.g. "AA00" wraps to "RR99" at the south-west corner).
* Mirrors the neighbor logic decompiled from the QTH定位器 app.
*/
fun qthNeighbors(square: String): List<String> {
if (square.length != 4) return emptyList()
val lonField = (square[0].uppercaseChar().code - 65).coerceIn(0, 17)
val latField = (square[1].uppercaseChar().code - 65).coerceIn(0, 17)
val lonSquare = square[2].digitToCharOrNull() ?: return emptyList()
val latSquare = square[3].digitToCharOrNull() ?: return emptyList()
val result = mutableListOf<String>()
for (dLat in 1 downTo -1) { // north -> south
for (dLon in -1..1) { // west -> east
var lf = lonField
var tf = latField
var ls = lonSquare + dLon
var ts = latSquare + dLat
if (ls < 0) { lf -= 1; ls = 9 } else if (ls > 9) { lf += 1; ls = 0 }
if (ts < 0) { tf -= 1; ts = 9 } else if (ts > 9) { tf += 1; ts = 0 }
lf = (lf + 18) % 18
tf = (tf + 18) % 18
result += "${('A' + lf).toChar()}${('A' + tf).toChar()}$ls$ts"
}
}
return result
}
private fun Char.digitToCharOrNull(): Int? = digitToIntOrNull()
@@ -13,15 +13,12 @@ class DopplerFrequencyCalculatorTest {
upHigh: Long = 145_500_000L,
downLow: Long = 435_000_000L,
downHigh: Long? = 435_500_000L,
inverted: Boolean = false,
info: String = "Linear Transponder",
downlinkMode: String? = "USB",
uplinkMode: String? = "LSB"
inverted: Boolean = false
) = SatRadio(
uuid = "linear", info = info, isAlive = true,
uuid = "linear", info = "Linear Transponder", isAlive = true,
downlinkLow = downLow, downlinkHigh = downHigh,
downlinkMode = downlinkMode, uplinkLow = upLow, uplinkHigh = upHigh,
uplinkMode = uplinkMode, isInverted = inverted, catnum = 12345
downlinkMode = "USB", uplinkLow = upLow, uplinkHigh = upHigh,
uplinkMode = "LSB", isInverted = inverted, catnum = 12345
)
private fun fmTransponder() = SatRadio(
@@ -51,28 +48,6 @@ class DopplerFrequencyCalculatorTest {
assertFalse(DopplerFrequencyCalculator.isLinearTransponder(xpdr))
}
@Test
fun isNamedLinearTransponder_returnsTrueForLinearTransponderName() {
assertTrue(DopplerFrequencyCalculator.isNamedLinearTransponder(linearTransponder()))
}
@Test
fun isNamedLinearTransponder_returnsTrueForSsbTransponderName() {
val xpdr = linearTransponder(info = "Mode V/U SSB Transponder", downlinkMode = "USB", uplinkMode = "LSB")
assertTrue(DopplerFrequencyCalculator.isNamedLinearTransponder(xpdr))
}
@Test
fun isNamedLinearTransponder_returnsFalseForRangeEntryWithoutTransponderName() {
val driftingRangeEntry = linearTransponder(info = "Upper side band (drifting)")
assertFalse(DopplerFrequencyCalculator.isNamedLinearTransponder(driftingRangeEntry))
}
@Test
fun isNamedLinearTransponder_returnsFalseForFmRepeater() {
assertFalse(DopplerFrequencyCalculator.isNamedLinearTransponder(fmTransponder()))
}
@Test
fun computeUplinkFromDownlink_linear_noDoppler() {
val xpdr = linearTransponder()
@@ -118,53 +93,6 @@ class DopplerFrequencyCalculatorTest {
assertNull(result)
}
@Test
fun computeDownlinkFromUplink_withPositiveOffset_addsOffsetToDownlink() {
val xpdr = linearTransponder()
val orbitalPos = pos(0.0)
val downlink = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
uplinkHz = 145_200_000L,
transponder = xpdr,
orbitalPos = orbitalPos,
offsetHz = 2_500L
)
assertEquals(435_202_500L, downlink)
}
@Test
fun computeUplinkFromDownlink_withPositiveOffset_subtractsOffsetBeforeMapping() {
val xpdr = linearTransponder()
val orbitalPos = pos(0.0)
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
downlinkHz = 435_202_500L,
transponder = xpdr,
orbitalPos = orbitalPos,
offsetHz = 2_500L
)
assertEquals(145_200_000L, uplink)
}
@Test
fun computeOffsetRoundTrip_handlesNegativeOffset() {
val xpdr = linearTransponder()
val orbitalPos = pos(0.0)
val downlink = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
uplinkHz = 145_200_000L,
transponder = xpdr,
orbitalPos = orbitalPos,
offsetHz = -2_500L
)
assertEquals(435_197_500L, downlink)
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
downlinkHz = downlink!!,
transponder = xpdr,
orbitalPos = orbitalPos,
offsetHz = -2_500L
)
assertEquals(145_200_000L, uplink)
}
@Test
fun computeUplinkFromDownlink_invertedTransponder() {
val xpdr = linearTransponder(inverted = true, downHigh = 435_500_000L)
@@ -18,7 +18,9 @@
package com.rtbishop.look4sat.core.domain
import com.rtbishop.look4sat.core.domain.utility.positionToQth
import com.rtbishop.look4sat.core.domain.utility.qthNeighbors
import com.rtbishop.look4sat.core.domain.utility.qthToPosition
import com.rtbishop.look4sat.core.domain.utility.qthToSquare
import org.junit.Test
class QthConverterTest {
@@ -26,21 +28,36 @@ class QthConverterTest {
@Test
fun `Given valid QTH returns correct POS`() {
var result = qthToPosition("io91VL39FX")
assert(result?.latitude == 51.4792 && result.longitude == -0.2083)
assert(result?.latitude == 51.499913 && result.longitude == -0.22309)
result = qthToPosition("gf15vc")
assert(result?.latitude == -34.8958 && result.longitude == -56.2083)
assert(result?.latitude == -34.895833 && result.longitude == -56.208333)
// 8-char locators: finer 30" x 15" cell center
result = qthToPosition("io91vl47")
assert(result?.latitude == 51.489583 && result.longitude == -0.2125)
result = qthToPosition("jn58td25")
assert(result?.latitude == 48.147917 && result.longitude == 11.604167)
}
@Test
fun `Given invalid QTH returns null`() {
assert(qthToPosition("ZZ00zz") == null)
assert(qthToPosition("JN58") == null)
assert(qthToPosition("io9") == null)
assert(qthToPosition("IO91VL7") == null)
assert(qthToPosition("IO91VL4X") == null)
}
@Test
fun `Given valid POS returns correct QTH`() {
assert(positionToQth(51.4878, -0.2146) == "IO91vl")
assert(positionToQth(48.1466, 11.6083) == "JN58td")
// default precision is 8 chars
assert(positionToQth(51.4878, -0.2146) == "IO91vl47")
assert(positionToQth(48.1466, 11.6083) == "JN58td25")
// 6-char precision still available for backwards compatibility
assert(positionToQth(51.4878, -0.2146, 6) == "IO91vl")
assert(positionToQth(48.1466, 11.6083, 6) == "JN58td")
// 10-char precision
assert(positionToQth(51.4878, -0.2146, 10) == "IO91vl47fb")
assert(positionToQth(48.1466, 11.6083, 10) == "JN58td25xe")
}
@Test
@@ -48,4 +65,61 @@ class QthConverterTest {
assert(positionToQth(91.0542, -170.1142) == null)
assert(positionToQth(89.0542, -240.1142) == null)
}
@Test
fun `Given boundary POS stays in valid grid`() {
// antipodal / edge cases must not overflow the A-R / 0-9 / a-x alphabet
assert(positionToQth(-90.0, -180.0, 8) == "AA00aa00")
assert(positionToQth(90.0, 180.0, 8) == "RR00aa00")
assert(positionToQth(0.0, 0.0, 8) == "JJ00aa00")
// roundtrip stability: 8-char roundtrip is stable across a sample of positions
val positions = listOf(
Pair(51.4878, -0.2146), Pair(48.1466, 11.6083), Pair(-33.8688, 151.2093),
Pair(39.9042, 116.4074), Pair(35.6895, 139.6917), Pair(41.714, -72.727)
)
positions.forEach { (lat, lon) ->
val qth = positionToQth(lat, lon, 8)
val pos = qthToPosition(qth!!)
val qth2 = positionToQth(pos!!.latitude, pos.longitude, 8)
assert(qth == qth2) { "Roundtrip failed for ($lat, $lon): $qth -> $qth2" }
}
}
@Test
fun `Given square returns correct 3x3 neighbors`() {
// Reference grid from the QTH定位器 screenshot: OL42
val neighbors = qthNeighbors("OL42")
assert(neighbors == listOf(
"OL33", "OL43", "OL53",
"OL32", "OL42", "OL52",
"OL31", "OL41", "OL51"
)) { "OL42 grid mismatch: $neighbors" }
// Center cell must be the input itself
assert(neighbors[4] == "OL42")
// 9 cells, all distinct
assert(neighbors.size == 9 && neighbors.toSet().size == 9)
}
@Test
fun `Given boundary square wraps fields correctly`() {
// South-west corner: AA00 neighbors wrap to RR99 / RA90 etc.
val sw = qthNeighbors("AA00")
assert(sw.size == 9 && sw.toSet().size == 9)
assert(sw[0] == "RA91" && sw[4] == "AA00" && sw[6] == "RR99" && sw[8] == "AR19")
// North-east corner: RR99 wraps to AA00
val ne = qthNeighbors("RR99")
assert(ne.size == 9 && ne.toSet().size == 9)
assert(ne[0] == "RA80" && ne[4] == "RR99" && ne[8] == "AR08")
// Field boundary: IO91's east neighbors cross into J field
val london = qthNeighbors("IO91")
assert(london[2] == "JO02" && london[5] == "JO01")
}
@Test
fun `Given full locator returns square part`() {
assert(qthToSquare("OL42ih45") == "OL42")
assert(qthToSquare("io91VL39FX") == "IO91")
assert(qthToSquare("JN58") == "JN58")
assert(qthToSquare("garbage!!") == "----")
}
}
@@ -38,6 +38,9 @@ sealed class Screen(val iconResId: Int, val titleResId: Int) : NavKey {
@Serializable
data object Mutual : Screen(R.drawable.ic_match, R.string.nav_mutual)
@Serializable
data object Roaming : Screen(R.drawable.ic_roaming, R.string.nav_roaming)
@Serializable
data object Settings : Screen(R.drawable.ic_settings, R.string.nav_prefs)
}
@@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="96"
android:viewportHeight="96">
<!-- Crosshair / grid-locator style icon: concentric rings + center dot -->
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="6"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M 48 12 L 48 84 M 12 48 L 84 48" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="6"
android:pathData="M 48 26 A 22 22 0 1 0 48.1 26" />
<path
android:fillColor="@android:color/white"
android:pathData="M 44 44 L 52 44 L 52 52 L 44 52 Z" />
</vector>
@@ -51,11 +51,11 @@
\n\nDoğru tahminler alabilmek için veritabanını en az haftada bir güncelleyin.</string>
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
<string name="pass_whatsnew_message" translatable="false">
* Switched to short-form commands in NetworkReporter, by theojalba
\n* Added CAT radio control for full-duplex SAT operation, by jcmerg
\n* Added normalized time for passes AOS/LOS, fixed refresh state bug
\n* Added multiple performance tweaks, improved passes calculation
\n* Fixed UTC pass time problem mentioned in issue #201
* Roaming sayfası QTH定位器 2.0'dan birebir taşındı: 8 haneli konum, 3x3 ızgara, kırmızı işaret, canlı GPS
\n\n* İçeriğin sistem durum/gezinme çubuklarıyla çakışması düzeltildi; ızgara sütunları referans düzenle eşleşiyor
\n\n* Roaming sayfası renkleri uygulama temasını takip eder - kırmızı gece modunda tamamen görünür
\n\n* Ayarlar: sıkışan anahtar satırları ve teşekkür aralığı düzeltildi
\n\n* Uygulama adı artık Look4Sat Pro
</string>
<!-- Radar screen -->
@@ -103,7 +103,7 @@
<string name="map_visible">Görünür</string>
<!-- Settings screen -->
<string name="prefs_app_title" translatable="false">Look4Sat v%s</string>
<string name="prefs_app_title" translatable="false">Look4Sat Pro v%s</string>
<string name="prefs_app_url" translatable="false">https://play.google.com/store/apps/details?id=com.rtbishop.look4sat</string>
<string name="prefs_donate_title">Bağış yap</string>
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
@@ -192,7 +192,7 @@
<string name="prefs_highlight_mid">Orta %1$d-%2$d°</string>
<string name="prefs_highlight_high">Yüksek &gt; %1$d°</string>
<string name="prefs_outro_title">Teşekkürler</string>
<string name="prefs_outro_title">BG7NTA ve orijinal yazar teşekkür eder</string>
<string name="prefs_outro_thanks" translatable="false">
• Look4Sat users and contributors!
\n• David A. B. Johnson (predict4java)
@@ -200,7 +200,8 @@
\n• Alexandru Csete (Gpredict)
\n• Dr T.S. Kelso (Celestrak)
\n• Libre Space Foundation (SatNOGS)
\n• BG7NTA (CW decoding feature)</string>
\n• BA7OPF (pass matching feature)
\n• BG7NTA</string>
<string name="prefs_outro_license">Bu uygulama herhangi bir garanti sunmaz</string>
</resources>
@@ -10,6 +10,7 @@
<string name="nav_pass">过境</string>
<string name="nav_radar">雷达</string>
<string name="nav_mutual">匹配</string>
<string name="nav_roaming">漫游</string>
<string name="nav_map">地图</string>
<string name="nav_prefs">设置</string>
@@ -38,6 +39,14 @@
<string name="pass_error_message">没有即将过境的卫星</string>
<string name="pass_welcome_title">欢迎来到 Look4Sat\!</string>
<string name="pass_welcome_message">请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性</string>
<string name="pass_whatsnew_title">Look4Sat Pro 更新内容</string>
<string name="pass_whatsnew_message">
* 漫游页完整移植 QTH定位器 2.0:8 位网格、3x3 九宫格、红点查表定位、实时 GPS
\n\n* 修复内容被系统状态栏/导航栏遮挡;九宫格列宽与原版一致
\n\n* 漫游页配色跟随应用主题,红色夜间模式下清晰可见
\n\n* 设置:修复开关行挤压与感谢列表间距
\n\n* 应用名现为 Look4Sat Pro
</string>
<!-- Radar screen -->
<string name="radar_back">后退</string>
@@ -92,7 +101,7 @@
<string name="prefs_updated_title">更新:%s</string>
<string name="prefs_updated_time">yyyy MMM d - HH:mm:ss</string>
<string name="prefs_loc_title">站点位置</string>
<string name="prefs_loc_title">站位</string>
<string name="prefs_loc_gps_title">GPS 定位</string>
<string name="prefs_loc_gps_error">检查您的位置权限</string>
<string name="prefs_loc_input_title">输入位置</string>
@@ -100,11 +109,11 @@
<string name="prefs_loc_qth_title">QTH 网格</string>
<string name="prefs_loc_qth_error">无效的 QTH 网格</string>
<string name="prefs_loc_success">位置更新成功</string>
<string name="prefs_station_title">站点位置设置</string>
<string name="prefs_station_lat_text">输入站点的纬度</string>
<string name="prefs_station_lon_text">输入站点的经度</string>
<string name="prefs_station_title">站位设置</string>
<string name="prefs_station_lat_text">输入站位的纬度</string>
<string name="prefs_station_lon_text">输入站位的经度</string>
<string name="prefs_locator_title">QTH 网格</string>
<string name="prefs_locator_text">使用梅登黑德网格设置站点位置</string>
<string name="prefs_locator_text">使用梅登黑德网格设置站位</string>
<string name="prefs_data_title">卫星数据更新</string>
<string name="prefs_data_entries">卫星:%s</string>
@@ -167,7 +176,7 @@
<string name="prefs_highlight_mid">中 %1$d-%2$d°</string>
<string name="prefs_highlight_high">高 &gt; %1$d°</string>
<string name="prefs_outro_title">我要感谢:</string>
<string name="prefs_outro_title">BG7NTA 与原作者感谢:</string>
<string name="prefs_outro_thanks" translatable="false">
• Look4Sat 所有用户及贡献者!
\n• David A. B. Johnson (predict4java)
@@ -176,39 +185,8 @@
\n• Dr T.S. Kelso (Celestrak)
\n• Libre Space Foundation (SatNOGS)
\n• xdsopl 和 Robot36 贡献者!
\n• BG7NTA(CW解码功能)</string>
\n• BA7OPF(过境匹配功能)
\n• BG7NTA</string>
<string name="prefs_outro_license">该应用程序不提供任何保修.</string>
<!-- 匹配页 Mutual match page -->
<string name="mutual_title">过境匹配</string>
<string name="mutual_station_you">你的位置</string>
<string name="mutual_station_opposite">友台位置</string>
<string name="mutual_label_you">你</string>
<string name="mutual_label_opposite">友台</string>
<string name="mutual_status_calculating">正在计算双方共同可见窗口…</string>
<string name="mutual_status_error">请修正提示后重新查询</string>
<string name="mutual_status_matches">找到 %d 个匹配 · 点击卡片展开曲线</string>
<string name="mutual_status_none">未找到共同可见窗口,调整时间或仰角后重试</string>
<string name="mutual_status_idle">输入双方位置后点击查询</string>
<string name="mutual_chip_calculating">计算中</string>
<string name="mutual_chip_error">有错误</string>
<string name="mutual_chip_matches">%d 个匹配</string>
<string name="mutual_chip_none">无匹配</string>
<string name="mutual_chip_idle">待查询</string>
<string name="mutual_use_current">填入当前位置</string>
<string name="mutual_lat">纬度</string>
<string name="mutual_lon">经度</string>
<string name="mutual_grid">网格</string>
<string name="mutual_grid_hint">支持 4/6/8 位 Maidenhead 网格;也可直接输入经纬度</string>
<string name="mutual_min_elevation">最小仰角:%d°</string>
<string name="mutual_time_range">时间范围</string>
<string name="mutual_calculating">计算中…</string>
<string name="mutual_query">查询匹配</string>
<string name="mutual_elevation_you">你:%d°</string>
<string name="mutual_elevation_opposite">友台:%d°</string>
<string name="mutual_both_tracks">双方轨迹</string>
<string name="mutual_open_radar">查看雷达</string>
<string name="mutual_station_a">站点A</string>
<string name="mutual_station_b">站点B</string>
</resources>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Commmon -->
<string name="app_name" translatable="false">Look4Sat</string>
<string name="app_name" translatable="false">Look4Sat Pro</string>
<string name="btn_accept">Accept</string>
<string name="btn_cancel">Cancel</string>
<string name="empty_list_message">No data to show</string>
@@ -11,6 +11,7 @@
<string name="nav_pass">Passes</string>
<string name="nav_radar">Radar</string>
<string name="nav_mutual">Match</string>
<string name="nav_roaming">Roaming</string>
<string name="nav_map">Map</string>
<string name="nav_prefs">Settings</string>
@@ -52,8 +53,11 @@
\n\nPlease update the database at least weekly to get accurate predictions.</string>
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
<string name="pass_whatsnew_message" translatable="false">
* Added required tweaks to support Android 17 (API 37)
\n\n* Added ACCESS_LOCAL_NETWORK permission check to support network data output
* Roaming page ported verbatim from QTH定位器 2.0: 8-char locator, 3x3 grid, red marker lookup, live GPS
\n\n* Fixed content overlapping the system status/navigation bars; grid columns now match the reference layout
\n\n* Roaming page colors follow the app theme - fully visible in red night mode
\n\n* Settings: fixed squeezed toggle rows and credits spacing
\n\n* App name is now Look4Sat Pro
</string>
<!-- Radar screen -->
@@ -111,7 +115,7 @@
<string name="map_visible">Visible</string>
<!-- Settings screen -->
<string name="prefs_app_title" translatable="false">Look4Sat v%s</string>
<string name="prefs_app_title" translatable="false">Look4Sat Pro v%s</string>
<string name="prefs_app_url" translatable="false">https://play.google.com/store/apps/details?id=com.rtbishop.look4sat</string>
<string name="prefs_donate_title">Donate</string>
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
@@ -202,7 +206,7 @@
<string name="prefs_highlight_mid">Mid %1$d–%2$d°</string>
<string name="prefs_highlight_high">High &gt; %1$d°</string>
<string name="prefs_outro_title">I would like to say thanks to</string>
<string name="prefs_outro_title">BG7NTA &amp; the original author would like to thank</string>
<string name="prefs_outro_thanks" translatable="false">
* Look4Sat users and contributors!
\n* David A. B. Johnson (predict4java)
@@ -211,40 +215,9 @@
\n* Dr T.S. Kelso (Celestrak)
\n* Libre Space Foundation (SatNOGS)
\n* xdsopl and Robot36 contributors!
\n* BG7NTA (CW decoding feature)
\n* BA7OPF (pass matching feature)
\n* BG7NTA
</string>
<string name="prefs_outro_license">The app comes with no warranty</string>
<!-- Mutual match page -->
<string name="mutual_title">Pass Match</string>
<string name="mutual_station_you">Your Station</string>
<string name="mutual_station_opposite">Opposite Station</string>
<string name="mutual_label_you">You</string>
<string name="mutual_label_opposite">Opposite</string>
<string name="mutual_status_calculating">Calculating mutual visibility windows…</string>
<string name="mutual_status_error">Fix the reported issues and retry</string>
<string name="mutual_status_matches">%d matches found · tap a card to expand</string>
<string name="mutual_status_none">No mutual windows found. Adjust time or min elevation and retry</string>
<string name="mutual_status_idle">Enter both stations, then tap Query</string>
<string name="mutual_chip_calculating">Calculating</string>
<string name="mutual_chip_error">Error</string>
<string name="mutual_chip_matches">%d matches</string>
<string name="mutual_chip_none">No matches</string>
<string name="mutual_chip_idle">Idle</string>
<string name="mutual_use_current">Use current</string>
<string name="mutual_lat">Lat</string>
<string name="mutual_lon">Lon</string>
<string name="mutual_grid">Grid</string>
<string name="mutual_grid_hint">Supports 4/6/8-char Maidenhead grid; or enter lat/lon directly</string>
<string name="mutual_min_elevation">Min elevation: %d°</string>
<string name="mutual_time_range">Time range</string>
<string name="mutual_calculating">Calculating…</string>
<string name="mutual_query">Query Match</string>
<string name="mutual_elevation_you">You: %d°</string>
<string name="mutual_elevation_opposite">Opposite: %d°</string>
<string name="mutual_both_tracks">Both tracks</string>
<string name="mutual_open_radar">Open Radar</string>
<string name="mutual_station_a">Station A</string>
<string name="mutual_station_b">Station B</string>
</resources>
-38
View File
@@ -1,38 +0,0 @@
import com.android.build.api.dsl.LibraryExtension
import com.android.build.api.variant.CanProduceConsumerProguardFiles
plugins {
alias(libs.plugins.convention.featurePlugin)
}
android {
namespace = "com.rtbishop.look4sat.feature.cw"
compileOptions {
encoding = "UTF-8"
}
// 照搬 Morse Expert 1.15: native 解码器仅 armeabi-v7a
defaultConfig {
ndk {
abiFilters += listOf("armeabi-v7a")
}
}
}
// CW 类保持规则(JNI 按类名注册 + 照搬混淆类保逻辑), 由 app 的 R8 消费
// AGP 9: consumer 规则走 variant 级 CanProduceConsumerProguardFiles
androidComponents {
onVariants(selector().all()) { variant ->
(variant as? CanProduceConsumerProguardFiles)?.consumerProguardFiles?.add(
project.layout.projectDirectory.file("proguard-rules.pro")
)
}
}
dependencies {
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
}
// 照搬的 Java 源码含中文注释, 强制 UTF-8 编译(compileOptions 在部分 AGP 版本不生效)
tasks.withType<JavaCompile>().configureEach {
options.encoding = "UTF-8"
}
-31
View File
@@ -1,31 +0,0 @@
# Look4Sat Pro — CW 解码模块(照搬 Morse Expert 1.15)混淆规则
# 所有 CW 类保持原名: pas.* 通过 RegisterNatives 按类名注册 JNI(混淆即崩);
# 其余为照搬的混淆名类(命名已无混淆意义, 保持以保逻辑完整)。
# JNI 引擎(RegisterNatives 按类名/方法名查表)
-keep class pas.** { *; }
# 照搬的解码/UI 类(全部保持)
-keep class com.ve3nea.morse_expert.** { *; }
-keep class H2.** { *; }
-keep class I2.b { *; }
-keep class J2.** { *; }
-keep class k3.** { *; }
-keep class i3.** { *; }
-keep class g3.** { *; }
-keep class j3.** { *; }
-keep class s.** { *; }
-keep class d1.AbstractC1518b { *; }
-keep class B0.b { *; }
-keep class B.RunnableC0001b { *; }
-keep class D.n { *; }
-keep class E2.g { *; }
-keep class F2.a { *; }
-keep class I0.d { *; }
-keep class K1.a { *; }
-keep class j1.C1646n { *; }
# sun.misc stub(Android 无此类, 仅编译期; 防止 R8 异常处理)
-dontwarn sun.misc.**
-keep class sun.misc.Unsafe { *; }
-keep class sun.misc.Cleaner { *; }
@@ -1,63 +0,0 @@
package B;
import android.text.SpannableStringBuilder;
import com.ve3nea.morse_expert.DecodedTextView;
import com.ve3nea.morse_expert.MainActivity;
import H2.b;
/**
* 照搬自 Morse Expert 1.15 B.RunnableC0001b, 类名保持混淆原名。
* R8 合并 Runnable 手术: 构造按 smali 真实签名 5 参 (Object,Object,int,int,boolean),
* 字段 = i(Object)/h(Object)/g(int)/f(int); run() 仅保留 case 4(f==2 对应 pswitch_4:
* 解码文本 UI 更新, H2.b 调用), 其余 case(权限/Intent/通知等库代码)已裁剪。
* 注: jadx 4 参构造为错误还原, 已按 smali 修正。
*/
public final class RunnableC0001b implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final int g;
/* renamed from: h, reason: collision with root package name */
public final Object h;
/* renamed from: i, reason: collision with root package name */
public final Object i;
public /* synthetic */ RunnableC0001b(Object obj, Object obj2, int i4, int i5, boolean z) {
this.i = obj;
this.h = obj2;
this.g = i4;
this.f = i5;
}
@Override // java.lang.Runnable
public final void run() {
if (this.f == 2) {
MainActivity mainActivity = ((b) this.i).f620p;
if (mainActivity != null) {
SpannableStringBuilder spannableStringBuilder = (SpannableStringBuilder) this.h;
int i5 = this.g;
DecodedTextView decodedTextView = (DecodedTextView) mainActivity.f11035D.f11888g;
if (!decodedTextView.f11032n && !decodedTextView.f11033o.get()) {
decodedTextView.beginBatchEdit();
try {
int length2 = decodedTextView.length();
int min = Math.min(length2, i5);
if (min > 0) {
decodedTextView.getEditableText().delete(length2 - min, length2);
}
decodedTextView.q(spannableStringBuilder);
decodedTextView.endBatchEdit();
return;
} catch (Throwable th) {
decodedTextView.endBatchEdit();
throw th;
}
}
return;
}
return;
}
}
}
-61
View File
@@ -1,61 +0,0 @@
package B0;
import android.widget.TextView;
import com.ve3nea.morse_expert.MainActivity;
import com.ve3nea.morse_expert.ScaleView;
import java.util.Locale;
import d1.AbstractC1518b;
/**
* 照搬自 Morse Expert 1.15 B0.b, 类名保持混淆原名。
* R8 合并 Runnable 手术: 构造按 smali 真实签名 (H2/b, String, [I) -> f=4;
* 只保留 case 4(状态栏更新: dBFS/Hz/dB/WPM + ScaleView 频率标记, H2.b 调用),
* 其余 case(WorkManager/动画等库代码)已裁剪。
* r3 = scaleView.getHeight()(smali 证实, jadx 寄存器未展开)。
*/
public final class b implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final Object g;
/* renamed from: h, reason: collision with root package name */
public final Object h;
/* renamed from: i, reason: collision with root package name */
public final Object i;
public b(H2.b bVar, String str, int[] iArr) {
this.f = 4;
this.g = bVar;
this.h = iArr;
this.i = str;
}
@Override // java.lang.Runnable
public final void run() {
if (this.f == 4) {
MainActivity mainActivity = ((H2.b) this.g).f621q;
if (mainActivity != null) {
String str2 = (String) this.i;
int[] iArr = (int[]) this.h;
String concat;
String format = String.format(Locale.US, " %3d dBFS", Integer.valueOf(iArr[0]));
if (iArr[5] == 0) {
concat = format.concat(" Idle ");
} else {
concat = format.concat(String.format(Locale.US, " %4d Hz %2d dB %2d WPM ", Integer.valueOf(iArr[1]), Integer.valueOf(iArr[2]), Integer.valueOf(iArr[3])));
}
((TextView) mainActivity.f11035D.f11890i).setText(AbstractC1518b.e(concat, str2));
ScaleView scaleView = (ScaleView) mainActivity.f11035D.f11889h;
int i4 = iArr[1];
scaleView.f11041g = iArr[5];
// r3 = scaleView.getHeight()(smali 证实)
scaleView.f = (scaleView.getHeight() - 1) - Math.round(((Math.round(i4 * 0.128f) - H2.b.f606z) * scaleView.getHeight()) / H2.b.f603B);
scaleView.invalidate();
return;
}
return;
}
}
}
-69
View File
@@ -1,69 +0,0 @@
package D;
import android.graphics.Bitmap;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import com.ve3nea.morse_expert.WaterfallSurfaceView;
/**
* 照搬自 Morse Expert 1.15, 类名保持混淆原名。
* R8 合并 Runnable 手术: 只保留 case 1(瀑布图频谱->OpenGL 纹理, H2.b 调用),
* 其余 case(库/广告)已裁剪。
*/
public final /* synthetic */ class n implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ Object f224g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ Object f225h;
public /* synthetic */ n(Object obj, int i4, Object obj2) {
this.f = i4;
this.f224g = obj;
this.f225h = obj2;
}
@Override // java.lang.Runnable
public final void run() {
int i4 = this.f;
Object obj = this.f225h;
Object obj2 = this.f224g;
if (i4 == 1) {
float[] fArr = (float[]) obj;
J2.b bVar = ((WaterfallSurfaceView) obj2).f;
Bitmap bitmap = bVar.f733h;
int width = bVar.f730d.getWidth();
double d4 = width;
double b4 = (bVar.b() + 4.0d) % d4;
double d5 = bVar.f737l - b4;
if (d5 > d4 / 2.0d) {
d5 -= d4;
} else if (d5 < (-width) / 2.0d) {
d5 += d4;
}
if (Math.abs(d5) > 8.0d) {
bVar.f737l = (int) Math.floor(b4);
} else {
bVar.f732g = (d5 * 1.0E-5d) + bVar.f732g;
}
int height = bitmap.getHeight() - 1;
for (int i5 = 0; i5 <= height; i5++) {
J2.a aVar = bVar.f734i;
int round = Math.round((fArr[i5] * 4.0f) + 30.0f);
aVar.getClass();
bitmap.setPixel(0, height - i5, aVar.f727a[Math.max(0, Math.min(255, round))]);
}
int i6 = bVar.f737l;
GLES20.glBindTexture(3553, bVar.f728a[0]);
J2.b.a();
GLUtils.texSubImage2D(3553, 0, i6, 0, bitmap);
J2.b.a();
int i7 = bVar.f737l + 1;
bVar.f737l = i7;
bVar.f737l = i7 % width;
return;
}
}
}
-44
View File
@@ -1,44 +0,0 @@
package E2;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import com.ve3nea.morse_expert.MainActivity;
import com.ve3nea.morse_expert.ScaleView;
/**
* 照搬自 Morse Expert 1.15 (com.ve3nea.morse_expert), 类名保持混淆原名。
* R8 合并类手术: 只保留 case 1(瀑布图触摸选频率=应用逻辑),
* 其余 case(广告 P4 / appcompat 焦点 / 弹窗)为库代码, 已裁剪。
*/
public final class g implements View.OnTouchListener {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ Object f411g;
public /* synthetic */ g(Object obj, int i4) {
this.f = i4;
this.f411g = obj;
}
@Override // android.view.View.OnTouchListener
public final boolean onTouch(View view, MotionEvent motionEvent) {
int i4 = this.f;
Object obj = this.f411g;
if (i4 == 1) {
MainActivity mainActivity = (MainActivity) obj;
if (motionEvent.getActionMasked() == 0 && mainActivity.f11037F != null) {
// smali 证实: p1 先被重赋值为 f11035D.h(ScaleView)再 getHeight;
// 公式 = round((f606z + ((scaleH-1-y)*f603B/scaleH)) / 0.128f)
ScaleView scaleView = (ScaleView) mainActivity.f11035D.f11889h;
float y3 = motionEvent.getY();
int round = Math.round((H2.b.f606z + ((((scaleView.getHeight() - 1) - y3) * H2.b.f603B) / scaleView.getHeight())) / 0.128f);
mainActivity.f11037F.f622r.set(round);
Log.i("Waterfall", String.format("Touch at %d Hz`", Integer.valueOf(round)));
}
return false;
}
return false;
}
}
-53
View File
@@ -1,53 +0,0 @@
package F2;
import com.rtbishop.look4sat.feature.cw.R;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import com.ve3nea.morse_expert.DecodedTextView;
/* loaded from: classes.dex */
public final class a implements ActionMode.Callback {
/* renamed from: a, reason: collision with root package name */
public final /* synthetic */ DecodedTextView f564a;
public a(DecodedTextView decodedTextView) {
this.f564a = decodedTextView;
}
@Override // android.view.ActionMode.Callback
public final boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
int itemId = menuItem.getItemId();
DecodedTextView decodedTextView = this.f564a;
if (itemId != 73) {
if (itemId != 74) {
return false;
}
decodedTextView.s();
return true;
}
decodedTextView.r();
actionMode.finish();
return true;
}
@Override // android.view.ActionMode.Callback
public final boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
this.f564a.f11032n = true;
// 原: if (menu.findItem(R.id.shareText) == null) 检查(shareText 为原 APK 资源, 简化后始终添加)
menu.add(0, 74, 0, "Share").setIcon(R.drawable.ic_baseline_share_24);
menu.add(0, 73, 0, "Save").setIcon(R.drawable.ic_baseline_save_24);
return true;
}
@Override // android.view.ActionMode.Callback
public final void onDestroyActionMode(ActionMode actionMode) {
this.f564a.f11032n = false;
}
@Override // android.view.ActionMode.Callback
public final boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
return false;
}
}
-82
View File
@@ -1,82 +0,0 @@
package H2;
import android.media.AudioRecord;
import android.util.Log;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/* loaded from: classes.dex */
public final class a implements Runnable {
public final int f;
/* renamed from: g, reason: collision with root package name */
public final AudioRecord f596g;
/* renamed from: h, reason: collision with root package name */
public final ExecutorService f597h;
/* renamed from: i, reason: collision with root package name */
public final AtomicBoolean f598i;
/* renamed from: j, reason: collision with root package name */
public final short[] f599j;
/* renamed from: k, reason: collision with root package name */
public final float[] f600k;
/* renamed from: l, reason: collision with root package name */
public b f601l;
public a() {
int minBufferSize = AudioRecord.getMinBufferSize(8000, 16, 2);
int i4 = b.f604C;
this.f = i4;
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
this.f597h = newSingleThreadExecutor;
AtomicBoolean atomicBoolean = new AtomicBoolean(false);
this.f598i = atomicBoolean;
this.f599j = new short[i4];
this.f600k = new float[i4];
AudioRecord audioRecord = new AudioRecord(1, 8000, 16, 2, minBufferSize * 4);
this.f596g = audioRecord;
if (audioRecord.getState() == 1) {
audioRecord.startRecording();
if (audioRecord.getState() != 1) {
Log.e("AudioRecord", "cannot start");
}
atomicBoolean.set(true);
newSingleThreadExecutor.execute(this);
return;
}
Log.e("AudioInput", "Failed to initialize");
}
@Override // java.lang.Runnable
public final void run() {
while (true) {
try {
if (this.f598i.get()) {
float[] fArr = this.f600k;
AudioRecord audioRecord = this.f596g;
short[] sArr = this.f599j;
int i4 = this.f;
if (audioRecord.read(sArr, 0, i4) == i4) {
for (int i5 = 0; i5 < i4; i5++) {
fArr[i5] = sArr[i5] / 32768.0f;
}
b bVar = this.f601l;
if (bVar != null) {
bVar.a(fArr);
}
}
} else {
TimeUnit.MILLISECONDS.sleep(100L);
}
} catch (Exception e4) {
Log.e("Audio thread", "Exception: ", e4);
}
}
}
}
-582
View File
@@ -1,582 +0,0 @@
package H2;
import B.RunnableC0001b;
import D.n;
import android.content.Context;
import android.media.MediaScannerConnection;
import android.os.Handler;
import android.os.Looper;
import android.text.SpannableStringBuilder;
import android.text.format.DateFormat;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import com.ve3nea.morse_expert.MainActivity;
import com.ve3nea.morse_expert.WaterfallSurfaceView;
import i3.d;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import k3.s;
import pas.decoder;
import pas.nativedecoder;
/* loaded from: classes.dex */
public final class b {
/* renamed from: A, reason: collision with root package name */
public static final int f602A;
/* renamed from: B, reason: collision with root package name */
public static final int f603B;
/* renamed from: C, reason: collision with root package name */
public static final int f604C;
/* renamed from: D, reason: collision with root package name */
public static final d f605D;
/* renamed from: z, reason: collision with root package name */
public static final int f606z;
/* renamed from: a, reason: collision with root package name */
public final float[] f607a;
/* renamed from: b, reason: collision with root package name */
public final float[] f608b;
public final float[] c;
/* renamed from: d, reason: collision with root package name */
public final float[] f609d;
/* renamed from: e, reason: collision with root package name */
public final nativedecoder.TNativeDecoder f610e;
public int f;
/* renamed from: g, reason: collision with root package name */
public int f611g;
/* renamed from: h, reason: collision with root package name */
public int f612h;
/* renamed from: i, reason: collision with root package name */
public decoder.TDecoderState f613i;
/* renamed from: j, reason: collision with root package name */
public String f614j;
/* renamed from: k, reason: collision with root package name */
public final c f615k;
/* renamed from: l, reason: collision with root package name */
public int f616l;
/* renamed from: m, reason: collision with root package name */
public final I2.b f617m;
/* renamed from: n, reason: collision with root package name */
public final Handler f618n;
/* renamed from: o, reason: collision with root package name */
public WaterfallSurfaceView f619o;
/* renamed from: p, reason: collision with root package name */
public MainActivity f620p;
/* renamed from: q, reason: collision with root package name */
public MainActivity f621q;
/* renamed from: r, reason: collision with root package name */
public final AtomicInteger f622r;
/* renamed from: s, reason: collision with root package name */
public final AtomicBoolean f623s;
/* renamed from: t, reason: collision with root package name */
public final Context f624t;
/* renamed from: u, reason: collision with root package name */
public int f625u;
/* renamed from: v, reason: collision with root package name */
public final AtomicBoolean f626v;
/* renamed from: w, reason: collision with root package name */
public DataOutputStream f627w;
/* renamed from: x, reason: collision with root package name */
public int f628x;
/* renamed from: y, reason: collision with root package name */
public String f629y;
/* JADX WARN: Removed duplicated region for block: B:14:0x012d */
/* JADX WARN: Type inference failed for: r0v4, types: [i3.d, java.lang.Object] */
static {
boolean z3;
float f;
float f4;
float f5;
int round = Math.round(25.6f);
f606z = round;
int round2 = Math.round(153.6f);
f602A = round2;
f603B = round2 - round;
f604C = Math.round(85.333336f);
i3.d obj = new i3.d();
if (2048 > 1073741824) {
z3 = true;
} else {
z3 = false;
}
obj.f11709l = z3;
int i4 = (int) 1024;
obj.f11700a = i4;
obj.f11701b = 1024L;
if (!z3) {
obj.f11708k = 1;
int[] iArr = new int[((int) g3.c.a((1 << (((int) (g3.c.d(((float) 1024) + 0.5f) / g3.c.d(2.0d))) / 2)) + 2)) + 2];
obj.c = iArr;
float[] fArr = new float[i4];
obj.f11703e = fArr;
int i5 = (i4 * 2) >> 2;
obj.f11704g = i5;
iArr[0] = i5;
iArr[1] = 1;
if (i5 > 2) {
int i6 = i5 >> 1;
float f6 = 0.7853982f / i6;
float f7 = 2.0f * f6;
float b4 = (float) g3.c.b(i6 * f6); // r9 = i6 (smali)
fArr[0] = 1.0f;
fArr[1] = b4;
if (i6 == 4) {
double d4 = f7;
f4 = 0.5f;
fArr[2] = (float) g3.c.b(d4);
fArr[3] = (float) g3.c.f(d4);
} else {
f4 = 0.5f;
if (i6 > 4) {
iArr[2] = 0;
iArr[3] = 16;
int i7 = i5;
int i8 = 2;
f5 = 1.0f;
while (i7 > 32) {
int i9 = i8 << 1;
int i10 = i8 << 4;
for (int i11 = i8; i11 < i9; i11++) {
int i12 = iArr[i11] << 2;
iArr[i8 + i11] = i12;
iArr[i9 + i11] = i12 + i10;
}
i7 >>= 2;
i8 = i9;
}
fArr[2] = 0.5f / ((float) g3.c.b(f7));
fArr[3] = 0.5f / ((float) g3.c.b(6.0f * f6));
for (int i13 = 4; i13 < i6; i13 += 4) {
float f8 = i13 * f6;
double d5 = f8;
fArr[i13] = (float) g3.c.b(d5);
fArr[i13 + 1] = (float) g3.c.f(d5);
double d6 = 3.0f * f8;
fArr[i13 + 2] = (float) g3.c.b(d6);
fArr[i13 + 3] = -((float) g3.c.f(d6));
}
int i14 = 0;
while (i6 > 2) {
int i15 = i14 + i6;
i6 >>= 1;
fArr[i15] = f5;
fArr[i15 + 1] = b4;
if (i6 == 4) {
float f9 = fArr[i14 + 4];
float f10 = fArr[i14 + 5];
fArr[i15 + 2] = f9;
fArr[i15 + 3] = f10;
} else if (i6 > 4) {
float f11 = fArr[i14 + 4];
float f12 = fArr[i14 + 6];
fArr[i15 + 2] = f4 / f11;
fArr[i15 + 3] = f4 / f12;
for (int i16 = 4; i16 < i6; i16 += 4) {
int i17 = (i16 * 2) + i14;
int i18 = i15 + i16;
float f13 = fArr[i17];
float f14 = fArr[i17 + 1];
float f15 = fArr[i17 + 2];
float f16 = fArr[i17 + 3];
fArr[i18] = f13;
fArr[i18 + 1] = f14;
fArr[i18 + 2] = f15;
fArr[i18 + 3] = f16;
}
}
i14 = i15;
}
}
}
f5 = 1.0f;
int i142 = 0;
while (i6 > 2) {
}
}
int i19 = i4 >> 2;
obj.f11706i = i19;
iArr[1] = i19;
if (i19 > 1) {
int i20 = i19 >> 1;
float f17 = 0.7853982f / i20;
float b5 = (float) g3.c.b(i20 * f17); // r6 = i20 (smali)
fArr[i5] = b5;
fArr[i5 + i20] = b5 * 0.5f;
for (int i21 = 1; i21 < i20; i21++) {
double d7 = i21 * f17;
fArr[i5 + i21] = ((float) g3.c.b(d7)) * 0.5f;
fArr[(i5 + i19) - i21] = ((float) g3.c.f(d7)) * 0.5f;
}
}
} else {
obj.f11708k = 1;
s sVar = new s(((long) g3.c.a((1 << ((int) (((long) (g3.c.d(((float) 1024) + 0.5f) / g3.c.d(2.0d))) / 2))) + 2)) + 2, true);
obj.f11702d = sVar;
k3.d dVar = new k3.d(1024L, true);
obj.f = dVar;
obj.f11705h = 512L;
sVar.c(0L, 512L);
sVar.c(1L, 1L);
if (512 > 2) {
long j4 = 512 >> 1;
float f18 = 0.7853982f / ((float) j4);
float f19 = f18 * 2.0f;
float b6 = (float) g3.c.b(j4 * f18); // r14 = j4 (smali 同模式)
dVar.c(0L, 1.0f);
dVar.c(1L, b6);
if (j4 == 4) {
f = 0.5f;
double d8 = f19;
dVar.c(2L, (float) g3.c.b(d8));
dVar.c(3L, (float) g3.c.f(d8));
} else {
f = 0.5f;
if (j4 > 4) {
sVar.c(2L, 0L);
sVar.c(3L, 16L);
long j5 = 2;
long j6 = 512;
while (j6 > 32) {
long j7 = j5 << 1;
long j8 = j5 << 4;
long j9 = j5;
while (j9 < j7) {
long b7 = sVar.b(j9) << 2;
sVar.c(j5 + j9, b7);
sVar.c(j7 + j9, b7 + j8);
j9++;
f19 = f19;
}
j6 >>= 2;
j5 = j7;
}
dVar.c(2L, 0.5f / ((float) g3.c.b(f19)));
dVar.c(3L, 0.5f / ((float) g3.c.b(6.0f * f18)));
long j10 = 4;
while (j10 < j4) {
float f20 = ((float) j10) * f18;
double d9 = f20;
dVar.c(j10, (float) g3.c.b(d9));
dVar.c(j10 + 1, (float) g3.c.f(d9));
double d10 = 3.0f * f20;
long j11 = j10;
dVar.c(j10 + 2, (float) g3.c.b(d10));
dVar.c(j11 + 3, -((float) g3.c.f(d10)));
j10 = j11 + 4;
}
}
}
long j12 = 0;
long j13 = 2;
while (j4 > j13) {
long j14 = j12 + j4;
j4 >>= 1;
dVar.c(j14, 1.0f);
dVar.c(j14 + 1, b6);
if (j4 == 4) {
float b8 = dVar.b(j12 + 4);
float b9 = dVar.b(j12 + 5);
j13 = 2;
dVar.c(j14 + 2, b8);
dVar.c(j14 + 3, b9);
} else {
j13 = 2;
if (j4 > 4) {
float b10 = dVar.b(j12 + 4);
float b11 = dVar.b(6 + j12);
dVar.c(j14 + 2, f / b10);
dVar.c(j14 + 3, f / b11);
long j15 = 4;
while (j15 < j4) {
long j16 = (j15 * 2) + j12;
float f21 = b6;
long j17 = j14 + j15;
long j18 = j12;
float b12 = dVar.b(j16);
long j19 = j14;
float b13 = dVar.b(j16 + 1);
float b14 = dVar.b(j16 + 2);
float b15 = dVar.b(j16 + 3);
dVar.c(j17, b12);
dVar.c(j17 + 1, b13);
dVar.c(j17 + 2, b14);
dVar.c(j17 + 3, b15);
j15 += 4;
b6 = f21;
j12 = j18;
j14 = j19;
}
}
}
b6 = b6;
j12 = j14;
}
}
long j20 = 256;
obj.f11707j = 256L;
long j21 = 1;
sVar.c(1L, 256L);
if (256 > 1) {
long j22 = 256 >> 1;
float f22 = 0.7853982f / ((float) j22);
long j23 = 512;
dVar.c(512L, (float) g3.c.b(j22 * f22)); // r1 = j22 (smali)
dVar.c(512 + j22, dVar.b(512L) * 0.5f);
long j24 = 1;
while (j24 < j22) {
long j25 = j20;
long j26 = j21;
double d11 = ((float) j24) * f22;
long j27 = j23;
dVar.c(j23 + j24, ((float) g3.c.b(d11)) * 0.5f);
dVar.c((j27 + j25) - j24, ((float) g3.c.f(d11)) * 0.5f);
j24 += j26;
j20 = j25;
j21 = j26;
j23 = j27;
}
}
}
f605D = obj;
}
public b(Context context) {
float[] fArr = new float[1024];
fArr[512] = 1.0f;
for (int i4 = 1; i4 < 512; i4++) {
double d4 = (i4 * 6.283185307179586d) / 1024;
float cos = (float) ((Math.cos(d4 * 2.0d) * 0.08d) + (0.42d - (Math.cos(d4) * 0.5d)));
fArr[i4] = cos;
double d5 = 0.003125f * 6.283185307179586d * (i4 - 512);
float sin = (float) ((Math.sin(d5) / d5) * cos);
fArr[i4] = sin;
fArr[1024 - i4] = sin;
}
this.f607a = fArr;
this.f608b = new float[1024];
this.c = new float[1024];
this.f609d = new float[f603B];
this.f610e = nativedecoder.TNativeDecoder.Create();
this.f614j = "";
this.f615k = new c(4000, 0);
I2.b bVar = new I2.b();
this.f617m = bVar;
this.f618n = new Handler(Looper.getMainLooper());
this.f622r = new AtomicInteger();
this.f623s = new AtomicBoolean(false);
this.f625u = 0;
this.f626v = new AtomicBoolean(false);
this.f628x = 0;
this.f624t = context;
bVar.b(context);
}
/* JADX WARN: Removed duplicated region for block: B:57:0x01e2 */
/* JADX WARN: Removed duplicated region for block: B:63:0x020a A[LOOP:6: B:62:0x0208->B:63:0x020a, LOOP_END] */
/* JADX WARN: Removed duplicated region for block: B:66:0x0216 A[LOOP:7: B:65:0x0214->B:66:0x0216, LOOP_END] */
/* JADX WARN: Removed duplicated region for block: B:70:0x0232 */
/* JADX WARN: Removed duplicated region for block: B:73:? A[RETURN, SYNTHETIC] */
/*
Code decompiled incorrectly, please refer to instructions dump.
*/
public final void a(float[] fArr) {
c cVar;
float[] fArr2;
float[] fArr3;
b bVar;
Exception exc;
int i4;
float f;
int i5;
WaterfallSurfaceView waterfallSurfaceView;
int length = fArr.length;
int i6 = 0;
while (true) {
cVar = this.f615k;
if (i6 >= length) {
break;
}
cVar.c(Math.abs(fArr[i6]));
i6++;
}
this.f616l = Math.round(((float) Math.log10(Math.max(-0.5d, cVar.f633e))) * 20.0f);
int i7 = f604C;
int i8 = 1024 - i7;
float[] fArr4 = this.f608b;
System.arraycopy(fArr4, i7, fArr4, 0, i8);
System.arraycopy(fArr, 0, fArr4, i8, i7);
int i9 = 0;
while (true) {
fArr2 = this.c;
if (i9 >= 1024) {
break;
}
fArr2[i9] = fArr4[i9] * this.f607a[i9];
i9++;
}
f605D.k(fArr2, 0);
int i10 = f606z;
int i11 = i10;
while (true) {
int i12 = f602A;
float f4 = 0.0f;
fArr3 = this.f609d;
if (i11 < i12) {
int i13 = i11 * 2;
float f5 = fArr2[i13];
float f6 = fArr2[i13 + 1];
float f7 = ((f6 * f6) + (f5 * f5)) * 0.001f;
if (!Float.isNaN(f7)) {
f4 = f7;
}
fArr3[i11 - i10] = f4;
i11++;
} else {
// smali: 循环内无 try; 录音写入 try 在循环后(见下)
break;
}
}
DataOutputStream dataOutputStream = this.f627w;
Context context = this.f624t;
AtomicBoolean atomicBoolean = this.f626v;
try { // smali try_start_0: 录音写入整块(原 jadx 误把 try 放 break 处)
if (dataOutputStream == null && atomicBoolean.get()) {
File file = new File(context.getExternalFilesDir(null), ((String) DateFormat.format("yyyy-MM-dd_HH_mm_ss", new Date())) + "_spectra.bin");
this.f629y = file.getAbsoluteFile().toString();
this.f627w = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file, false)));
this.f628x = 0;
this.f614j = " Recording...";
}
if (this.f627w != null) {
for (float f8 : fArr3) {
this.f627w.writeFloat(f8);
}
}
if (atomicBoolean.get()) {
int i14 = this.f628x + 1;
this.f628x = i14;
if (i14 >= 2048) {
atomicBoolean.set(false);
this.f627w.close();
this.f627w = null;
this.f614j = "";
MediaScannerConnection.scanFile(context, new String[]{this.f629y}, new String[]{"application/x-binary"}, null);
}
}
} catch (IOException e4) {
Log.e("File write", "Exception: ", e4);
}
nativedecoder.TNativeDecoder tNativeDecoder = this.f610e;
try {
this.f613i = tNativeDecoder.ProcessSpectrum(fArr3);
int i15 = this.f625u + 1;
this.f625u = i15;
Handler handler = this.f618n;
if (i15 == 15) {
try {
this.f625u = 0;
this.f = tNativeDecoder.getPitch();
this.f611g = tNativeDecoder.getSnr();
this.f612h = tNativeDecoder.getWpm();
handler.post(new B0.b(this, this.f614j, new int[]{this.f616l, this.f, this.f611g, this.f612h, tNativeDecoder.getSignalCount(), this.f613i.Value}));
AtomicInteger atomicInteger = this.f622r;
int i16 = atomicInteger.get();
if (i16 != 0) {
tNativeDecoder.LockToPitch(i16);
atomicInteger.set(0);
}
tNativeDecoder.setHamMode(this.f623s.get());
} catch (Exception e5) {
// jadx 幻觉区(空循环/空 if 为 catch 错位还原)已裁剪; 原 smali: 记录异常后跳回正常流程尾部
exc = e5;
bVar = this;
Log.e("Audio Processor", "Exception", exc);
}
}
int charCount = tNativeDecoder.getCharCount();
if (charCount > 0) {
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
int i18 = 0;
while (i18 < charCount) {
decoder.TCharInfo GetChar = tNativeDecoder.GetChar(i18);
spannableStringBuilder.append(GetChar.getChar());
ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(this.f617m.a(GetChar));
int i19 = i18 + 1;
spannableStringBuilder.setSpan(foregroundColorSpan, i18, i19, 33);
i18 = i19;
}
bVar = this;
try {
handler.post(new RunnableC0001b(bVar, spannableStringBuilder, tNativeDecoder.getDeleteCount(), 2, false));
} catch (Exception e6) {
// jadx 幻觉区已裁剪(同 catch e5); 原 smali: 记录异常后跳回正常流程尾部
exc = e6;
Log.e("Audio Processor", "Exception", exc);
}
} else {
bVar = this;
}
} catch (Exception e7) {
// jadx 幻觉(e 变量)已裁剪
bVar = this;
}
i4 = f603B;
int i1722 = i4 / 2;
float[] copyOfRange22 = Arrays.copyOfRange(fArr3, i1722 - 20, i1722 + 20);
Arrays.sort(copyOfRange22);
f = copyOfRange22[20];
if (f > 0.0f) {
for (int i20 = 0; i20 < i4; i20++) {
fArr3[i20] = fArr3[i20] / f;
}
}
float[] fArr522 = (float[]) fArr3.clone();
new c(12, 1).a(fArr522);
new c(12, 0).a(fArr522);
for (i5 = 0; i5 < i4; i5++) {
fArr3[i5] = fArr3[i5] - fArr522[i5];
}
for (int i21 = 0; i21 < i4; i21++) {
fArr3[i21] = ((float) Math.log10(Math.max(-0.5d, fArr3[i21]) + 1.0d)) * 10.0f;
}
waterfallSurfaceView = bVar.f619o;
if (waterfallSurfaceView != null) {
waterfallSurfaceView.queueEvent(new n(waterfallSurfaceView, 1, (float[]) fArr3.clone()));
}
}
}
-89
View File
@@ -1,89 +0,0 @@
package H2;
/* loaded from: classes.dex */
public final class c {
/* renamed from: a, reason: collision with root package name */
public final int f630a;
/* renamed from: b, reason: collision with root package name */
public float[] f631b;
public int c;
/* renamed from: d, reason: collision with root package name */
public float f632d;
/* renamed from: e, reason: collision with root package name */
public float f633e;
public final /* synthetic */ int f;
public c(int i4, int i5) {
this.f = i5;
this.f630a = i4 - 1;
}
public final void a(float[] fArr) {
int i4 = this.f630a / 2;
int length = fArr.length;
if (length > i4) {
this.f631b = null;
for (int i5 = 0; i5 < i4; i5++) {
c(fArr[i5]);
}
for (int i6 = i4; i6 < length; i6++) {
fArr[i6 - i4] = c(fArr[i6]);
}
for (int i7 = length; i7 < length + i4; i7++) {
fArr[i7 - i4] = c(fArr[length - 1]);
}
return;
}
throw new IllegalArgumentException("Sliding Filter error: input too short");
}
public final float b(float f, float f4) {
switch (this.f) {
case 0:
return Math.max(f, f4);
default:
return Math.min(f, f4);
}
}
public final float c(float f) {
float[] fArr = this.f631b;
int i4 = this.f630a;
if (fArr == null) {
this.f631b = new float[i4];
for (int i5 = 0; i5 < i4; i5++) {
this.f631b[i5] = f;
}
this.f632d = f;
return f;
}
float b4 = b(this.f632d, f);
this.f632d = b4;
this.f633e = b(this.f631b[this.c], b4);
float[] fArr2 = this.f631b;
int i6 = this.c;
int i7 = i6 + 1;
this.c = i7;
fArr2[i6] = f;
if (i7 == i4) {
int length = fArr2.length;
while (true) {
this.c = length - 1;
int i8 = this.c;
if (i8 <= 0) {
break;
}
float[] fArr3 = this.f631b;
fArr3[i8 - 1] = b(fArr3[i8 - 1], fArr3[i8]);
length = this.c;
}
this.c = 0;
this.f632d = f;
}
return this.f633e;
}
}
-42
View File
@@ -1,42 +0,0 @@
package I0;
/**
* 照搬自 Morse Expert 1.15 I0.d, 类名保持混淆原名。
* R8 合并类手术: 只保留 (i3.d, int, int, float[]) FFT 任务构造;
* run() 按 smali 原样还原: R8 已将任务体优化为死代码(aget 后 throw null,
* 永不执行), 保留原语义(行为与原 APK 完全一致)。
* SystemForegroundService(work 库)case 已裁剪。
*/
public final class d implements Runnable {
public final /* synthetic */ int f = 0;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ int f653g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ int f654h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ Object f655i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ Object f656j;
public d(i3.d dVar, int i4, int i5, float[] fArr) {
this.f656j = dVar;
this.f653g = i4;
this.f654h = i5;
this.f655i = fArr;
}
@Override // java.lang.Runnable
public final void run() {
int i6 = this.f654h;
int i7 = this.f653g;
if (i7 >= i6) {
return;
}
float f = ((float[]) this.f655i)[i7 * 2];
// R8 原代码: aget 后 throw null(死代码, 永不执行) —— 照搬语义, 不执行任何计算
}
}
-73
View File
@@ -1,73 +0,0 @@
package I2;
import android.content.Context;
import android.content.SharedPreferences;
import pas.decoder;
/* loaded from: classes.dex */
public final class b {
/* renamed from: b, reason: collision with root package name */
public static final String[] f663b = {"bg_color", "text_color", "text_color_weak", "call_color", "call_color_weak", "cq_color", "cq_color_weak", "rst_color", "rst_color_weak"};
public static final String[] c = {"Background color", "Text color", "Text color, weak", "Callsign color", "Callsign color, weak", "CQ color", "CQ color, weak", "RST color", "RST color, weak"};
/* renamed from: d, reason: collision with root package name */
public static final int[] f664d = {-3084048, -16777216, -5592406, -65536, -30584, -16776961, -7829249, -65281, -30465};
/* renamed from: a, reason: collision with root package name */
public final int[] f665a = (int[]) f664d.clone();
public final int a(decoder.TCharInfo tCharInfo) {
boolean lowSnr = tCharInfo.getLowSnr();
int[] iArr = this.f665a;
if (lowSnr) {
int i4 = tCharInfo.getWordType().Value;
if (i4 != 0) {
if (i4 != 1) {
if (i4 != 2) {
if (i4 != 3) {
if (i4 != 4) {
if (i4 == 5) {
return iArr[4];
}
return -16777216;
}
return iArr[2];
}
return iArr[6];
}
return iArr[8];
}
return iArr[2];
}
return iArr[2];
}
int i5 = tCharInfo.getWordType().Value;
if (i5 != 0) {
if (i5 != 1) {
if (i5 != 2) {
if (i5 != 3) {
if (i5 != 4) {
if (i5 != 5) {
return -16777216;
}
return iArr[3];
}
return iArr[1];
}
return iArr[5];
}
return iArr[7];
}
return iArr[1];
}
return iArr[1];
}
public final void b(Context context) {
SharedPreferences sharedPreferences = context.getSharedPreferences(context.getPackageName() + "_preferences", 0); // 原 c0.x.a(context)
for (int i4 = 0; i4 < 9; i4++) {
this.f665a[i4] = sharedPreferences.getInt(f663b[i4], f664d[i4]);
}
}
}
-45
View File
@@ -1,45 +0,0 @@
package J2;
/* loaded from: classes.dex */
public final class a {
/* renamed from: b, reason: collision with root package name */
public static final int[] f726b = {16, 17749, 65535, 16776960, 16711680};
/* renamed from: a, reason: collision with root package name */
public final int[] f727a;
public a() {
int[] iArr = new int[256];
this.f727a = iArr;
int i4 = 4;
float f = 255.0f / 4;
int[] iArr2 = f726b;
int i5 = 0;
int i6 = -16777216;
iArr[0] = iArr2[0] | (-16777216);
int i7 = 1;
while (i7 <= i4) {
int round = Math.round(i7 * f);
iArr[round] = iArr2[i7] | i6;
int i8 = round - i5;
int i9 = iArr[i5];
int i10 = (i9 >> 16) & 255;
int i11 = (i9 >> 8) & 255;
int i12 = i9 & 255;
float f4 = i8;
// r10 = iArr2[i7] (下一个锚点色, smali: aget v10, v4, v8)
float f5 = (((iArr2[i7] >> 16) & 255) - i10) / f4;
float f6 = (((iArr2[i7] >> 8) & 255) - i11) / f4;
float f7 = ((iArr2[i7] & 255) - i12) / f4;
for (int i13 = 1; i13 < i8; i13++) {
float f8 = i13;
iArr[i5 + i13] = ((Math.round(f5 * f8) + i10) << 16) | (-16777216) | ((Math.round(f6 * f8) + i11) << 8) | (Math.round(f8 * f7) + i12);
}
i7++;
i5 = round;
i4 = 4;
i6 = -16777216;
}
}
}
-183
View File
@@ -1,183 +0,0 @@
package J2;
import android.content.Context;
import android.graphics.Bitmap;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.GLUtils;
import android.os.SystemClock;
import android.util.Log;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
/* loaded from: classes.dex */
public final class b implements GLSurfaceView.Renderer {
/* renamed from: b, reason: collision with root package name */
public FloatBuffer f729b;
public int c;
/* renamed from: d, reason: collision with root package name */
public final Bitmap f730d;
public final Context f;
/* renamed from: g, reason: collision with root package name */
public double f732g;
/* renamed from: h, reason: collision with root package name */
public final Bitmap f733h;
/* renamed from: i, reason: collision with root package name */
public final a f734i;
/* renamed from: j, reason: collision with root package name */
public long f735j;
/* renamed from: k, reason: collision with root package name */
public double f736k;
/* renamed from: l, reason: collision with root package name */
public int f737l;
/* renamed from: a, reason: collision with root package name */
public final int[] f728a = new int[1];
/* renamed from: e, reason: collision with root package name */
public final float[] f731e = {-1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f};
public b(Context context, int i4) {
Bitmap.Config config = Bitmap.Config.ARGB_8888;
Bitmap createBitmap = Bitmap.createBitmap(2048, i4, config);
this.f730d = createBitmap;
createBitmap.eraseColor(16);
this.f732g = 0.09375d;
this.f734i = new a();
this.f735j = 0L;
this.f736k = 0.0d;
this.f737l = -1;
this.f = context;
this.f733h = Bitmap.createBitmap(1, i4, config);
}
public static void a() {
int glGetError = GLES20.glGetError();
if (glGetError != 0) {
StackTraceElement stackTraceElement = Thread.currentThread().getStackTrace()[3];
Log.e(stackTraceElement.getFileName() + ":" + stackTraceElement.getLineNumber(), ", GLES20 error: " + glGetError);
}
}
public final double b() {
long uptimeMillis = SystemClock.uptimeMillis();
double width = (((uptimeMillis - this.f735j) * this.f732g) + this.f736k) % this.f730d.getWidth();
this.f735j = uptimeMillis;
this.f736k = width;
return width;
}
public final void c(GL10 gl10) {
GLES20.glUseProgram(this.c);
a();
GLES20.glActiveTexture(33984);
a();
GLES20.glBindTexture(3553, this.f728a[0]);
a();
GLES20.glUniform1i(GLES20.glGetUniformLocation(this.c, "uTexture"), 0);
a();
int glGetAttribLocation = GLES20.glGetAttribLocation(this.c, "aPosition");
a();
int glGetAttribLocation2 = GLES20.glGetAttribLocation(this.c, "aTexPos");
a();
this.f729b.position(0);
a();
GLES20.glVertexAttribPointer(glGetAttribLocation, 2, 5126, false, 16, (Buffer) this.f729b);
a();
GLES20.glEnableVertexAttribArray(glGetAttribLocation);
a();
this.f729b.position(2);
a();
GLES20.glVertexAttribPointer(glGetAttribLocation2, 2, 5126, false, 16, (Buffer) this.f729b);
a();
GLES20.glEnableVertexAttribArray(glGetAttribLocation2);
a();
GLES20.glDrawArrays(5, 0, 4);
a();
}
public final void d(GL10 gl10, int i4, int i5) {
GLES20.glViewport(0, 0, i4, i5);
a();
}
@Override // android.opengl.GLSurfaceView.Renderer
public final void onDrawFrame(GL10 gl10) {
float b4 = (float) (b() / this.f730d.getWidth());
float f = 0.5f + b4;
float[] fArr = this.f731e;
fArr[6] = f;
fArr[2] = f;
float f4 = b4 + 1.0f;
fArr[14] = f4;
fArr[10] = f4;
this.f729b.position(0);
this.f729b.put(this.f731e);
c(gl10);
}
@Override // android.opengl.GLSurfaceView.Renderer
public final void onSurfaceChanged(GL10 gl10, int i4, int i5) {
d(gl10, i4, i5);
float width = 1.0f - (((this.f730d.getWidth() * 0.7f) / (i4 / this.f.getResources().getDisplayMetrics().density)) * 2.0f);
float[] fArr = this.f731e;
fArr[4] = width;
fArr[0] = width;
this.f729b.position(0);
this.f729b.put(this.f731e);
}
@Override // android.opengl.GLSurfaceView.Renderer
public final void onSurfaceCreated(GL10 gl10, EGLConfig eGLConfig) {
int glCreateShader = GLES20.glCreateShader(35633);
a();
GLES20.glShaderSource(glCreateShader, "attribute vec2 aPosition;\nattribute vec2 aTexPos;\nvarying vec2 vTexPos;\nvoid main() {\n vTexPos = aTexPos;\n gl_Position = vec4(aPosition.xy, 0.0, 1.0);\n}");
a();
GLES20.glCompileShader(glCreateShader);
a();
int glCreateShader2 = GLES20.glCreateShader(35632);
a();
GLES20.glShaderSource(glCreateShader2, "precision mediump float;\nuniform sampler2D uTexture;\nvarying vec2 vTexPos;\nvoid main(void)\n{\n gl_FragColor = texture2D(uTexture, vTexPos);\n}");
a();
GLES20.glCompileShader(glCreateShader2);
a();
this.c = GLES20.glCreateProgram();
a();
GLES20.glAttachShader(this.c, glCreateShader);
a();
GLES20.glAttachShader(this.c, glCreateShader2);
a();
GLES20.glLinkProgram(this.c);
a();
int[] iArr = this.f728a;
GLES20.glGenTextures(1, iArr, 0);
a();
GLES20.glBindTexture(3553, iArr[0]);
a();
GLES20.glTexParameteri(3553, 10240, 9729);
GLES20.glTexParameteri(3553, 10242, 10497);
GLES20.glTexParameteri(3553, 10241, 9729);
GLES20.glTexParameteri(3553, 10243, 10497);
a();
GLUtils.texImage2D(3553, 0, this.f730d, 0);
a();
float[] fArr = this.f731e;
ByteBuffer allocateDirect = ByteBuffer.allocateDirect(fArr.length * 4);
allocateDirect.order(ByteOrder.nativeOrder());
FloatBuffer asFloatBuffer = allocateDirect.asFloatBuffer();
this.f729b = asFloatBuffer;
asFloatBuffer.put(fArr);
}
}
-10
View File
@@ -1,10 +0,0 @@
package K1;
/**
* 照搬自 Morse Expert 1.15 K1.a 精简版: 仅保留 k3 FFT 库引用的内存统计静态字段
* (K1.a.c 长整型记账)。原 K1.a(828 行)的其余方法为状态栏/库代码, 已裁剪
* (状态栏逻辑已在 MainActivity 内联)。
*/
public class a {
public static long c = 0;
}
@@ -1,213 +0,0 @@
/*
* CwDecodeScreen.kt — Look4Sat 的 CW 解码页面(Compose 外壳)。
*
* 照搬自 Morse Expert 1.15 的 com.ve3nea.morse_expert.MainActivity(已改为普通控制器类):
* - AndroidView 嵌入原版 activity_main.xml(ConstraintLayout 根, 含 decodedTextView/scaleView/
* statusTextView/verticalLayout/waterfallView);
* - 生命周期 onCreate/onResume/onPause/onDestroy 全部委托给控制器, 顺序与原 Activity 一致
* (onCreate 必须在 onResume 前, onDispose 先 onPause 再 onDestroy);
* - 麦克风权限(RECORD_AUDIO)由 Compose 侧管理, 授予后调用 onPermissionGranted()(= v()),
* 用 initialized 标记保证只调一次(v() 会 new a() 覆盖 f11036E);
* - 原 options_menu 的 清除/暂停/保存/录音/设置 改为顶部一行按钮(图标用模块内原版 drawable)。
*
* 注意: 控制器 onCreate 内的 setImmersive(window, false) 会把宿主窗口恢复为
* decorFitsSystemWindows(true)(原版行为照搬, 会对整个宿主 Activity 生效)。
*/
package com.rtbishop.look4sat.feature.cw
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
import android.view.LayoutInflater
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.constraintlayout.widget.ConstraintLayout
import com.ve3nea.morse_expert.MainActivity
@Composable
fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
val context = LocalContext.current
val activity = remember(context) {
context as? Activity ?: error("CwDecodeScreen must be hosted in an Activity")
}
// 照搬的控制器(普通类, 非 Activity); 每次进入页面新建实例
val controller = remember { MainActivity() }
// 提前 inflate 原版布局, 供 AndroidView 与控制器 onCreate 共用同一根视图
val rootView = remember(context) {
LayoutInflater.from(context).inflate(R.layout.activity_main, null) as ConstraintLayout
}
var permissionGranted by remember {
mutableStateOf(
context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) ==
PackageManager.PERMISSION_GRANTED
)
}
var permanentlyDenied by remember { mutableStateOf(false) }
// v() 会 new a() 覆盖 f11036E, 必须保证只初始化一次
var initialized by remember { mutableStateOf(false) }
var showSettings by remember { mutableStateOf(false) }
// 生命周期: onCreate 必须在 onResume 前; onDispose 先 onPause 再 onDestroy
DisposableEffect(Unit) {
controller.onCreate(activity, rootView)
controller.onResume()
onDispose {
controller.onPause()
controller.onDestroy()
}
}
// 权限授予后启动解码核心; "首次进入已授权" 与 "弹窗回调授权" 两条路径统一走这里
LaunchedEffect(permissionGranted) {
if (permissionGranted && !initialized) {
controller.onPermissionGranted() // = v(): 创建音频采集 + 解码核心
// v() 只创建核心; 页面此刻已处于 resumed, 需再走一次 onResume 立即启动录音
// (AudioRecord 是新建的, startRecording 不会重复; GLSurfaceView.onResume 幂等)
controller.onResume()
initialized = true
}
}
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
permissionGranted = granted
permanentlyDenied = !granted &&
!activity.shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)
}
// 原版 tap_back_again_to_close: 单击返回只提示, 2 秒内再按才退出
BackHandler {
if (!controller.handleBackPress()) navigateUp()
}
if (showSettings) {
CwSettingsDialog(controller = controller, onDismiss = { showSettings = false })
}
Column(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
) {
// 原 options_menu: 暂停/清除/保存(原版图标) + 录音/设置(文字按钮)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly
) {
IconButton(onClick = { controller.togglePause() }) {
Icon(
painter = painterResource(R.drawable.ic_baseline_pause_24),
contentDescription = stringResource(R.string.pause)
)
}
IconButton(onClick = { controller.clearDecoded() }) {
Icon(
painter = painterResource(R.drawable.ic_baseline_delete_24),
contentDescription = stringResource(R.string.clear)
)
}
IconButton(onClick = { controller.saveText() }) {
Icon(
painter = painterResource(R.drawable.ic_baseline_save_24),
contentDescription = stringResource(R.string.save_text)
)
}
// recordSignals 需要解码核心(f11037F), 未初始化时禁用
TextButton(
onClick = { if (initialized) controller.recordSignals() },
enabled = initialized
) {
Text(stringResource(R.string.record_signals))
}
TextButton(onClick = { showSettings = true }) {
Text(stringResource(R.string.settings))
}
}
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
AndroidView(
factory = { rootView },
modifier = Modifier.fillMaxSize()
)
if (!permissionGranted) {
// 未授予麦克风权限: 遮罩 + 提示文字 + 请求按钮
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0x99000000)),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "Microphone permission is required for CW decoding",
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp)
)
Spacer(modifier = Modifier.height(12.dp))
Button(onClick = {
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}) {
Text("Grant permission")
}
if (permanentlyDenied) {
// 勾选了"不再询问": 引导去系统设置开启
TextButton(onClick = {
activity.startActivity(
Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.parse("package:${activity.packageName}")
)
)
}) {
Text("Open app settings")
}
}
}
}
}
}
}
}
@@ -1,218 +0,0 @@
/*
* 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.
*/
package com.rtbishop.look4sat.feature.cw
import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.ve3nea.morse_expert.MainActivity
import kotlin.math.roundToInt
/** 键名/默认值照搬原 Morse Expert(root_preferences.xml + SettingsActivity 逻辑)。 */
private const val KEY_MESSAGE_TYPE = "message_type"
private const val KEY_TEXT_FONT_SIZE = "text_font_size"
private const val DEFAULT_MESSAGE_TYPE = "general_text"
private const val DEFAULT_TEXT_FONT_SIZE = 18
private const val MIN_FONT_SIZE = 7
private const val MAX_FONT_SIZE = 99
/** 预置色板(9 色, 3x3 网格);原 ColorPreferenceCompat 用第三方 colorpicker, 不引入。 */
private val PaletteColors = listOf(
0xFFD0F0F0.toInt(), 0xFF000000.toInt(), 0xFFFFFFFF.toInt(),
0xFFAAAAAA.toInt(), 0xFFFF0000.toInt(), 0xFF00FF00.toInt(),
0xFF0000FF.toInt(), 0xFFFFFF00.toInt(), 0xFFFF00FF.toInt(),
)
/**
* CW 设置弹窗(替代原 Morse Expert 的 SettingsActivity)。
* 设置项与键名照搬原 APK: message_type / text_font_size / 9 个颜色
* (I2.b.f663b 键名, I2.b.f664d 默认值, I2.b.c 英文标题)。
* 存储: SharedPreferences(getPackageName() + "_preferences")。
* 点击 OK 保存并调用 controller.onResume() 立即生效; Cancel/点外部仅关闭不保存。
*/
@Composable
fun CwSettingsDialog(controller: MainActivity, onDismiss: () -> Unit) {
val activity = controller.mActivity ?: return
val prefs = remember(activity) {
activity.getSharedPreferences(activity.packageName + "_preferences", Context.MODE_PRIVATE)
}
var messageType by remember {
mutableStateOf(prefs.getString(KEY_MESSAGE_TYPE, DEFAULT_MESSAGE_TYPE) ?: DEFAULT_MESSAGE_TYPE)
}
var fontSize by remember {
mutableStateOf(
prefs.getInt(KEY_TEXT_FONT_SIZE, DEFAULT_TEXT_FONT_SIZE)
.toFloat().coerceIn(MIN_FONT_SIZE.toFloat(), MAX_FONT_SIZE.toFloat())
)
}
var colorValues by remember {
mutableStateOf(IntArray(9) { i -> prefs.getInt(I2.b.f663b[i], I2.b.f664d[i]) })
}
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("CW Settings") },
text = {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(top = 4.dp)
) {
Text("Message type", style = MaterialTheme.typography.titleSmall)
MessageTypeRow("general_text", "General Text", messageType) { messageType = it }
MessageTypeRow("ham_radio_qso", "Ham Radio QSO", messageType) { messageType = it }
HorizontalDivider(Modifier.padding(vertical = 8.dp))
Text("Text font size", style = MaterialTheme.typography.titleSmall)
Row(verticalAlignment = Alignment.CenterVertically) {
Slider(
value = fontSize,
onValueChange = { fontSize = it },
valueRange = MIN_FONT_SIZE.toFloat()..MAX_FONT_SIZE.toFloat(),
steps = MAX_FONT_SIZE - MIN_FONT_SIZE - 1,
modifier = Modifier.weight(1f),
)
Text(
text = "${fontSize.roundToInt()}",
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.End,
modifier = Modifier.width(44.dp),
)
}
HorizontalDivider(Modifier.padding(vertical = 8.dp))
Text("Colors", style = MaterialTheme.typography.titleSmall)
for (i in 0 until 9) {
ColorSettingRow(
title = I2.b.c[i],
value = colorValues[i],
onSelect = { selected ->
colorValues = colorValues.copyOf().also { it[i] = selected }
},
)
}
}
},
confirmButton = {
TextButton(onClick = {
val editor = prefs.edit()
editor.putString(KEY_MESSAGE_TYPE, messageType)
editor.putInt(KEY_TEXT_FONT_SIZE, fontSize.roundToInt())
for (i in 0 until 9) {
editor.putInt(I2.b.f663b[i], colorValues[i])
}
editor.apply()
// 原 SettingsActivity 返回时由 MainActivity.onResume() 重新读取全部设置; 照搬逻辑已就位
controller.onResume()
onDismiss()
}) { Text("OK") }
},
dismissButton = {
TextButton(onClick = onDismiss) { Text("Cancel") }
},
)
}
@Composable
private fun MessageTypeRow(option: String, label: String, selected: String, onSelect: (String) -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(6.dp))
.clickable { onSelect(option) }
.padding(vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(selected = option == selected, onClick = { onSelect(option) })
Text(label, style = MaterialTheme.typography.bodyMedium)
}
}
@Composable
private fun ColorSettingRow(title: String, value: Int, onSelect: (Int) -> Unit) {
Column(Modifier.fillMaxWidth().padding(vertical = 6.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(title, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
Box(
Modifier
.size(20.dp)
.clip(RoundedCornerShape(4.dp))
.background(Color(value)),
)
Spacer(Modifier.width(6.dp))
Text(
text = String.format("#%06X", value and 0xFFFFFF),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Spacer(Modifier.height(6.dp))
PaletteGrid(selected = value, onSelect = onSelect)
}
}
@Composable
private fun PaletteGrid(selected: Int, onSelect: (Int) -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
PaletteColors.chunked(3).forEach { rowColors ->
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
rowColors.forEach { color ->
val isSelected = color == selected
Box(
modifier = Modifier
.size(36.dp)
.border(
width = if (isSelected) 3.dp else 1.dp,
color = if (isSelected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.outlineVariant,
shape = RoundedCornerShape(6.dp),
)
.clip(RoundedCornerShape(6.dp))
.background(Color(color))
.clickable { onSelect(color) },
)
}
}
}
}
}
@@ -1,14 +0,0 @@
package com.rtbishop.look4sat.feature.cw.suncompat;
/**
* 编译期 stub:Android 无 sun.misc.Cleaner(照搬 Morse Expert 1.15 k3 FFT 库的清理回调,
* 实际 FFT 1024 点不触发 native 路径)。
*/
public class Cleaner {
public static Cleaner create(Object referent, Runnable thunk) {
return null;
}
public void clean() {
}
}
@@ -1,83 +0,0 @@
package com.rtbishop.look4sat.feature.cw.suncompat;
/**
* 编译期 stub:Android 无 sun.misc.Unsafe(运行时通过反射获取,失败时 k3.r.f12109a 为 null)。
* 照搬自 Morse Expert 1.15(k3 FFT 库的大数组 native 路径 >1GB 不触发,实际 FFT 走 float[] 分支)。
*/
public class Unsafe {
public long allocateMemory(long bytes) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void freeMemory(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public byte getByte(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putByte(long address, byte value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public short getShort(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putShort(long address, short value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public int getInt(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putInt(long address, int value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public long getLong(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putLong(long address, long value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public float getFloat(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putFloat(long address, float value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public double getDouble(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putDouble(long address, double value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void setMemory(long address, long bytes, byte value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void copyMemory(long src, long dst, long len) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public Object getObject(Object o, long offset) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putObject(Object o, long offset, Object value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public long objectFieldOffset(java.lang.reflect.Field field) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
}
@@ -1,138 +0,0 @@
package com.ve3nea.morse_expert;
import F2.a;
import android.content.Context;
import android.content.Intent;
import android.media.MediaScannerConnection;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ActionMode;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import java.io.PrintWriter;
import java.util.Date;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 照搬自 Morse Expert 1.15 DecodedTextView, 类名保持混淆原名。
* 适配: 原 extends l.C1702e0(appcompat 混淆类) -> 直接 extends TextView
* (C1702e0 仅提供 TextView 兼容功能, 本类未使用其特有方法)。
*/
public class DecodedTextView extends TextView {
/* renamed from: m, reason: collision with root package name */
public final Context f11031m;
/* renamed from: n, reason: collision with root package name */
public boolean f11032n;
/* renamed from: o, reason: collision with root package name */
public final AtomicBoolean f11033o;
public DecodedTextView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
this.f11032n = false;
this.f11033o = new AtomicBoolean(false);
this.f11031m = context;
setCustomSelectionActionModeCallback(getActionModeCallback());
}
private ActionMode.Callback getActionModeCallback() {
return new a(this);
}
private String getDecodedText() {
int selectionStart = getSelectionStart();
int selectionEnd = getSelectionEnd();
int max = Math.max(0, Math.min(selectionStart, selectionEnd));
int max2 = Math.max(0, Math.max(selectionStart, selectionEnd));
if (max2 > max) {
return getText().subSequence(max, max2).toString();
}
return getText().toString();
}
@Override // android.widget.TextView
public final void onSelectionChanged(int i4, int i5) {
super.onSelectionChanged(i4, i5);
if (hasSelection()) {
this.f11032n = true;
}
}
public final void q(SpannableStringBuilder spannableStringBuilder) {
boolean z3;
try {
CharSequence text = getText();
Layout layout = getLayout();
int lineCount = getLineCount();
int length = text.length();
int lineHeight = getLineHeight();
int scrollY = getScrollY();
if (layout.getLineBottom(lineCount - 1) == getHeight() + scrollY) {
z3 = true;
} else {
z3 = false;
}
getSelectionStart();
getSelectionEnd();
hasSelection();
if (z3) {
Selection.setSelection((Spannable) text, length, length);
}
append(spannableStringBuilder);
int i4 = lineCount - 50;
if (i4 > 0) {
int i5 = lineHeight * i4;
getEditableText().delete(0, layout.getLineEnd(lineCount - 51));
if (scrollY >= i5) {
scrollBy(0, -i5);
}
}
} catch (Exception e4) {
Log.e("printChars()", "Exception: ", e4);
}
}
public final void r() {
String decodedText = getDecodedText();
boolean isEmpty = TextUtils.isEmpty(decodedText);
Context context = this.f11031m;
if (isEmpty) {
Toast.makeText(context, "No text to save", 0).show();
return;
}
try {
File file = new File(getContext().getExternalFilesDir(null), ((String) DateFormat.format("yyyy-MM-dd_HH_mm_ss", new Date())) + ".txt");
String absolutePath = file.getAbsolutePath();
PrintWriter printWriter = new PrintWriter(file);
try {
printWriter.println(decodedText);
printWriter.close();
MediaScannerConnection.scanFile(context, new String[]{absolutePath}, new String[]{"text/plain"}, null);
Toast.makeText(context, "Text saved to " + absolutePath, 1).show();
} catch (Throwable th) {
printWriter.close();
throw th;
}
} catch (Exception e4) {
Log.e("File Save", "Failed", e4);
Toast.makeText(context, "Unable to save: " + e4.getMessage(), 1).show();
}
}
public final void s() {
Intent intent = new Intent("android.intent.action.SEND");
intent.setFlags(268435456);
intent.putExtra("android.intent.extra.TEXT", getDecodedText());
intent.setType("text/plain");
this.f11031m.startActivity(Intent.createChooser(intent, "Share Decoded Text"));
}
}
@@ -1,232 +0,0 @@
package com.ve3nea.morse_expert;
import android.content.Context;
import android.os.Build;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.rtbishop.look4sat.feature.cw.R;
import E2.g;
import H2.a;
import H2.b;
import j1.C1646n;
/**
* Ported verbatim from Morse Expert 1.15 MainActivity (com.ve3nea.morse_expert); obfuscated
* class/field names kept as-is. Surgery notes: originally extends e.AbstractActivityC1557l
* (obfuscated AppCompat base) -> now a plain controller class, Activity injected via
* onCreate(Activity, ConstraintLayout) for Compose AndroidView integration. Ads (d1.*),
* billing (I2.a), premium/rate menu removed per user. h3.b.r (recursive findViewById),
* K1.a.M (immersive status bar), B.i (permissions) replaced with equivalent standard APIs.
* Decoder logic (H2.a audio, H2.b core, pas engine) untouched.
*/
public class MainActivity {
public static final /* synthetic */ int f11034K = 0;
/* renamed from: D, reason: collision with root package name */
public C1646n f11035D;
/* renamed from: E, reason: collision with root package name */
public a f11036E;
/* renamed from: F, reason: collision with root package name */
public b f11037F;
/* renamed from: G, reason: collision with root package name */
public long f11038G = 0;
/* renamed from: J, reason: collision with root package name */
public final g f11040J = new g(this, 1);
/** Injected Activity context (replaces the original this Activity capabilities) */
public android.app.Activity mActivity;
public void onCreate(android.app.Activity activity, ConstraintLayout constraintLayout) {
onCreate(activity, constraintLayout, true);
}
/**
* applyImmersive=false 用于嵌入小容器(如转发器 CW 面板):
* 不触碰宿主窗口的系统栏标志(避免影响整个 Activity)。
*/
public void onCreate(android.app.Activity activity, ConstraintLayout constraintLayout, boolean applyImmersive) {
this.mActivity = activity;
if (applyImmersive) {
// K1.a.M(getWindow(), false): immersive status bar (nav/fullscreen flags, ported as-is)
setImmersive(activity.getWindow(), false);
}
View inflate = constraintLayout;
int i4 = R.id.decodedTextView;
DecodedTextView decodedTextView = (DecodedTextView) findViewRecursive(inflate, R.id.decodedTextView);
if (decodedTextView != null) {
i4 = R.id.scaleView;
ScaleView scaleView = (ScaleView) findViewRecursive(inflate, R.id.scaleView);
if (scaleView != null) {
i4 = R.id.statusTextView;
TextView textView = (TextView) findViewRecursive(inflate, R.id.statusTextView);
if (textView != null) {
i4 = R.id.verticalLayout;
LinearLayout linearLayout = (LinearLayout) findViewRecursive(inflate, R.id.verticalLayout);
if (linearLayout != null) {
i4 = R.id.waterfallView;
WaterfallSurfaceView waterfallSurfaceView = (WaterfallSurfaceView) findViewRecursive(inflate, R.id.waterfallView);
if (waterfallSurfaceView != null) {
this.f11035D = new C1646n(constraintLayout, decodedTextView, scaleView, textView, linearLayout, waterfallSurfaceView);
((WaterfallSurfaceView) this.f11035D.f11892k).setOnTouchListener(this.f11040J);
// Originally: permission check then v(); permission flow handled by the Compose page
// Originally: ad banner mounting (removed)
return;
}
}
}
}
}
throw new NullPointerException("Missing required view with ID: " + inflate.getResources().getResourceName(i4));
}
/** Original h3.b.r: recursive findViewById (ported behavior) */
private static View findViewRecursive(View view, int id) {
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View found = viewGroup.getChildAt(i).findViewById(id);
if (found != null) {
return found;
}
}
return null;
}
return null;
}
/** Original K1.a.M(window, false): immersive (hide nav/fullscreen flags, ported as-is) */
private static void setImmersive(Window window, boolean immersive) {
if (Build.VERSION.SDK_INT >= 30) {
window.setDecorFitsSystemWindows(!immersive);
if (immersive) {
window.getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN);
} else {
window.getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
} else {
View decorView = window.getDecorView();
int systemUiVisibility = decorView.getSystemUiVisibility();
decorView.setSystemUiVisibility(immersive ? (systemUiVisibility & (-1793)) : (systemUiVisibility | 1792));
}
}
/** Original v(): audio capture + decoder core init (logic unchanged) */
public void v() {
this.f11036E = new a();
b bVar = new b(mActivity.getApplicationContext());
this.f11037F = bVar;
this.f11036E.f601l = bVar;
bVar.f619o = (WaterfallSurfaceView) this.f11035D.f11892k;
bVar.f620p = this;
bVar.f621q = this;
}
/** Original onResume: restore decoding (message type/font/background/record start/waterfall) */
public void onResume() {
android.content.SharedPreferences prefs = mActivity.getSharedPreferences(
mActivity.getPackageName() + "_preferences", Context.MODE_PRIVATE);
String messageType = prefs.getString("message_type", "ham_radio_qso");
b bVar = this.f11037F;
if (bVar != null) {
bVar.f623s.set(messageType.equals("ham_radio_qso"));
}
((DecodedTextView) this.f11035D.f11888g).setTextSize(Math.max(7, prefs.getInt("text_font_size", 18)));
b bVar2 = this.f11037F;
if (bVar2 != null) {
bVar2.f617m.b(mActivity);
((DecodedTextView) this.f11035D.f11888g).setBackgroundColor(this.f11037F.f617m.f665a[0]);
}
a aVar = this.f11036E;
if (aVar != null) {
android.media.AudioRecord audioRecord = aVar.f596g;
audioRecord.startRecording();
if (audioRecord.getState() != 1) {
android.util.Log.e("AudioRecord", "cannot start");
}
aVar.f598i.set(true);
}
((WaterfallSurfaceView) this.f11035D.f11892k).onResume();
// Originally: ad lifecycle (removed)
}
/** Original onPause: stop recording + waterfall pause */
public void onPause() {
a aVar = this.f11036E;
if (aVar != null) {
aVar.f598i.set(false);
android.media.AudioRecord audioRecord = aVar.f596g;
audioRecord.stop();
if (audioRecord.getState() != 1) {
android.util.Log.e("AudioRecord", "cannot stop");
}
}
((WaterfallSurfaceView) this.f11035D.f11892k).onPause();
}
/** Original onDestroy: release audio/decoder (billing cleanup removed) */
public void onDestroy() {
a aVar = this.f11036E;
if (aVar != null) {
aVar.f598i.set(false);
try {
aVar.f596g.stop();
aVar.f596g.release();
} catch (Exception ignored) {
}
aVar.f597h.shutdownNow();
}
this.f11036E = null;
this.f11037F = null;
}
/** Menu: clear decoded text (original clear_mnu) */
public void clearDecoded() {
((DecodedTextView) this.f11035D.f11888g).setText("");
}
/** Menu: pause/resume decoding (original pause_mnu, red/white icon toggle logic) */
public boolean togglePause() {
java.util.concurrent.atomic.AtomicBoolean atomicBoolean = ((DecodedTextView) this.f11035D.f11888g).f11033o;
atomicBoolean.set(!atomicBoolean.get());
return atomicBoolean.get();
}
/** Menu: save text (original save_mnu -> DecodedTextView.r()) */
public void saveText() {
((DecodedTextView) this.f11035D.f11888g).r();
}
/** Menu: record signals (original record_signals_mnu) */
public void recordSignals() {
this.f11037F.f626v.set(true);
}
/** Original tap_back_again_to_close hint */
public boolean handleBackPress() {
if (System.currentTimeMillis() < this.f11038G + 2000) {
return true; // double-back: exit
}
this.f11038G = System.currentTimeMillis();
Toast.makeText(mActivity, R.string.tap_back_again_to_close, 0).show();
return false;
}
/** Start decoding after permission granted (original onRequestPermissionsResult success) */
public void onPermissionGranted() {
v();
}
}
@@ -1,76 +0,0 @@
package com.ve3nea.morse_expert;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
/* loaded from: classes.dex */
public class ScaleView extends View {
public int f;
/* renamed from: g, reason: collision with root package name */
public int f11041g;
/* renamed from: h, reason: collision with root package name */
public final Paint f11042h;
/* renamed from: i, reason: collision with root package name */
public final Paint f11043i;
/* renamed from: j, reason: collision with root package name */
public final Paint f11044j;
/* renamed from: k, reason: collision with root package name */
public final Paint f11045k;
public ScaleView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
Paint paint = new Paint();
this.f11042h = paint;
paint.setColor(-16711936);
Paint paint2 = new Paint();
this.f11043i = paint2;
paint2.setColor(-16744448);
Paint paint3 = new Paint();
this.f11044j = paint3;
paint3.setColor(-256);
Paint paint4 = new Paint();
this.f11045k = paint4;
paint4.setColor(-8355840);
}
@Override // android.view.View
public final void onDraw(Canvas canvas) {
Paint paint;
super.onDraw(canvas);
int i4 = this.f11041g;
if (i4 != 1) {
if (i4 != 2) {
if (i4 != 3) {
if (i4 != 4) {
return;
} else {
paint = this.f11044j;
}
} else {
paint = this.f11045k;
}
} else {
paint = this.f11042h;
}
} else {
paint = this.f11043i;
}
int round = (int) Math.round(getWidth() * 0.7d);
Path path = new Path();
path.moveTo(0.0f, this.f);
float f = round;
path.lineTo(f, this.f - round);
path.lineTo(f, this.f + round);
path.close();
canvas.drawPath(path, paint);
}
}
@@ -1,19 +0,0 @@
package com.ve3nea.morse_expert;
import J2.b;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
/* loaded from: classes.dex */
public class WaterfallSurfaceView extends GLSurfaceView {
public final b f;
public WaterfallSurfaceView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
setEGLContextClientVersion(2);
b bVar = new b(context, H2.b.f603B);
this.f = bVar;
setRenderer(bVar);
}
}
@@ -1,82 +0,0 @@
package d1;
/* renamed from: d1.b, reason: case insensitive filesystem */
/* 照搬自 Morse Expert 1.15, 混淆名保持。b(Parcel)/c(y2.d) 为库工具已裁剪(L1/y2 库依赖)。 */
public abstract /* synthetic */ class AbstractC1518b {
public static float a(float f, float f4, k3.d dVar, long j4, float f5, float f6) {
dVar.c(j4, f - f4);
return f5 + f6;
}
public static String d(String str, int i4) {
return str + i4;
}
public static String e(String str, String str2) {
return str + str2;
}
public static String f(String str, String str2, String str3) {
return str + str2 + str3;
}
public static String g(StringBuilder sb, int i4, String str) {
sb.append(i4);
sb.append(str);
return sb.toString();
}
public static String h(StringBuilder sb, String str, String str2) {
sb.append(str);
sb.append(str2);
return sb.toString();
}
public static /* synthetic */ boolean i(Object obj) {
return obj != null;
}
public static float j(float f, float f4, k3.d dVar, long j4, float f5, float f6) {
dVar.c(j4, f + f4);
return f5 + f6;
}
public static String k(String str, String str2) {
return str + str2;
}
public static float l(float f, float f4, k3.d dVar, long j4, float f5, float f6) {
dVar.c(j4, f - f4);
return f5 - f6;
}
public static float m(float f, float f4, k3.d dVar, long j4, float f5, float f6) {
dVar.c(j4, f + f4);
return f5 - f6;
}
public static /* synthetic */ String n(int i4) {
switch (i4) {
case 1:
return "NONE";
case 2:
return "LEFT";
case 3:
return "TOP";
case 4:
return "RIGHT";
case 5:
return "BOTTOM";
case 6:
return "BASELINE";
case 7:
return "CENTER";
case 8:
return "CENTER_X";
case 9:
return "CENTER_Y";
default:
throw null;
}
}
}
-34
View File
@@ -1,34 +0,0 @@
package g3;
/* loaded from: classes.dex */
public final class a {
/* renamed from: a, reason: collision with root package name */
public final int f11582a;
/* renamed from: b, reason: collision with root package name */
public final double f11583b;
public final double c;
public a(double d4) {
int i4 = (int) (0.6366197723675814d * d4);
while (true) {
double d5 = -i4;
double d6 = 1.570796251296997d * d5;
double d7 = d4 + d6;
double d8 = 7.549789948768648E-8d * d5;
double d9 = d8 + d7;
double d10 = (-((d7 - d4) - d6)) + (-((d9 - d7) - d8));
double d11 = d5 * 6.123233995736766E-17d;
double d12 = d11 + d9;
double d13 = d10 + (-((d12 - d9) - d11));
if (d12 > 0.0d) {
this.f11582a = i4;
this.f11583b = d12;
this.c = d13;
return;
}
i4--;
}
}
}
-8
View File
@@ -1,8 +0,0 @@
package g3;
/* loaded from: classes.dex */
public abstract class b {
/* renamed from: a, reason: collision with root package name */
public static final double[][] f11584a = (double[][]) d.f11591a.clone();
}
-507
View File
@@ -1,507 +0,0 @@
package g3;
/* loaded from: classes.dex */
public abstract class c {
/* renamed from: a, reason: collision with root package name */
public static final double[][] f11585a;
/* renamed from: b, reason: collision with root package name */
public static final double[] f11586b;
public static final double[] c;
/* renamed from: d, reason: collision with root package name */
public static final double[] f11587d;
/* renamed from: e, reason: collision with root package name */
public static final double[] f11588e;
public static final long[] f;
/* renamed from: g, reason: collision with root package name */
public static final long[] f11589g;
/* renamed from: h, reason: collision with root package name */
public static final double[] f11590h;
static {
StrictMath.log(Double.MAX_VALUE);
f11585a = new double[][]{new double[]{1.0d, 5.669184079525E-24d}, new double[]{-0.25d, -0.25d}, new double[]{0.3333333134651184d, 1.986821492305628E-8d}, new double[]{-0.25d, -6.663542893624021E-14d}, new double[]{0.19999998807907104d, 1.1921056801463227E-8d}, new double[]{-0.1666666567325592d, -7.800414592973399E-9d}, new double[]{0.1428571343421936d, 5.650007086920087E-9d}, new double[]{-0.12502530217170715d, -7.44321345601866E-11d}, new double[]{0.11113807559013367d, 9.219544613762692E-9d}};
f11586b = new double[]{0.0d, 0.1246747374534607d, 0.24740394949913025d, 0.366272509098053d, 0.4794255495071411d, 0.5850973129272461d, 0.6816387176513672d, 0.7675435543060303d, 0.8414709568023682d, 0.902267575263977d, 0.9489846229553223d, 0.9808930158615112d, 0.9974949359893799d, 0.9985313415527344d};
c = new double[]{0.0d, -4.068233003401932E-9d, 9.755392680573412E-9d, 1.9987994582857286E-8d, -1.0902938113007961E-8d, -3.9986783938944604E-8d, 4.23719669792332E-8d, -5.207000323380292E-8d, 2.800552834259E-8d, 1.883511811213715E-8d, -3.5997360512765566E-9d, 4.116164446561962E-8d, 5.0614674548127384E-8d, -1.0129027912496858E-9d};
f11587d = new double[]{1.0d, 0.9921976327896118d, 0.9689123630523682d, 0.9305076599121094d, 0.8775825500488281d, 0.8109631538391113d, 0.7316888570785522d, 0.6409968137741089d, 0.5403022766113281d, 0.4311765432357788d, 0.3153223395347595d, 0.19454771280288696d, 0.07073719799518585d, -0.05417713522911072d};
f11588e = new double[]{0.0d, 3.4439717236742845E-8d, 5.865827662008209E-8d, -3.7999795083850525E-8d, 1.184154459111628E-8d, -3.43338934259355E-8d, 1.1795268640216787E-8d, 4.438921624363781E-8d, 2.925681159240093E-8d, -2.6437112632041807E-8d, 2.2860509143963117E-8d, -4.813899778443457E-9d, 3.6725170580355583E-9d, 2.0217439756338078E-10d};
f = new long[]{2935890503282001226L, 9154082963658192752L, 3952090531849364496L, 9193070505571053912L, 7910884519577875640L, 113236205062349959L, 4577762542105553359L, -5034868814120038111L, 4208363204685324176L, 5648769086999809661L, 2819561105158720014L, -4035746434778044925L, -302932621132653753L, -2644281811660520851L, -3183605296591799669L, 6722166367014452318L, -3512299194304650054L, -7278142539171889152L};
f11589g = new long[]{-3958705157555305932L, -4267615245585081135L};
f11590h = new double[]{0.0d, 0.125d, 0.25d, 0.375d, 0.5d, 0.625d, 0.75d, 0.875d, 1.0d, 1.125d, 1.25d, 1.375d, 1.5d, 1.625d};
}
public static double a(double d4) {
double d5;
if (d4 != d4) {
return d4;
}
if (d4 == d4 && d4 < 4.503599627370496E15d && d4 > -4.503599627370496E15d) {
long j4 = (long) d4;
if (d4 < 0.0d && j4 != d4) {
j4--;
}
if (j4 == 0) {
d5 = j4 * d4;
} else {
d5 = j4;
}
} else {
d5 = d4;
}
if (d5 == d4) {
return d5;
}
double d6 = d5 + 1.0d;
if (d6 == 0.0d) {
return d4 * d6;
}
return d6;
}
public static double b(double d4) {
double g4;
double d5 = 0.0d;
if (d4 < 0.0d) {
d4 = -d4;
}
if (d4 != d4 || d4 == Double.POSITIVE_INFINITY) {
return Double.NaN;
}
int i4 = 0;
if (d4 > 3294198.0d) {
double[] dArr = new double[3];
e(d4, dArr);
i4 = ((int) dArr[0]) & 3;
d4 = dArr[1];
d5 = dArr[2];
} else if (d4 > 1.5707963267948966d) {
a aVar = new a(d4);
i4 = aVar.f11582a & 3;
d4 = aVar.f11583b;
d5 = aVar.c;
}
if (i4 != 0) {
if (i4 != 1) {
if (i4 != 2) {
if (i4 != 3) {
return Double.NaN;
}
return g(d4, d5);
}
g4 = c(d4, d5);
} else {
g4 = g(d4, d5);
}
return -g4;
}
return c(d4, d5);
}
public static double c(double d4, double d5) {
double d6 = 1.5707963267948966d - d4;
return g(d6, (6.123233995736766E-17d - d5) + (-((d6 - 1.5707963267948966d) + d4)));
}
public static double d(double d4) {
if (d4 != 0.0d) {
long doubleToRawLongBits = Double.doubleToRawLongBits(d4);
if (((Long.MIN_VALUE & doubleToRawLongBits) != 0 || d4 != d4) && d4 != 0.0d) {
return Double.NaN;
}
if (d4 == Double.POSITIVE_INFINITY) {
return Double.POSITIVE_INFINITY;
}
int i4 = ((int) (doubleToRawLongBits >> 52)) - 1023;
if ((9218868437227405312L & doubleToRawLongBits) == 0) {
if (d4 == 0.0d) {
return Double.NEGATIVE_INFINITY;
}
doubleToRawLongBits <<= 1;
while ((4503599627370496L & doubleToRawLongBits) == 0) {
i4--;
doubleToRawLongBits <<= 1;
}
}
if ((i4 == -1 || i4 == 0) && d4 < 1.01d && d4 > 0.99d) {
double d5 = d4 - 1.0d;
double d6 = d5 * 1.073741824E9d;
double d7 = (d5 + d6) - d6;
double d8 = d5 - d7;
double[][] dArr = f11585a;
double[] dArr2 = dArr[dArr.length - 1];
double d9 = dArr2[0];
double d10 = dArr2[1];
for (int length = dArr.length - 2; length >= 0; length--) {
double d11 = d9 * d7;
double d12 = (d10 * d8) + (d10 * d7) + (d9 * d8);
double d13 = d11 * 1.073741824E9d;
double d14 = (d11 + d13) - d13;
double d15 = (d11 - d14) + d12;
double[] dArr3 = dArr[length];
double d16 = d14 + dArr3[0];
double d17 = d15 + dArr3[1];
double d18 = d16 * 1.073741824E9d;
d9 = (d16 + d18) - d18;
d10 = (d16 - d9) + d17;
}
double d19 = d9 * d7;
double d20 = (d10 * d8) + (d7 * d10) + (d9 * d8);
double d21 = 1.073741824E9d * d19;
double d22 = (d19 + d21) - d21;
return (d19 - d22) + d20 + d22;
}
long j4 = 4499201580859392L & doubleToRawLongBits;
double[] dArr4 = b.f11584a[(int) (j4 >> 42)];
double d23 = (doubleToRawLongBits & 4398046511103L) / (j4 + 4.503599627370496E15d);
double d24 = (((((((((((-0.16624882440418567d) * d23) + 0.19999954120254515d) * d23) - 0.2499999997677497d) * d23) + 0.3333333333332802d) * d23) - 0.5d) * d23) + 1.0d) * d23;
double d25 = i4;
double d26 = 0.6931470632553101d * d25;
double d27 = dArr4[0];
double d28 = d26 + d27;
double d29 = (-((d28 - d26) - d27)) + 0.0d;
double d30 = d28 + d24;
double d31 = d29 + (-((d30 - d28) - d24));
double d32 = d25 * 1.1730463525082348E-7d;
double d33 = d30 + d32;
double d34 = d31 + (-((d33 - d30) - d32));
double d35 = dArr4[1];
double d36 = d33 + d35;
double d37 = d36 + 0.0d;
return d34 + (-((d36 - d33) - d35)) + (-((d37 - d36) - 0.0d)) + d37;
}
return Double.NEGATIVE_INFINITY;
}
public static void e(double d4, double[] dArr) {
long j4;
long j5;
long j6;
boolean z3;
boolean z4;
boolean z5;
boolean z6;
boolean z7;
boolean z8;
boolean z9;
boolean z10;
boolean z11;
boolean z12;
boolean z13;
boolean z14;
boolean z15;
boolean z16;
boolean z17;
boolean z18;
boolean z19;
boolean z20;
boolean z21;
boolean z22;
boolean z23;
long j7;
long doubleToRawLongBits = Double.doubleToRawLongBits(d4);
int i4 = ((int) ((doubleToRawLongBits >> 52) & 2047)) - 1022;
long j8 = ((doubleToRawLongBits & 4503599627370495L) | 4503599627370496L) << 11;
int i5 = i4 >> 6;
int i6 = i4 - (i5 << 6);
long[] jArr = f;
if (i6 != 0) {
if (i5 == 0) {
j7 = 0;
} else {
j7 = jArr[i5 - 1] << i6;
}
long j9 = jArr[i5];
int i7 = 64 - i6;
j4 = j7 | (j9 >>> i7);
long j10 = jArr[i5 + 1];
j5 = (j9 << i6) | (j10 >>> i7);
j6 = (jArr[i5 + 2] >>> i7) | (j10 << i6);
} else {
if (i5 == 0) {
j4 = 0;
} else {
j4 = jArr[i5 - 1];
}
j5 = jArr[i5];
j6 = jArr[i5 + 1];
}
long j11 = j8 >>> 32;
long j12 = j8 & 4294967295L;
long j13 = j5 >>> 32;
long j14 = j5 & 4294967295L;
long j15 = j11 * j13;
long j16 = j12 * j14;
long j17 = j13 * j12;
long j18 = j14 * j11;
long j19 = j16 + (j18 << 32);
long j20 = j15 + (j18 >>> 32);
if ((j16 & Long.MIN_VALUE) != 0) {
z3 = true;
} else {
z3 = false;
}
if ((j18 & 2147483648L) != 0) {
z4 = true;
} else {
z4 = false;
}
long j21 = j19 & Long.MIN_VALUE;
if (j21 != 0) {
z5 = true;
} else {
z5 = false;
}
if ((z3 && z4) || ((z3 || z4) && !z5)) {
j20++;
}
if (j21 != 0) {
z6 = true;
} else {
z6 = false;
}
if ((j17 & 2147483648L) != 0) {
z7 = true;
} else {
z7 = false;
}
long j22 = j19 + (j17 << 32);
long j23 = j20 + (j17 >>> 32);
long j24 = j22 & Long.MIN_VALUE;
if (j24 != 0) {
z8 = true;
} else {
z8 = false;
}
if ((z6 && z7) || ((z6 || z7) && !z8)) {
j23++;
}
long j25 = j6 >>> 32;
long j26 = (j11 * j25) + ((((j6 & 4294967295L) * j11) + (j25 * j12)) >>> 32);
if (j24 != 0) {
z9 = true;
} else {
z9 = false;
}
if ((j26 & Long.MIN_VALUE) != 0) {
z10 = true;
} else {
z10 = false;
}
long j27 = j22 + j26;
if ((j27 & Long.MIN_VALUE) != 0) {
z11 = true;
} else {
z11 = false;
}
if ((z9 && z10) || ((z9 || z10) && !z11)) {
j23++;
}
long j28 = j4 >>> 32;
long j29 = j4 & 4294967295L;
long j30 = (j12 * j29) + (((j11 * j29) + (j12 * j28)) << 32) + j23;
int i8 = (int) (j30 >>> 62);
long j31 = (j27 >>> 62) | (j30 << 2);
long j32 = j27 << 2;
long j33 = j31 >>> 32;
long j34 = j31 & 4294967295L;
long[] jArr2 = f11589g;
long j35 = jArr2[0];
long j36 = j35 >>> 32;
long j37 = j35 & 4294967295L;
long j38 = j34 * j37;
long j39 = j34 * j36;
long j40 = j33 * j37;
long j41 = j38 + (j40 << 32);
long j42 = (j33 * j36) + (j40 >>> 32);
if ((j38 & Long.MIN_VALUE) != 0) {
z12 = true;
} else {
z12 = false;
}
if ((j40 & 2147483648L) != 0) {
z13 = true;
} else {
z13 = false;
}
long j43 = j41 & Long.MIN_VALUE;
if (j43 != 0) {
z14 = true;
} else {
z14 = false;
}
if ((z12 && z13) || ((z12 || z13) && !z14)) {
j42++;
}
if (j43 != 0) {
z15 = true;
} else {
z15 = false;
}
if ((j39 & 2147483648L) != 0) {
z16 = true;
} else {
z16 = false;
}
long j44 = j41 + (j39 << 32);
long j45 = j42 + (j39 >>> 32);
long j46 = j44 & Long.MIN_VALUE;
if (j46 != 0) {
z17 = true;
} else {
z17 = false;
}
if ((z15 && z16) || ((z15 || z16) && !z17)) {
j45++;
}
long j47 = jArr2[1];
long j48 = j47 >>> 32;
long j49 = (j33 * j48) + (((j33 * (j47 & 4294967295L)) + (j34 * j48)) >>> 32);
if (j46 != 0) {
z18 = true;
} else {
z18 = false;
}
if ((j49 & Long.MIN_VALUE) != 0) {
z19 = true;
} else {
z19 = false;
}
long j50 = j44 + j49;
long j51 = j50 & Long.MIN_VALUE;
if (j51 != 0) {
z20 = true;
} else {
z20 = false;
}
if ((z18 && z19) || ((z18 || z19) && !z20)) {
j45++;
}
long j52 = j32 >>> 32;
long j53 = (j52 * j36) + (((j52 * j37) + ((j32 & 4294967295L) * j36)) >>> 32);
if (j51 != 0) {
z21 = true;
} else {
z21 = false;
}
if ((j53 & Long.MIN_VALUE) != 0) {
z22 = true;
} else {
z22 = false;
}
if (((j50 + j53) & Long.MIN_VALUE) != 0) {
z23 = true;
} else {
z23 = false;
}
if ((z21 && z22) || ((z21 || z22) && !z23)) {
j45++;
}
double d5 = (j45 >>> 12) / 4.503599627370496E15d;
double d6 = ((((j45 & 4095) << 40) + (f11589g[0] >>> 24)) / 4.503599627370496E15d) / 4.503599627370496E15d; // r34 = f11589g[0](smali: aget-wide v5, v2)
double d7 = d5 + d6;
dArr[0] = i8;
dArr[1] = d7 * 2.0d;
dArr[2] = (-((d7 - d5) - d6)) * 2.0d;
}
/* JADX WARN: Removed duplicated region for block: B:20:0x005f */
/* JADX WARN: Removed duplicated region for block: B:22:0x0063 */
/* JADX WARN: Removed duplicated region for block: B:32:0x007a */
/*
Code decompiled incorrectly, please refer to instructions dump.
*/
public static double f(double d4) {
boolean z3;
double d5;
double d6;
double g4;
int i4 = 0;
double d7 = 0.0d;
if (d4 < 0.0d) {
d5 = -d4;
z3 = true;
} else {
z3 = false;
d5 = d4;
}
if (d5 == 0.0d) {
if (Double.doubleToRawLongBits(d4) >= 0) {
return 0.0d;
}
return -0.0d;
}
if (d5 != d5 || d5 == Double.POSITIVE_INFINITY) {
return Double.NaN;
}
if (d5 > 3294198.0d) {
double[] dArr = new double[3];
e(d5, dArr);
i4 = ((int) dArr[0]) & 3;
d5 = dArr[1];
d6 = dArr[2];
} else {
if (d5 > 1.5707963267948966d) {
a aVar = new a(d5);
int i5 = aVar.f11582a & 3;
d5 = aVar.f11583b;
d6 = aVar.c;
i4 = i5;
}
if (z3) {
i4 ^= 2;
}
// smali 证实: f() = sin(象限 switch!):
// case 0 -> g(d5,0)(sin(0-π/2))
// case 1 -> c(d5,0)(sin(π/2-π) = cos(余角))
// case 2 -> -g(d5,0)(sin(π-3π/2) = -sin(余角))
// case 3 -> -c(d5,0)(sin(3π/2-2π) = -cos(余角))
// jadx 畸形嵌套 if 把 case 0 还原成 return NaN(小角度全 NaN!),
// 且 case 1/2/3 全走 g() —— 旋转因子表 329 个 NaN + 频谱错乱的根源!
if (i4 == 0) {
return g(d5, d7);
}
if (i4 == 1) {
return c(d5, d7);
}
if (i4 == 2) {
return -g(d5, d7);
}
return -c(d5, d7);
}
d7 = d6;
// jadx 还原不完整(smali 650 行, jadx 只还原 63 行); f() 未被任何代码调用(死代码),
// 按 smali 尾部语义补 return(cond_0: v22 = v22 + v14; return v22)
return d7;
}
public static double g(double d4, double d5) {
int i4 = (int) ((8.0d * d4) + 0.5d);
double d6 = d4 - f11590h[i4];
double d7 = f11586b[i4];
double d8 = c[i4];
double d9 = f11587d[i4];
double d10 = f11588e[i4];
double d11 = d6 * d6;
double d12 = ((((((2.7553817452272217E-6d * d11) - 1.9841269659586505E-4d) * d11) + 0.008333333333329196d) * d11) - 0.16666666666666666d) * d11 * d6;
double d13 = ((((((2.479773539153719E-5d * d11) - 0.0013888888689039883d) * d11) + 0.041666666666621166d) * d11) - 0.49999999999999994d) * d11;
double d14 = 1.073741824E9d * d6;
double d15 = (d6 + d14) - d14;
double d16 = (d6 - d15) + d12;
double d17 = d7 + 0.0d;
double d18 = d9 * d15;
double d19 = d17 + d18;
double d20 = (d10 * d16) + (d8 * d13) + (d10 * d15) + (d9 * d16) + (d7 * d13) + (-((d17 - 0.0d) - d7)) + 0.0d + (-((d19 - d17) - d18)) + d8;
if (d5 != 0.0d) {
double d21 = (((d13 + 1.0d) * (d9 + d10)) - ((d15 + d16) * (d7 + d8))) * d5;
double d22 = d19 + d21;
d20 += -((d22 - d19) - d21);
d19 = d22;
}
return d19 + d20;
}
}
-9
View File
@@ -1,9 +0,0 @@
package g3;
/* loaded from: classes.dex */
public abstract class d {
/* renamed from: a, reason: collision with root package name */
public static final double[][] f11591a = {new double[]{0.0d, 0.0d}, new double[]{9.760860120877624E-4d, -3.903230345984362E-11d}, new double[]{0.0019512202125042677d, -8.124251825289188E-11d}, new double[]{0.0029254043474793434d, -1.8374207360194882E-11d}, new double[]{0.0038986406289041042d, -2.1324678121885073E-10d}, new double[]{0.004870930686593056d, -4.5199654318611534E-10d}, new double[]{0.00584227591753006d, -2.933016992001806E-10d}, new double[]{0.006812678650021553d, -2.325147219074669E-10d}, new double[]{0.007782140746712685d, -3.046577356838847E-10d}, new double[]{0.008750664070248604d, -5.500631513861575E-10d}, new double[]{0.00971824862062931d, 8.48292035519895E-10d}, new double[]{0.010684899985790253d, 1.1422610134013436E-10d}, new double[]{0.01165061630308628d, 9.168889933128375E-10d}, new double[]{0.012615403160452843d, -5.303786078838E-10d}, new double[]{0.013579258695244789d, -5.688639355498786E-10d}, new double[]{0.01454218477010727d, 7.296670293275653E-10d}, new double[]{0.015504186972975731d, -4.370104767451421E-10d}, new double[]{0.016465261578559875d, 1.43695591408832E-9d}, new double[]{0.01742541790008545d, -1.1862263158849434E-9d}, new double[]{0.018384650349617004d, -9.482976524690715E-10d}, new double[]{0.01934296265244484d, 1.9068609515836638E-10d}, new double[]{0.020300358533859253d, 2.655990315697216E-10d}, new double[]{0.021256837993860245d, 1.0315548713040775E-9d}, new double[]{0.022212404757738113d, 5.13345647019085E-10d}, new double[]{0.02316705882549286d, 4.5604151934208014E-10d}, new double[]{0.02412080392241478d, -1.1255706987475148E-9d}, new double[]{0.025073636323213577d, 1.2289023836765196E-9d}, new double[]{0.02602556347846985d, 1.7990281828096504E-9d}, new double[]{0.026976589113473892d, -1.4152718164638451E-9d}, new double[]{0.02792670577764511d, 7.568772963781632E-10d}, new double[]{0.0288759246468544d, -1.1449998592111558E-9d}, new double[]{0.029824241995811462d, -1.6850976862319495E-9d}, new double[]{0.030771657824516296d, 8.422373919843096E-10d}, new double[]{0.0317181795835495d, 6.872350402175489E-10d}, new double[]{0.03266380727291107d, -4.541194749189272E-10d}, new double[]{0.03360854089260101d, -8.9064764856495E-10d}, new double[]{0.034552380442619324d, 1.0640404096769032E-9d}, new double[]{0.0354953333735466d, -3.5901655945224663E-10d}, new double[]{0.03643739968538284d, -3.4829517943661266E-9d}, new double[]{0.037378571927547455d, 8.149473794244232E-10d}, new double[]{0.03831886500120163d, -6.990650304449166E-10d}, new double[]{0.03925827145576477d, 1.0883076226453258E-9d}, new double[]{0.040196798741817474d, 3.845192807999274E-10d}, new double[]{0.04113444685935974d, -1.1570594692045927E-9d}, new double[]{0.04207121580839157d, -1.8877045166697178E-9d}, new double[]{0.043007105588912964d, -1.6332083257987747E-10d}, new double[]{0.04394212365150452d, -1.7950057534514933E-9d}, new double[]{0.04487626254558563d, 2.302710041648838E-9d}, new double[]{0.045809537172317505d, -1.1410233017161343E-9d}, new double[]{0.04674194008111954d, -3.0498741599744685E-9d}, new double[]{0.04767347127199173d, -1.8026348269183678E-9d}, new double[]{0.04860413819551468d, -3.233204600453039E-9d}, new double[]{0.04953393340110779d, 1.7211688427961583E-9d}, new double[]{0.05046287178993225d, -2.329967807055457E-10d}, new double[]{0.05139094591140747d, -4.191810118556531E-11d}, new double[]{0.052318163216114044d, -3.5574324788328143E-9d}, new double[]{0.053244516253471375d, -1.7346590916458485E-9d}, new double[]{0.05417001247406006d, -4.343048751383674E-10d}, new double[]{0.055094651877880096d, 1.92909364037955E-9d}, new double[]{0.056018441915512085d, -5.139745677199588E-10d}, new double[]{0.05694137513637543d, 1.2637629975129189E-9d}, new double[]{0.05786345899105072d, 1.3840561112481119E-9d}, new double[]{0.058784693479537964d, 1.414889689612056E-9d}, new double[]{0.05970507860183716d, 2.9199191907666474E-9d}, new double[]{0.0606246218085289d, 7.90594243412116E-12d}, new double[]{0.06154331564903259d, 1.6844747839686189E-9d}, new double[]{0.06246116757392883d, 2.0498074572151747E-9d}, new double[]{0.06337818503379822d, -4.800180493433863E-9d}, new double[]{0.06429435312747955d, -2.4220822960064277E-9d}, new double[]{0.06520968675613403d, -4.179048566709334E-9d}, new double[]{0.06612417101860046d, 6.363872957010456E-9d}, new double[]{0.06703783571720123d, 9.339468680056365E-10d}, new double[]{0.06795066595077515d, -4.04226739708981E-9d}, new double[]{0.0688626617193222d, -7.043545052852817E-9d}, new double[]{0.06977382302284241d, -6.552819560439773E-9d}, new double[]{0.07068414986133575d, -1.0571674860370546E-9d}, new double[]{0.07159365713596344d, -3.948954622015801E-9d}, new double[]{0.07250232994556427d, 1.1776625988228244E-9d}, new double[]{0.07341018319129944d, 9.221072639606492E-10d}, new double[]{0.07431721687316895d, -3.219119568928366E-9d}, new double[]{0.0752234160900116d, 5.147575929018918E-9d}, new double[]{0.07612881064414978d, -2.291749683541979E-9d}, new doubleLine truncated
new double[]{0.6813591718673706d, 5.294053246347931E-8d}, new double[]{0.681853175163269d, -1.2220826223585654E-9d}, new double[]{0.6823468208312988d, 5.8377876767612725E-8d}, new double[]{0.6828403472900391d, -6.437492120743254E-9d}, new double[]{0.6833335161209106d, 4.2990710043633113E-8d}, new double[]{0.6838265657424927d, -3.1516131027023284E-8d}, new double[]{0.684319257736206d, 8.70017386744679E-9d}, new double[]{0.6848117113113403d, 4.466959125843237E-8d}, new double[]{0.6853040456771851d, -4.25782656420497E-8d}, new double[]{0.6857960224151611d, -1.4386267593671393E-8d}, new double[]{0.6862877607345581d, 1.0274494061148778E-8d}, new double[]{0.686779260635376d, 3.164186629229597E-8d}, new double[]{0.6872705221176147d, 4.995334552140326E-8d}, new double[]{0.687761664390564d, -5.3763211240398744E-8d}, new double[]{0.6882524490356445d, -4.0852427502515625E-8d}, new double[]{0.688742995262146d, -3.0287143914420064E-8d}, new double[]{0.6892333030700684d, -2.183125937905008E-8d}, new double[]{0.6897233724594116d, -1.524901992178814E-8d}, new double[]{0.6902132034301758d, -1.0305018010328949E-8d}, new double[]{0.6907027959823608d, -6.764191876212205E-9d}, new double[]{0.6911921501159668d, -4.391824838015402E-9d}, new double[]{0.6916812658309937d, -2.9535446262017846E-9d}, new double[]{0.6921701431274414d, -2.2153227096187463E-9d}, new double[]{0.6926587820053101d, -1.943473623641502E-9d}};
}
-41
View File
@@ -1,41 +0,0 @@
package i3;
/* loaded from: classes.dex */
public final class a implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ int f11686g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ int f11687h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ float[] f11688i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ float[] f11689j;
/* renamed from: k, reason: collision with root package name */
public final /* synthetic */ d f11690k;
public a(d dVar, int i4, int i5, int i6, float[] fArr, float[] fArr2) {
this.f11690k = dVar;
this.f = i4;
this.f11686g = i5;
this.f11687h = i6;
this.f11688i = fArr;
this.f11689j = fArr2;
}
@Override // java.lang.Runnable
public final void run() {
int i4 = this.f11686g;
int i5 = this.f;
if (i5 >= i4) {
return;
}
float f = this.f11689j[this.f11687h + i5];
throw null;
}
}
-41
View File
@@ -1,41 +0,0 @@
package i3;
/* loaded from: classes.dex */
public final class b implements Runnable {
public final /* synthetic */ long f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ long f11691g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ long f11692h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ k3.d f11693i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ k3.d f11694j;
/* renamed from: k, reason: collision with root package name */
public final /* synthetic */ d f11695k;
public b(d dVar, long j4, long j5, long j6, k3.d dVar2, k3.d dVar3) {
this.f11695k = dVar;
this.f = j4;
this.f11691g = j5;
this.f11692h = j6;
this.f11693i = dVar2;
this.f11694j = dVar3;
}
@Override // java.lang.Runnable
public final void run() {
long j4 = this.f11691g;
long j5 = this.f;
if (j5 >= j4) {
return;
}
this.f11694j.b(this.f11692h + j5);
throw null;
}
}
-55
View File
@@ -1,55 +0,0 @@
package i3;
import k3.g;
import k3.s;
/* loaded from: classes.dex */
public final class c implements Runnable {
public final /* synthetic */ int f = 1;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ long f11696g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ long f11697h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ g f11698i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ Object f11699j;
public c(long j4, long j5, s sVar, s sVar2) {
this.f11696g = j4;
this.f11697h = j5;
this.f11698i = sVar;
this.f11699j = sVar2;
}
@Override // java.lang.Runnable
public final void run() {
switch (this.f) {
case 0:
k3.d dVar = (k3.d) this.f11698i;
long j4 = this.f11697h;
long j5 = this.f11696g;
if (j5 >= j4) {
return;
}
dVar.b(j5 * 2);
throw null;
default:
for (long j6 = this.f11696g; j6 < this.f11697h; j6++) {
((s) this.f11698i).c(j6, ((s) this.f11699j).b(j6));
}
return;
}
}
public c(d dVar, long j4, long j5, k3.d dVar2) {
this.f11699j = dVar;
this.f11696g = j4;
this.f11697h = j5;
this.f11698i = dVar2;
}
}
File diff suppressed because it is too large. Load diff
-42
View File
@@ -1,42 +0,0 @@
package j1;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.ve3nea.morse_expert.DecodedTextView;
import com.ve3nea.morse_expert.ScaleView;
import com.ve3nea.morse_expert.WaterfallSurfaceView;
/**
* 照搬自 Morse Expert 1.15 j1.C1646n, 类名/字段名保持混淆原名。
* R8 合并类手术: 只保留视图持有器(6 参构造 + 5 个 View 字段);
* 其余 158-1278 行(广告/计费合并方法)已整段裁剪(用户要求广告不迁移)。
*/
public final class C1646n {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public Object f11888g;
/* renamed from: h, reason: collision with root package name */
public Object f11889h;
/* renamed from: i, reason: collision with root package name */
public Object f11890i;
/* renamed from: j, reason: collision with root package name */
public Object f11891j;
/* renamed from: k, reason: collision with root package name */
public Object f11892k;
public C1646n(ConstraintLayout constraintLayout, DecodedTextView decodedTextView, ScaleView scaleView, TextView textView, LinearLayout linearLayout, WaterfallSurfaceView waterfallSurfaceView) {
this.f = 1;
this.f11888g = decodedTextView;
this.f11889h = scaleView;
this.f11890i = textView;
this.f11891j = linearLayout;
this.f11892k = waterfallSurfaceView;
}
}
-100
View File
@@ -1,100 +0,0 @@
package j3;
/* loaded from: classes.dex */
public final class a implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ int f11919g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ int f11920h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ int f11921i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ float[] f11922j;
/* renamed from: k, reason: collision with root package name */
public final /* synthetic */ float[] f11923k;
/* renamed from: l, reason: collision with root package name */
public final /* synthetic */ int f11924l;
public /* synthetic */ a(int i4, int i5, int i6, int i7, int i8, float[] fArr, float[] fArr2) {
this.f = i8;
this.f11919g = i4;
this.f11920h = i5;
this.f11921i = i6;
this.f11922j = fArr;
this.f11923k = fArr2;
this.f11924l = i7;
}
@Override // java.lang.Runnable
public final void run() {
switch (this.f) {
case 0:
int i4 = this.f11919g;
int i5 = this.f11920h;
int i6 = i4 + i5;
int i7 = this.f11921i;
while (true) {
int i8 = i7;
if (i8 > 512) {
i7 = i8 >> 2;
c.w(i7, i6 - i7, this.f11924l - (i8 >> 3), this.f11922j, this.f11923k);
} else {
int i9 = this.f11924l;
float[] fArr = this.f11923k;
c.u(i8, 1, this.f11922j, i6 - i8, i9, fArr);
int i10 = i4 - i8;
int i11 = 0;
int i12 = i5 - i8;
while (i12 > 0) {
int i13 = i11 + 1;
int i14 = this.f11924l;
float[] fArr2 = this.f11923k;
int i15 = i12;
i11 = i13;
int i16 = this.f11924l;
float[] fArr3 = this.f11923k;
c.u(i8, c.E(i8, i12, i13, this.f11919g, i14, this.f11922j, fArr2), this.f11922j, i10 + i15, i16, fArr3);
i12 = i15 - i8;
}
return;
}
}
default:
int i17 = this.f11919g;
int i18 = this.f11920h;
int i19 = i17 + i18;
int i20 = this.f11921i;
int i21 = 1;
while (i20 > 512) {
i20 >>= 2;
i21 <<= 2;
c.y(i20, i19 - i20, this.f11924l - i20, this.f11922j, this.f11923k);
}
int i22 = this.f11924l;
float[] fArr4 = this.f11923k;
c.u(i20, 0, this.f11922j, i19 - i20, i22, fArr4);
int i23 = i21 >> 1;
int i24 = i17 - i20;
int i25 = i18 - i20;
while (i25 > 0) {
int i26 = i23 + 1;
int i27 = this.f11924l;
float[] fArr5 = this.f11923k;
int i28 = i25;
i23 = i26;
int i29 = this.f11924l;
float[] fArr6 = this.f11923k;
c.u(i20, c.E(i20, i25, i26, this.f11919g, i27, this.f11922j, fArr5), this.f11922j, i24 + i28, i29, fArr6);
i25 = i28 - i20;
}
return;
}
}
}
-104
View File
@@ -1,104 +0,0 @@
package j3;
import k3.d;
/* loaded from: classes.dex */
public final class b implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ long f11925g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ long f11926h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ long f11927i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ d f11928j;
/* renamed from: k, reason: collision with root package name */
public final /* synthetic */ d f11929k;
/* renamed from: l, reason: collision with root package name */
public final /* synthetic */ long f11930l;
public /* synthetic */ b(long j4, long j5, long j6, d dVar, d dVar2, long j7, int i4) {
this.f = i4;
this.f11925g = j4;
this.f11926h = j5;
this.f11927i = j6;
this.f11928j = dVar;
this.f11929k = dVar2;
this.f11930l = j7;
}
@Override // java.lang.Runnable
public final void run() {
switch (this.f) {
case 0:
long j4 = this.f11925g;
long j5 = this.f11926h;
long j6 = j4 + j5;
long j7 = this.f11927i;
while (j7 > 512) {
long j8 = j7 >> 2;
c.x(j8, j6 - j8, this.f11930l - (j7 >> 3), this.f11928j, this.f11929k);
j7 = j8;
}
c.v(j7, 1L, j6 - j7, this.f11930l, this.f11928j, this.f11929k);
long j9 = j4 - j7;
long j10 = j5 - j7;
long j11 = 0;
for (long j12 = 0; j10 > j12; j12 = 0) {
long j13 = j11 + 1;
long j14 = j10;
c.v(j7, c.F(j7, j10, j13, this.f11925g, this.f11930l, this.f11928j, this.f11929k), j9 + j14, this.f11930l, this.f11928j, this.f11929k);
j10 = j14 - j7;
j11 = j13;
j9 = j9;
}
return;
case 1:
long j15 = this.f11925g;
long j16 = this.f11926h;
long j17 = j15 + j16;
long j18 = this.f11927i;
long j19 = 1;
while (j18 > 512) {
long j20 = j18 >> 2;
j19 <<= 2;
c.z(j20, j17 - j20, this.f11930l - j20, this.f11928j, this.f11929k);
j18 = j20;
}
c.v(j18, 0L, j17 - j18, this.f11930l, this.f11928j, this.f11929k);
long j21 = j19 >> 1;
long j22 = j15 - j18;
long j23 = j16 - j18;
while (j23 > 0) {
long j24 = j21 + 1;
long j25 = j23;
j21 = j24;
c.v(j18, c.F(j18, j23, j24, this.f11925g, this.f11930l, this.f11928j, this.f11929k), j22 + j25, this.f11930l, this.f11928j, this.f11929k);
j23 = j25 - j18;
}
return;
default:
for (long j26 = this.f11925g; j26 < this.f11926h; j26++) {
this.f11928j.c(this.f11927i + j26, this.f11929k.b(this.f11930l + j26));
}
return;
}
}
public b(long j4, long j5, long j6, long j7, d dVar, d dVar2) {
this.f = 2;
this.f11925g = j4;
this.f11926h = j5;
this.f11928j = dVar;
this.f11927i = j6;
this.f11929k = dVar2;
this.f11930l = j7;
}
}
File diff suppressed because it is too large. Load diff
-11
View File
@@ -1,11 +0,0 @@
package k3;
import java.lang.Thread;
/* loaded from: classes.dex */
public final class a implements Thread.UncaughtExceptionHandler {
@Override // java.lang.Thread.UncaughtExceptionHandler
public final void uncaughtException(Thread thread, Throwable th) {
th.printStackTrace();
}
}
-25
View File
@@ -1,25 +0,0 @@
package k3;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
/* loaded from: classes.dex */
public final class b implements ThreadFactory {
/* renamed from: b, reason: collision with root package name */
public static final ThreadFactory f12091b = Executors.defaultThreadFactory();
/* renamed from: a, reason: collision with root package name */
public final a f12092a;
public b(a aVar) {
this.f12092a = aVar;
}
@Override // java.util.concurrent.ThreadFactory
public final Thread newThread(Runnable runnable) {
Thread newThread = f12091b.newThread(runnable);
newThread.setUncaughtExceptionHandler(this.f12092a);
return newThread;
}
}
-42
View File
@@ -1,42 +0,0 @@
package k3;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/* loaded from: classes.dex */
public abstract class c {
/* renamed from: a, reason: collision with root package name */
public static final ExecutorService f12093a;
/* renamed from: b, reason: collision with root package name */
public static ExecutorService f12094b;
public static final int c;
/* renamed from: d, reason: collision with root package name */
public static final long f12095d;
/* JADX WARN: Type inference failed for: r1v0, types: [java.lang.Object, k3.a] */
static {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(new b(new a()));
f12093a = newCachedThreadPool;
f12094b = newCachedThreadPool;
c = Runtime.getRuntime().availableProcessors();
f12095d = 100000L;
}
public static Future a(Runnable runnable) {
if (f12094b.isShutdown() || f12094b.isTerminated()) {
f12094b = f12093a;
}
return f12094b.submit(runnable);
}
public static void b(Future[] futureArr) throws InterruptedException, java.util.concurrent.ExecutionException {
// smali: 无 catch 直接向上抛 -> Java 声明 throws(忠实表达)
for (Future future : futureArr) {
future.get();
}
}
}
-100
View File
@@ -1,100 +0,0 @@
package k3;
import com.rtbishop.look4sat.feature.cw.suncompat.Cleaner;
/* loaded from: classes.dex */
public final class d extends g {
/* renamed from: k, reason: collision with root package name */
public float[] f12096k;
public d() {
}
public d(long j4, boolean z3) {
this.f = q.f12106g;
this.f12103h = 4L;
if (j4 > 0) {
this.f12102g = j4;
if (j4 > 1073741824) {
this.f12105j = r.f12109a.allocateMemory(4 * j4);
if (z3) {
a(j4);
}
Cleaner.create(this, new f(this.f12105j, this.f12102g, this.f12103h));
K1.a.c += this.f12102g * this.f12103h;
return;
}
this.f12096k = new float[(int) j4];
return;
}
throw new IllegalArgumentException(j4 + " is not a positive long value");
}
public final float b(long j4) {
float[] fArr = this.f12096k;
long j5 = this.f12105j;
if (j5 != 0) {
return r.f12109a.getFloat((this.f12103h * j4) + j5);
}
if (this.f12104i) {
return fArr[0];
}
return fArr[(int) j4];
}
public final void c(long j4, float f) {
long j5 = this.f12105j;
if (j5 != 0) {
r.f12109a.putFloat((this.f12103h * j4) + j5, f);
} else {
if (!this.f12104i) {
this.f12096k[(int) j4] = f;
return;
}
throw new IllegalAccessError("Constant arrays cannot be modified.");
}
}
/* JADX WARN: Type inference failed for: r0v1, types: [k3.g, java.lang.Object, k3.d] */
public final Object clone() {
if (this.f12104i) {
long j4 = this.f12102g;
float b4 = b(0L);
d gVar = new d();
gVar.f = q.f12106g;
gVar.f12103h = 4L;
if (j4 > 0) {
gVar.f12102g = j4;
gVar.f12104i = true;
gVar.f12096k = new float[]{b4};
return gVar;
}
throw new IllegalArgumentException(j4 + " is not a positive long value");
}
d dVar = new d(this.f12102g, false);
r.a(0L, 0L, this.f12102g, this, dVar);
return dVar;
}
@Override // k3.g
public final boolean equals(Object obj) {
if (!super.equals(obj) || this.f12096k != ((d) obj).f12096k) {
return false;
}
return true;
}
@Override // k3.g
public final int hashCode() {
int i4;
int hashCode = super.hashCode() * 29;
float[] fArr = this.f12096k;
if (fArr != null) {
i4 = fArr.hashCode();
} else {
i4 = 0;
}
return hashCode + i4;
}
}
-79
View File
@@ -1,79 +0,0 @@
package k3;
/* loaded from: classes.dex */
public final class e implements Runnable {
public final /* synthetic */ long f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ long f12097g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ long f12098h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ g f12099i;
public e(g gVar, long j4, long j5, long j6) {
this.f12099i = gVar;
this.f = j4;
this.f12097g = j5;
this.f12098h = j6;
}
/* JADX WARN: Can't fix incorrect switch cases order, some code will duplicate */
/* JADX WARN: Failed to find 'out' block for switch in B:2:0x0011. Please report as an issue. */
@Override // java.lang.Runnable
public final void run() {
g gVar = this.f12099i;
int ordinal = gVar.f.ordinal();
long j4 = this.f12098h;
long j5 = this.f12097g;
long j6 = this.f;
switch (ordinal) {
case 0:
case 1:
case 2:
case 10:
case 11:
while (j6 < j5) {
r.f12109a.putByte((gVar.f12103h * j6) + j4, (byte) 0);
j6++;
}
return;
case 3:
while (j6 < j5) {
r.f12109a.putShort((gVar.f12103h * j6) + j4, (short) 0);
j6++;
}
return;
case 4:
while (j6 < j5) {
r.f12109a.putInt((gVar.f12103h * j6) + j4, 0);
j6++;
}
return;
case 5:
while (j6 < j5) {
r.f12109a.putLong((gVar.f12103h * j6) + j4, 0L);
j6++;
}
return;
case 6:
while (j6 < j5) {
r.f12109a.putFloat((gVar.f12103h * j6) + j4, 0.0f);
j6++;
}
return;
case 7:
while (j6 < j5) {
r.f12109a.putDouble((gVar.f12103h * j6) + j4, 0.0d);
j6++;
}
return;
case 8:
case 9:
default:
throw new IllegalArgumentException("Invalid array type.");
}
}
}
-32
View File
@@ -1,32 +0,0 @@
package k3;
/* loaded from: classes.dex */
public final class f implements Runnable {
public long f;
/* renamed from: g, reason: collision with root package name */
public final long f12100g;
/* renamed from: h, reason: collision with root package name */
public final long f12101h;
public f(long j4, long j5, long j6) {
this.f = j4;
this.f12100g = j5;
this.f12101h = j6;
}
@Override // java.lang.Runnable
public final void run() {
long j4 = this.f;
if (j4 != 0) {
r.f12109a.freeMemory(j4);
this.f = 0L;
long j5 = K1.a.c - (this.f12100g * this.f12101h);
K1.a.c = j5;
if (j5 < 0) {
K1.a.c = 0L;
}
}
}
}
-92
View File
@@ -1,92 +0,0 @@
package k3;
import java.io.Serializable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/* loaded from: classes.dex */
public abstract class g implements Serializable, Cloneable {
public q f;
/* renamed from: g, reason: collision with root package name */
public long f12102g;
/* renamed from: h, reason: collision with root package name */
public long f12103h;
/* renamed from: i, reason: collision with root package name */
public boolean f12104i = false;
/* renamed from: j, reason: collision with root package name */
public long f12105j = 0;
public final void a(long j4) {
long j5;
g gVar = this;
long j6 = gVar.f12105j;
if (j6 != 0) {
long j7 = c.c;
double[][] dArr = g3.c.f11585a;
if (j4 <= j7) {
j7 = j4;
}
int i4 = (int) j7;
if (i4 > 2 && j4 >= c.f12095d) {
long j8 = j4 / i4;
Future[] futureArr = new Future[i4];
int i5 = 0;
while (i5 < i4) {
long j9 = i5 * j8;
if (i5 == i4 - 1) {
j5 = j4;
} else {
j5 = j9 + j8;
}
futureArr[i5] = c.a(new e(gVar, j9, j5, j6));
i5++;
gVar = gVar;
j6 = j6;
}
g gVar2 = gVar;
try {
c.b(futureArr);
return;
} catch (InterruptedException unused) {
r.f12109a.setMemory(gVar2.f12105j, gVar2.f12103h * j4, (byte) 0);
return;
} catch (ExecutionException unused2) {
r.f12109a.setMemory(gVar2.f12105j, j4 * gVar2.f12103h, (byte) 0);
return;
}
}
r.f12109a.setMemory(j6, gVar.f12103h * j4, (byte) 0);
}
}
public boolean equals(Object obj) {
if (obj != null && (obj instanceof g)) {
g gVar = (g) obj;
if (this.f == gVar.f && this.f12102g == gVar.f12102g && this.f12103h == gVar.f12103h && this.f12104i == gVar.f12104i && this.f12105j == gVar.f12105j) {
return true;
}
return false;
}
return false;
}
public int hashCode() {
int i4;
q qVar = this.f;
if (qVar != null) {
i4 = qVar.hashCode();
} else {
i4 = 0;
}
long j4 = this.f12102g;
int i5 = (((203 + i4) * 29) + ((int) (j4 ^ (j4 >>> 32)))) * 29;
long j5 = this.f12103h;
int i6 = (((i5 + ((int) (j5 ^ (j5 >>> 32)))) * 29) + (this.f12104i ? 1 : 0)) * 841;
long j6 = this.f12105j;
return i6 + ((int) (j6 ^ (j6 >>> 32)));
}
}
-8
View File
@@ -1,8 +0,0 @@
package k3;
/* loaded from: classes.dex */
public final class l extends q {
public l() {
super("LONG", 5);
}
}
-8
View File
@@ -1,8 +0,0 @@
package k3;
/* loaded from: classes.dex */
public final class m extends q {
public m() {
super("FLOAT", 6);
}
}
-8
View File
@@ -1,8 +0,0 @@
package k3;
/* loaded from: classes.dex */
public final class n extends q {
public n() {
super("DOUBLE", 7);
}
}
-68
View File
@@ -1,68 +0,0 @@
package k3;
/**
* R8 生成的伪 enum(k3 FFT 库的数组类型枚举)。
* smali 中 q 的直接子类:l(LONG/5)、m(FLOAT/6)、n(DOUBLE/7),
* 其余常量(BYTE/UNSIGNED_BYTE/SHORT/INT/COMPLEX_FLOAT/COMPLEX_DOUBLE/STRING/OBJECT)
* 直接以 q 实例表达。
*/
public class q {
public static final l f;
/* renamed from: g, reason: collision with root package name */
public static final m f12106g;
/* renamed from: h, reason: collision with root package name */
public static final n f12107h;
/* renamed from: i, reason: collision with root package name */
public static final /* synthetic */ q[] f12108i;
private final String name;
private final int ordinal;
protected q(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
public final String name() {
return name;
}
public final int ordinal() {
return ordinal;
}
public static q valueOf(String str) {
for (q qVar : f12108i) {
if (qVar.name.equals(str)) {
return qVar;
}
}
throw new IllegalArgumentException("No enum constant k3.q." + str);
}
public static q[] values() {
return (q[]) f12108i.clone();
}
static {
q logic = new q("LOGIC", 0);
q byteType = new q("BYTE", 1);
q unsignedByte = new q("UNSIGNED_BYTE", 2);
q shortType = new q("SHORT", 3);
q intType = new q("INT", 4);
l longType = new l();
f = longType;
m floatType = new m();
f12106g = floatType;
n doubleType = new n();
f12107h = doubleType;
q complexFloat = new q("COMPLEX_FLOAT", 8);
q complexDouble = new q("COMPLEX_DOUBLE", 9);
q string = new q("STRING", 10);
q object = new q("OBJECT", 11);
f12108i = new q[]{logic, byteType, unsignedByte, shortType, intType, longType, floatType, doubleType, complexFloat, complexDouble, string, object};
}
}
-97
View File
@@ -1,97 +0,0 @@
package k3;
import java.lang.reflect.Field;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import com.rtbishop.look4sat.feature.cw.suncompat.Unsafe;
/* loaded from: classes.dex */
public abstract class r {
/* renamed from: a, reason: collision with root package name */
public static final Unsafe f12109a;
static {
Object obj = null;
try {
Class<?> cls = Class.forName("sun.misc.Unsafe");
Field declaredField = cls.getDeclaredField("theUnsafe");
declaredField.setAccessible(true);
obj = declaredField.get(cls);
} catch (ClassNotFoundException | IllegalAccessException | IllegalArgumentException | NoSuchFieldException | SecurityException ignored) {
// Android 适配: sun.misc.Unsafe 可能不可用(隐藏 API), 静默降级为 null;
// 大数组 native 路径(j4 > 1GB)不会在实际解码中触发
}
f12109a = (Unsafe) obj;
}
public static void a(long j4, long j5, long j6, d dVar, d dVar2) {
long j7;
if (j4 >= 0 && j4 < dVar.f12102g) {
if (j5 >= 0 && j5 < dVar2.f12102g) {
if (j6 >= 0) {
if (!dVar2.f12104i) {
long j8 = c.c;
double[][] dArr = g3.c.f11585a;
if (j6 <= j8) {
j8 = j6;
}
int i4 = (int) j8;
if (i4 >= 2 && j6 >= c.f12095d) {
long j9 = j6 / i4;
Future[] futureArr = new Future[i4];
int i5 = 0;
while (i5 < i4) {
long j10 = i5 * j9;
if (i5 == i4 - 1) {
j7 = j6;
} else {
j7 = j10 + j9;
}
Future[] futureArr2 = futureArr;
int i6 = i5;
futureArr2[i6] = c.a(new j3.b(j10, j7, j5, j4, dVar2, dVar));
i5 = i6 + 1;
futureArr = futureArr2;
}
try {
c.b(futureArr);
return;
} catch (InterruptedException unused) {
long j11 = j4;
long j12 = j5;
while (j11 < j4 + j6) {
dVar2.c(j12, dVar.b(j11));
j11++;
j12++;
}
return;
} catch (ExecutionException unused2) {
long j13 = j4;
long j14 = j5;
while (j13 < j4 + j6) {
dVar2.c(j14, dVar.b(j13));
j13++;
j14++;
}
return;
}
}
long j15 = j4;
long j16 = j5;
while (j15 < j4 + j6) {
dVar2.c(j16, dVar.b(j15));
j15++;
j16++;
}
return;
}
throw new IllegalArgumentException("Constant arrays cannot be modified.");
}
throw new IllegalArgumentException("length < 0");
}
throw new ArrayIndexOutOfBoundsException("destPos < 0 || destPos >= dest.length()");
}
throw new ArrayIndexOutOfBoundsException("srcPos < 0 || srcPos >= src.length()");
}
}
-167
View File
@@ -1,167 +0,0 @@
package k3;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import com.rtbishop.look4sat.feature.cw.suncompat.Cleaner;
import com.rtbishop.look4sat.feature.cw.suncompat.Unsafe;
/* loaded from: classes.dex */
public final class s extends g {
/* renamed from: k, reason: collision with root package name */
public long[] f12110k;
public s() {
}
public s(long j4, boolean z3) {
this.f = q.f;
this.f12103h = 8L;
if (j4 > 0) {
this.f12102g = j4;
if (j4 > 1073741824) {
this.f12105j = r.f12109a.allocateMemory(8 * j4);
if (z3) {
a(j4);
}
Cleaner.create(this, new f(this.f12105j, this.f12102g, this.f12103h));
K1.a.c += this.f12102g * this.f12103h;
return;
}
this.f12110k = new long[(int) j4];
return;
}
throw new IllegalArgumentException(j4 + " is not a positive long value");
}
public final long b(long j4) {
long[] jArr = this.f12110k;
long j5 = this.f12105j;
if (j5 != 0) {
return r.f12109a.getLong((this.f12103h * j4) + j5);
}
if (this.f12104i) {
return jArr[0];
}
return jArr[(int) j4];
}
public final void c(long j4, long j5) {
long j6 = this.f12105j;
if (j6 != 0) {
r.f12109a.putLong((this.f12103h * j4) + j6, j5);
} else {
if (!this.f12104i) {
this.f12110k[(int) j4] = j5;
return;
}
throw new IllegalAccessError("Constant arrays cannot be modified.");
}
}
/* JADX WARN: Type inference failed for: r0v29, types: [k3.g, java.lang.Object, k3.s] */
public final Object clone() {
long j4;
int i4 = 0;
long j5 = 0;
if (this.f12104i) {
long j6 = this.f12102g;
long b4 = b(0L);
s gVar = new s();
gVar.f = q.f12107h;
gVar.f12103h = 8L;
if (j6 > 0) {
gVar.f12102g = j6;
gVar.f12104i = true;
gVar.f12110k = new long[]{b4};
return gVar;
}
throw new IllegalArgumentException(j6 + " is not a positive long value");
}
s sVar = new s(this.f12102g, false);
long j7 = this.f12102g;
Unsafe unsafe = r.f12109a;
if (0 < j7) {
if (0 < sVar.f12102g) {
if (j7 >= 0) {
if (!sVar.f12104i) {
long j8 = c.c;
double[][] dArr = g3.c.f11585a;
if (j7 <= j8) {
j8 = j7;
}
int i5 = (int) j8;
if (i5 >= 2 && j7 >= c.f12095d) {
long j9 = j7 / i5;
Future[] futureArr = new Future[i5];
while (i4 < i5) {
long j10 = i4 * j9;
if (i4 == i5 - 1) {
j4 = j7;
} else {
j4 = j10 + j9;
}
Future[] futureArr2 = futureArr;
int i6 = i4;
futureArr2[i6] = c.a(new i3.c(j10, j4, sVar, this));
i4 = i6 + 1;
futureArr = futureArr2;
}
try {
c.b(futureArr);
return sVar;
} catch (InterruptedException unused) {
long j11 = 0;
while (j5 < j7) {
sVar.c(j11, b(j5));
j5++;
j11++;
}
} catch (ExecutionException unused2) {
long j12 = 0;
while (j5 < j7) {
sVar.c(j12, b(j5));
j5++;
j12++;
}
}
} else {
long j13 = 0;
while (j5 < j7) {
sVar.c(j13, b(j5));
j5++;
j13++;
}
}
return sVar;
}
throw new IllegalArgumentException("Constant arrays cannot be modified.");
}
throw new IllegalArgumentException("length < 0");
}
throw new ArrayIndexOutOfBoundsException("destPos < 0 || destPos >= dest.length()");
}
throw new ArrayIndexOutOfBoundsException("srcPos < 0 || srcPos >= src.length()");
}
@Override // k3.g
public final boolean equals(Object obj) {
if (!super.equals(obj) || this.f12110k != ((s) obj).f12110k) {
return false;
}
return true;
}
@Override // k3.g
public final int hashCode() {
int i4;
int hashCode = super.hashCode() * 29;
long[] jArr = this.f12110k;
if (jArr != null) {
i4 = jArr.hashCode();
} else {
i4 = 0;
}
return hashCode + i4;
}
}
-144
View File
@@ -1,144 +0,0 @@
package pas;
import pas.system;
/* loaded from: classes.dex */
public class cwstru {
/* loaded from: classes.dex */
public static class TMorseCharInfo extends system.Record {
public TMorseCharInfo(long j4, boolean z3) {
__Init(j4, z3);
}
private native void __Destroy(long j4);
@Override // pas.system.PascalObjectEx
public void __Release() {
__Destroy(this._pasobj);
this._pasobj = 0L;
}
@Override // pas.system.Record
public int __Size() {
return __Size(0);
}
public native char getCh();
public native int getIdxE();
public native boolean getLowSnr();
public native TWordEndType getWordEnd();
public native float getWordSpaceProb();
public native TMorseWordType getWordType();
public native void setCh(char c);
public native void setIdxE(int i4);
public native void setLowSnr(boolean z3);
public native void setWordEnd(TWordEndType tWordEndType);
public native void setWordSpaceProb(float f);
public native void setWordType(TMorseWordType tMorseWordType);
public TMorseCharInfo() {
__Init(0L, true);
}
public TMorseCharInfo(system.PascalObject pascalObject) {
super(pascalObject);
}
public TMorseCharInfo(long j4) {
super(j4);
}
}
/* loaded from: classes.dex */
public static class TMorseWordType extends system.Enum {
public static final int wtCall = 5;
public static final int wtCq = 3;
public static final int wtGenWord = 0;
public static final int wtKeyword = 1;
public static final int wtQrl = 4;
public static final int wtRst = 2;
public TMorseWordType(int i4) {
this.Value = i4;
}
public static TMorseWordType wtCall() {
return new TMorseWordType(5);
}
public static TMorseWordType wtCq() {
return new TMorseWordType(3);
}
public static TMorseWordType wtGenWord() {
return new TMorseWordType(0);
}
public static TMorseWordType wtKeyword() {
return new TMorseWordType(1);
}
public static TMorseWordType wtQrl() {
return new TMorseWordType(4);
}
public static TMorseWordType wtRst() {
return new TMorseWordType(2);
}
@Override // pas.system.Enum
public boolean equals(Object obj) {
if (((obj instanceof TMorseWordType) && this.Value == ((TMorseWordType) obj).Value) || super.equals(obj)) {
return true;
}
return false;
}
}
/* loaded from: classes.dex */
public static class TWordEndType extends system.Enum {
public static final int weLong = 2;
public static final int weNone = 0;
public static final int weShort = 1;
public TWordEndType(int i4) {
this.Value = i4;
}
public static TWordEndType weLong() {
return new TWordEndType(2);
}
public static TWordEndType weNone() {
return new TWordEndType(0);
}
public static TWordEndType weShort() {
return new TWordEndType(1);
}
@Override // pas.system.Enum
public boolean equals(Object obj) {
if (((obj instanceof TWordEndType) && this.Value == ((TWordEndType) obj).Value) || super.equals(obj)) {
return true;
}
return false;
}
}
static {
system.InitJni();
}
}
-171
View File
@@ -1,171 +0,0 @@
package pas;
import pas.cwstru;
import pas.system;
/* loaded from: classes.dex */
public class decoder {
/* loaded from: classes.dex */
public static class TCharInfo extends system.TObject {
public TCharInfo(system.PascalObject pascalObject) {
super(pascalObject);
}
public static TCharInfo Class() {
return new TCharInfo(system.GetClassRef(1));
}
public static native TCharInfo Create();
public static native TCharInfo Create(char c);
public static native TCharInfo Create(cwstru.TMorseCharInfo tMorseCharInfo);
public static system.TClass TClass() {
return system.GetTClass(1);
}
public native char getChar();
public native boolean getLowSnr();
public native TMorseWordType getWordType();
public TCharInfo(long j4) {
super(j4);
}
}
/* loaded from: classes.dex */
public static class TDecoder extends system.TObject {
public TDecoder(system.PascalObject pascalObject) {
super(pascalObject);
}
public static TDecoder Class() {
return new TDecoder(system.GetClassRef(2));
}
public static native TDecoder Create();
public static system.TClass TClass() {
return system.GetTClass(2);
}
public native TCharInfo GetChar(int i4);
public native void LockToPitch(int i4);
public native int getCharCount();
public native int getDeleteCount();
public native boolean getHamMode();
public native int getPitch();
public native int getSignalCount();
public native int getSnr();
public native int getWpm();
public native void setHamMode(boolean z3);
public TDecoder(long j4) {
super(j4);
}
}
/* loaded from: classes.dex */
public static class TDecoderState extends system.Enum {
public static final int dsDecoding = 2;
public static final int dsLockedDecoding = 4;
public static final int dsLockedPause = 3;
public static final int dsNone = 0;
public static final int dsPause = 1;
public TDecoderState(int i4) {
this.Value = i4;
}
public static TDecoderState dsDecoding() {
return new TDecoderState(2);
}
public static TDecoderState dsLockedDecoding() {
return new TDecoderState(4);
}
public static TDecoderState dsLockedPause() {
return new TDecoderState(3);
}
public static TDecoderState dsNone() {
return new TDecoderState(0);
}
public static TDecoderState dsPause() {
return new TDecoderState(1);
}
@Override // pas.system.Enum
public boolean equals(Object obj) {
if (((obj instanceof TDecoderState) && this.Value == ((TDecoderState) obj).Value) || super.equals(obj)) {
return true;
}
return false;
}
}
/* loaded from: classes.dex */
public static class TMorseWordType extends system.Enum {
public static final int wtCall = 5;
public static final int wtCq = 3;
public static final int wtGenWord = 0;
public static final int wtKeyword = 1;
public static final int wtQrl = 4;
public static final int wtRst = 2;
public TMorseWordType(int i4) {
this.Value = i4;
}
public static TMorseWordType wtCall() {
return new TMorseWordType(5);
}
public static TMorseWordType wtCq() {
return new TMorseWordType(3);
}
public static TMorseWordType wtGenWord() {
return new TMorseWordType(0);
}
public static TMorseWordType wtKeyword() {
return new TMorseWordType(1);
}
public static TMorseWordType wtQrl() {
return new TMorseWordType(4);
}
public static TMorseWordType wtRst() {
return new TMorseWordType(2);
}
@Override // pas.system.Enum
public boolean equals(Object obj) {
if (((obj instanceof TMorseWordType) && this.Value == ((TMorseWordType) obj).Value) || super.equals(obj)) {
return true;
}
return false;
}
}
static {
system.InitJni();
}
}
@@ -1,35 +0,0 @@
package pas;
import pas.decoder;
import pas.system;
/* loaded from: classes.dex */
public class nativedecoder {
/* loaded from: classes.dex */
public static class TNativeDecoder extends decoder.TDecoder {
public TNativeDecoder(system.PascalObject pascalObject) {
super(pascalObject);
}
public static TNativeDecoder Class() {
return new TNativeDecoder(system.GetClassRef(3));
}
public static native TNativeDecoder Create();
public static system.TClass TClass() {
return system.GetTClass(3);
}
public native decoder.TDecoderState ProcessSpectrum(Object obj);
public TNativeDecoder(long j4) {
super(j4);
}
}
static {
system.InitJni();
}
}
-385
View File
@@ -1,385 +0,0 @@
package pas;
import java.util.Date;
import java.util.TimeZone;
/* loaded from: classes.dex */
public class system {
private static boolean _JniLibLoaded = false;
/* loaded from: classes.dex */
public static abstract class BaseSet {
protected int Value = 0;
public abstract int Base();
public abstract int ElMax();
public abstract byte Size();
}
/* loaded from: classes.dex */
public static class Enum {
public int Value;
public int Ord() {
return this.Value;
}
public boolean equals(Object obj) {
return (obj instanceof Integer) && this.Value == ((Integer) obj).intValue();
}
public int hashCode() {
return this.Value;
}
public boolean equals(int i4) {
return this.Value == i4;
}
}
/* loaded from: classes.dex */
public static class MethodPtr extends PascalObjectEx {
protected String mName;
protected Object mObject;
protected String mSignature;
public MethodPtr() {
this._cleanup = true;
this._pasobj = -1L;
}
private native void __Destroy();
private native void __Init(Object obj, String str, String str2, boolean z3);
public void Init() {
boolean z3;
Object obj = this.mObject;
String str = this.mName;
String str2 = this.mSignature;
if (this != obj) {
z3 = true;
} else {
z3 = false;
}
__Init(obj, str, str2, z3);
}
@Override // pas.system.PascalObjectEx
public void __Release() {
if (this._pasobj > 0) {
__Destroy();
}
}
}
/* loaded from: classes.dex */
public static abstract class PascalInterface extends PascalObjectEx {
public PascalInterface(long j4, boolean z3) {
this._pasobj = j4;
__Init();
}
public abstract void __Init();
public void __TypeCast(PascalObject pascalObject, String str) {
if (pascalObject != null) {
if (pascalObject instanceof PascalInterface) {
this._pasobj = pascalObject._pasobj;
__Init();
} else {
this._pasobj = system.InterfaceCast(pascalObject._pasobj, str);
}
}
}
}
/* loaded from: classes.dex */
public static class Record extends PascalObjectEx {
protected PascalObject _objref;
public Record(PascalObject pascalObject) {
super(pascalObject);
this._objref = pascalObject;
}
public void __Init(long j4, boolean z3) {
this._pasobj = j4;
this._cleanup = z3;
if (j4 == 0 && __Size() != 0) {
this._pasobj = system.AllocMemory(__Size());
}
}
public int __Size() {
return 0;
}
@Override // pas.system.PascalObjectEx, pas.system.PascalObject
public void finalize() {
long j4 = this._pasobj;
if (j4 < 0) {
this._pasobj = -j4;
this._cleanup = true;
}
super.finalize();
}
public Record(long j4) {
super(j4);
}
public final int __Size(int i4) {
return system.GetRecordSize(i4);
}
public Record() {
}
}
/* loaded from: classes.dex */
public static abstract class Set<TS extends BaseSet, TE extends Enum> extends BaseSet {
public Set() {
}
public void Assign(TS ts) {
this.Value = ts.Value;
}
public void Exclude(TE... teArr) {
for (TE te : teArr) {
this.Value = (~GetMask(te)) & this.Value;
}
}
public int GetMask(TE te) {
return 1 << (te.Ord() - Base());
}
public boolean Has(TE te) {
if ((GetMask(te) & this.Value) != 0) {
return true;
}
return false;
}
public void Include(TE... teArr) {
for (TE te : teArr) {
this.Value = GetMask(te) | this.Value;
}
}
public void Intersect(TS ts) {
this.Value = ts.Value & this.Value;
}
public boolean IsEmpty() {
if (this.Value == 0) {
return true;
}
return false;
}
public boolean equals(TS ts) {
return this.Value == ts.Value;
}
public Set(TE... teArr) {
Include(teArr);
}
public void Exclude(TS ts) {
this.Value = (~ts.Value) & this.Value;
}
public void Include(TS ts) {
this.Value = ts.Value | this.Value;
}
public boolean equals(TE te) {
return this.Value == te.Ord();
}
public Set(TS... tsArr) {
for (TS ts : tsArr) {
Include(ts); // smali: Set(TS...) 循环调 Include(BaseSet)
}
}
public boolean equals(int i4) {
return this.Value == i4;
}
}
/* loaded from: classes.dex */
public static class TClass extends TObject {
public TClass(PascalObject pascalObject) {
super(pascalObject);
}
}
/* loaded from: classes.dex */
public static class TDateTime {
public static double get(Date date) {
return ((date.getTime() + TimeZone.getDefault().getOffset(date.getTime())) / 8.64E7d) + 25569.0d;
}
public static double getUTC(Date date) {
return (date.getTime() / 8.64E7d) + 25569.0d;
}
public static Date toDate(double d4) {
return new Date(Math.round((d4 - 25569.0d) * 8.64E7d) - TimeZone.getDefault().getOffset(Math.round((d4 - 25569.0d) * 8.64E7d))); // r3 = 前值(smali)
}
public static Date toDateUTC(double d4) {
return new Date(Math.round((d4 - 25569.0d) * 8.64E7d));
}
}
/* loaded from: classes.dex */
public static class TObject extends PascalObject {
public TObject(PascalObject pascalObject) {
super(pascalObject);
}
public static TObject Class() {
return new TObject(system.GetClassRef(0));
}
public static native TObject Create();
public static TClass TClass() {
return system.GetTClass(0);
}
public native String ClassName();
public native boolean ClassNameIs(String str);
public native TClass ClassParent();
public native TClass ClassType();
public native void Destroy();
public native void Free();
public native boolean InheritsFrom(TClass tClass);
public native int InstanceSize();
public TObject(long j4) {
super(j4);
}
}
static {
InitJni();
}
public static native long AllocMemory(int i4);
public static native long GetClassRef(int i4);
public static native byte[] GetMemoryAsArray(long j4, int i4);
/* JADX INFO: Access modifiers changed from: private */
public static native int GetRecordSize(int i4);
public static TClass GetTClass(int i4) {
TClass tClass = new TClass(null);
tClass._pasobj = GetClassRef(i4);
return tClass;
}
public static void InitJni() {
if (!_JniLibLoaded) {
_JniLibLoaded = true;
System.loadLibrary("nativedecoderjni");
}
}
/* JADX INFO: Access modifiers changed from: private */
public static native long InterfaceCast(long j4, String str);
public static long Pointer(PascalObject pascalObject) {
if (pascalObject == null) {
return 0L;
}
return pascalObject._pasobj;
}
public static native void SetMemoryFromArray(long j4, byte[] bArr);
/* loaded from: classes.dex */
public static class PascalObject {
protected long _pasobj;
static {
system.InitJni();
}
public PascalObject() {
this._pasobj = 0L;
}
public boolean equals(Object obj) {
if ((obj instanceof PascalObject) && this._pasobj == ((PascalObject) obj)._pasobj) {
return true;
}
return false;
}
public void finalize() {
}
public int hashCode() {
return (int) this._pasobj;
}
public PascalObject(PascalObject pascalObject) {
this._pasobj = 0L;
if (pascalObject != null) {
this._pasobj = pascalObject._pasobj;
}
}
public PascalObject(long j4) {
this._pasobj = j4;
}
}
/* loaded from: classes.dex */
public static class PascalObjectEx extends PascalObject {
protected boolean _cleanup;
public PascalObjectEx() {
this._cleanup = false;
}
public void __Release() {
this._pasobj = 0L;
}
@Override // pas.system.PascalObject
public void finalize() {
if (this._cleanup) {
__Release();
}
super.finalize();
}
public PascalObjectEx(PascalObject pascalObject) {
super(pascalObject);
this._cleanup = false;
}
public PascalObjectEx(long j4) {
super(j4);
this._cleanup = false;
}
}
}
@@ -1,16 +0,0 @@
package s;
/* renamed from: s.e, reason: case insensitive filesystem */
/* loaded from: classes.dex */
public abstract /* synthetic */ class AbstractC1880e {
/* renamed from: a, reason: collision with root package name */
public static final /* synthetic */ int[] f13163a = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28};
public static /* synthetic */ int a(int i4) {
if (i4 != 0) {
return i4 - 1;
}
throw null;
}
}
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?attr/colorControlNormal"
android:height="24dp"
android:width="24dp"
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>
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?attr/colorControlNormal"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
</vector>
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?attr/colorControlNormal"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
</vector>
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?attr/colorControlNormal"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>
@@ -1,55 +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:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:id="@+id/verticalLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<TextView
android:textColor="#ffffff"
android:id="@+id/statusTextView"
android:background="@color/purple_500"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/idle"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="100dp">
<com.ve3nea.morse_expert.WaterfallSurfaceView
android:id="@+id/waterfallView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:contentDescription="@string/waterfall_display"/>
<com.ve3nea.morse_expert.ScaleView
android:id="@+id/scaleView"
android:background="#24545a"
android:layout_width="12dp"
android:layout_height="match_parent"
android:layout_weight="0"
android:contentDescription="@string/frequency_scale"/>
</LinearLayout>
<com.ve3nea.morse_expert.DecodedTextView
android:textSize="18sp"
android:typeface="monospace"
android:textColor="#444444"
android:textColorHint="#888888"
android:gravity="start|top"
android:id="@+id/decodedTextView"
android:background="#d1f0f4"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:hint="@string/decoded_text"
android:freezesText="false"
android:layout_weight="1"
android:inputType="none"
android:fadeScrollbars="false"
android:textIsSelectable="true"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,59 +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">
<LinearLayout
android:orientation="vertical"
android:id="@+id/verticalLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<TextView
android:textColor="#ffffff"
android:textSize="10sp"
android:singleLine="true"
android:ellipsize="end"
android:visibility="gone"
android:id="@+id/statusTextView"
android:background="@color/purple_500"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/idle"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="80dp">
<com.ve3nea.morse_expert.WaterfallSurfaceView
android:id="@+id/waterfallView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:contentDescription="@string/waterfall_display"/>
<com.ve3nea.morse_expert.ScaleView
android:id="@+id/scaleView"
android:background="#24545a"
android:layout_width="8dp"
android:layout_height="match_parent"
android:layout_weight="0"
android:contentDescription="@string/frequency_scale"/>
</LinearLayout>
<com.ve3nea.morse_expert.DecodedTextView
android:textSize="12sp"
android:typeface="monospace"
android:textColor="#444444"
android:textColorHint="#888888"
android:gravity="start|top"
android:id="@+id/decodedTextView"
android:background="#d1f0f4"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:hint="@string/decoded_text"
android:freezesText="false"
android:layout_weight="1"
android:minHeight="48dp"
android:inputType="none"
android:fadeScrollbars="false"
android:textIsSelectable="true"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:icon="@drawable/ic_baseline_pause_24"
android:id="@+id/pause_mnu"
android:title="@string/pause"
app:showAsAction="ifRoom"/>
<item
android:icon="@drawable/ic_baseline_delete_24"
android:id="@+id/clear_mnu"
android:title="@string/clear"
app:showAsAction="ifRoom"/>
<item
android:icon="@drawable/ic_baseline_save_24"
android:id="@+id/save_mnu"
android:title="@string/save_text"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/record_signals_mnu"
android:title="@string/record_signals"/>
<item
android:id="@+id/settings_mnu"
android:title="@string/settings"/>
<item
android:id="@+id/help_mnu"
android:title="@string/help"/>
<item
android:id="@+id/rate_mnu"
android:title="@string/rate"/>
<item
android:id="@+id/premium_mnu"
android:title="@string/premium"/>
</menu>
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_500">#ff6200ee</color>
<integer name="default_font_size">18</integer>
<string name="idle">Idle</string>
<string name="waterfall_display">Waterfall display</string>
<string name="frequency_scale">Frequency scale</string>
<string name="decoded_text">Decoded text</string>
<string name="clear">Clear</string>
<string name="pause">Pause Decoding</string>
<string name="save_text">Save Text</string>
<string name="record_signals">Record Signals</string>
<string name="settings">Settings</string>
<string name="help">Online Help</string>
<string name="rate">Rate this App</string>
<string name="premium">Get Premium Version</string>
<string name="tap_back_again_to_close">Tap Back again to close the app</string>
<string name="message_type">Message Type</string>
</resources>
@@ -24,7 +24,6 @@ import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.ui.res.stringResource
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -41,7 +40,6 @@ import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.rtbishop.look4sat.core.presentation.R
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -228,8 +226,8 @@ fun ElevationCurveChart(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Text(stringResource(R.string.mutual_station_a), color = colorA, fontSize = 12.sp)
Text(stringResource(R.string.mutual_station_b), color = colorB, fontSize = 12.sp)
Text("● 站点A", color = colorA, fontSize = 12.sp)
Text("● 站点B", color = colorB, fontSize = 12.sp)
}
}
}
@@ -57,8 +57,8 @@ private const val STROKE_WIDTH = 2.5f
fun MutualRadarView(
trackSamples: List<TrackSample>,
progress: Float = 0.5f,
labelA: String = "Station A",
labelB: String = "Station B",
labelA: String = "站点A",
labelB: String = "站点B",
modifier: Modifier = Modifier
) {
if (trackSamples.isEmpty()) return
@@ -44,7 +44,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -58,7 +57,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.rtbishop.look4sat.core.domain.repository.MutualPassData
@@ -90,35 +88,28 @@ fun MutualScreen(
},
topInfo = {
Text(
text = stringResource(R.string.mutual_title),
text = "过境匹配",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
},
bottomInfo = {
Text(
text = mutualStatusText(state),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
if (state.mutualPasses.isNotEmpty()) {
Text(
text = "找到 ${state.mutualPasses.size} 个匹配",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
endAction = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
if (state.isCalculating) {
CircularProgressIndicator(
modifier = Modifier
.height(24.dp)
.width(24.dp),
strokeWidth = 2.dp
)
}
MutualStatusChip(state)
if (state.isCalculating) {
CircularProgressIndicator(
modifier = Modifier
.height(24.dp)
.width(24.dp),
strokeWidth = 2.dp
)
}
}
)
@@ -196,9 +187,11 @@ private fun MutualContent(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
MatchIntroCard()
if (isVertical) {
StationInputCard(
title = stringResource(R.string.mutual_station_you),
title = "你的位置",
titleColor = MaterialTheme.colorScheme.primary,
lat = state.stationALat,
lon = state.stationALon,
@@ -214,7 +207,7 @@ private fun MutualContent(
currentPositionAction = onUseCurrentPosition
)
StationInputCard(
title = stringResource(R.string.mutual_station_opposite),
title = "友台位置",
titleColor = MaterialTheme.colorScheme.tertiary,
lat = state.stationBLat,
lon = state.stationBLon,
@@ -234,7 +227,7 @@ private fun MutualContent(
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
StationInputCard(
title = stringResource(R.string.mutual_station_you),
title = "你的位置",
titleColor = MaterialTheme.colorScheme.primary,
lat = state.stationALat,
lon = state.stationALon,
@@ -251,7 +244,7 @@ private fun MutualContent(
modifier = Modifier.weight(1f)
)
StationInputCard(
title = stringResource(R.string.mutual_station_opposite),
title = "友台位置",
titleColor = MaterialTheme.colorScheme.tertiary,
lat = state.stationBLat,
lon = state.stationBLon,
@@ -294,8 +287,8 @@ private fun MutualContent(
startTime = pass.startTime,
endTime = pass.endTime,
maxElev = maxOf(pass.maxElevationA, pass.maxElevationB, 10.0),
labelA = stringResource(R.string.mutual_label_you),
labelB = stringResource(R.string.mutual_label_opposite)
labelA = "你",
labelB = "友台"
)
MutualPassCard(
pass = pass,
@@ -311,52 +304,25 @@ private fun MutualContent(
}
@Composable
private fun MutualStatusChip(state: MutualUiState) {
val colorScheme = MaterialTheme.colorScheme
val containerColor = when {
state.isCalculating -> colorScheme.primaryContainer
state.errorMessage != null -> colorScheme.errorContainer
state.mutualPasses.isNotEmpty() -> colorScheme.primary
state.hasSearched -> colorScheme.surfaceVariant
else -> colorScheme.surfaceVariant
private fun MatchIntroCard(modifier: Modifier = Modifier) {
ElevatedCard(modifier = modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "位置匹配",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary
)
Text(
text = "输入双方位置,查找共同可见的卫星过境窗口",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
val contentColor = when {
state.isCalculating -> colorScheme.onPrimaryContainer
state.errorMessage != null -> colorScheme.onErrorContainer
state.mutualPasses.isNotEmpty() -> colorScheme.onPrimary
else -> colorScheme.onSurfaceVariant
}
Surface(
color = containerColor,
contentColor = contentColor,
shape = MaterialTheme.shapes.small
) {
Text(
text = mutualStatusChipText(state),
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Medium,
maxLines = 1,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp)
)
}
}
@Composable
private fun mutualStatusText(state: MutualUiState): String = when {
state.isCalculating -> stringResource(R.string.mutual_status_calculating)
state.errorMessage != null -> stringResource(R.string.mutual_status_error)
state.mutualPasses.isNotEmpty() -> stringResource(R.string.mutual_status_matches, state.mutualPasses.size)
state.hasSearched -> stringResource(R.string.mutual_status_none)
else -> stringResource(R.string.mutual_status_idle)
}
@Composable
private fun mutualStatusChipText(state: MutualUiState): String = when {
state.isCalculating -> stringResource(R.string.mutual_chip_calculating)
state.errorMessage != null -> stringResource(R.string.mutual_chip_error)
state.mutualPasses.isNotEmpty() -> stringResource(R.string.mutual_chip_matches, state.mutualPasses.size)
state.hasSearched -> stringResource(R.string.mutual_chip_none)
else -> stringResource(R.string.mutual_chip_idle)
}
@Composable
@@ -395,7 +361,7 @@ private fun StationInputCard(
)
if (currentPositionAction != null) {
OutlinedButton(onClick = currentPositionAction) {
Text(stringResource(R.string.mutual_use_current), style = MaterialTheme.typography.bodySmall)
Text("填入当前位置", style = MaterialTheme.typography.bodySmall)
}
}
}
@@ -407,7 +373,7 @@ private fun StationInputCard(
OutlinedTextField(
value = lat,
onValueChange = onLatChange,
label = { Text(stringResource(R.string.mutual_lat)) },
label = { Text("纬度") },
placeholder = { Text(latPlaceholder) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
singleLine = true,
@@ -416,7 +382,7 @@ private fun StationInputCard(
OutlinedTextField(
value = lon,
onValueChange = onLonChange,
label = { Text(stringResource(R.string.mutual_lon)) },
label = { Text("经度") },
placeholder = { Text(lonPlaceholder) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
singleLine = true,
@@ -427,15 +393,15 @@ private fun StationInputCard(
OutlinedTextField(
value = grid,
onValueChange = onGridChange,
label = { Text(stringResource(R.string.mutual_grid)) },
label = { Text("网格") },
placeholder = { Text(gridPlaceholder) },
supportingText = { Text(stringResource(R.string.mutual_grid_hint)) },
supportingText = { Text("支持 4/6/8 位 Maidenhead 网格;也可直接输入经纬度") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Text(
text = stringResource(R.string.mutual_min_elevation, minElev.toInt()),
text = "最小仰角:${minElev.toInt()}°",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -463,7 +429,7 @@ private fun MatchSearchCard(
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(
text = stringResource(R.string.mutual_time_range),
text = "时间范围",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Medium
)
@@ -494,7 +460,7 @@ private fun MatchSearchCard(
)
Spacer(Modifier.width(8.dp))
}
Text(if (isCalculating) stringResource(R.string.mutual_calculating) else stringResource(R.string.mutual_query))
Text(if (isCalculating) "计算中..." else "查询匹配")
}
}
}
@@ -560,12 +526,12 @@ private fun MutualPassCard(
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = stringResource(R.string.mutual_elevation_you, pass.maxElevationA.toInt()),
text = "你: ${pass.maxElevationA}°",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
Text(
text = stringResource(R.string.mutual_elevation_opposite, pass.maxElevationB.toInt()),
text = "友台: ${pass.maxElevationB}°",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.tertiary
)
@@ -581,7 +547,7 @@ private fun MutualPassCard(
// Dual-station radar track (polar plot)
if (visibleTracks.isNotEmpty()) {
Text(
text = stringResource(R.string.mutual_both_tracks),
text = "双方轨迹",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurfaceVariant
@@ -589,8 +555,8 @@ private fun MutualPassCard(
MutualRadarView(
trackSamples = visibleTracks,
progress = dragProgress,
labelA = stringResource(R.string.mutual_label_you),
labelB = stringResource(R.string.mutual_label_opposite)
labelA = "你",
labelB = "友台"
)
Spacer(Modifier.height(8.dp))
}
@@ -615,7 +581,7 @@ private fun MutualPassCard(
modifier = Modifier.height(16.dp).width(16.dp)
)
Spacer(Modifier.width(6.dp))
Text(stringResource(R.string.mutual_open_radar))
Text("查看雷达")
}
}
}
@@ -49,7 +49,6 @@ data class MutualUiState(
val hoursAhead: Int = 24,
val mutualPasses: List<MutualPass> = emptyList(),
val isCalculating: Boolean = false,
val hasSearched: Boolean = false,
val selectedPassIndex: Int = -1,
val errorMessage: String? = null
)
@@ -158,25 +157,17 @@ class MutualViewModel(
var posB = resolvePosition(state.stationBLat, state.stationBLon, state.stationBGrid)
if (posA == null || posB == null) {
_uiState.update { it.copy(errorMessage = "Please enter valid coordinates or a Maidenhead grid (4/6/8 chars)") }
_uiState.update { it.copy(errorMessage = "请输入有效的位置坐标或网格(4/6/8位)") }
return
}
val satellites = satelliteRepo.satellites.value
if (satellites.isEmpty()) {
_uiState.update { it.copy(errorMessage = "No satellite data. Select satellites from the list first.") }
_uiState.update { it.copy(errorMessage = "没有卫星数据,请先在卫星列表中选择卫星") }
return
}
_uiState.update {
it.copy(
isCalculating = true,
hasSearched = true,
errorMessage = null,
mutualPasses = emptyList(),
selectedPassIndex = -1
)
}
_uiState.update { it.copy(isCalculating = true, errorMessage = null, mutualPasses = emptyList()) }
viewModelScope.launch {
val time = System.currentTimeMillis()
@@ -193,9 +184,9 @@ class MutualViewModel(
val passCount = satelliteRepo.passes.value.size
val satCount = satellites.size
when {
passCount == 0 -> "Pass list is empty. Select satellites from the list first."
satCount == 0 -> "No satellites selected."
else -> "No mutual passes found (${passCount} passes, ${satCount} satellites)."
passCount == 0 -> "过境列表为空,请先在卫星列表中选择卫星"
satCount == 0 -> "没有已选中的卫星"
else -> "未找到共同过境(${passCount}个过境,${satCount}颗卫星)"
}
} else null
@@ -38,7 +38,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.TimeZone
@@ -163,10 +162,7 @@ class PassesViewModel(
return SimpleDateFormat(pattern, Locale.getDefault()).also { it.timeZone = tz }
}
// Computes sunrise/sunset strings for each unique calendar day in the pass list, plus today for DeepSpace.
// CelestialComputer.findSunRiseSet() returns the next rise/set after the supplied time, so pass
// the calendar day's 00:00 in the same timezone used by the visible date group. Passing a pass's
// AOS time can jump to the following day's sunrise/sunset when that AOS is after the local event.
// Computes sunrise/sunset strings for each unique calendar day in the pass list, plus today for DeepSpace
private fun computeSunTimes(passes: List<OrbitalPass>, isUtc: Boolean): Map<String, Pair<String, String>> {
val stationPos = settingsRepo.stationPosition.value
val tz = if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault()
@@ -175,7 +171,7 @@ class PassesViewModel(
val result = LinkedHashMap<String, Pair<String, String>>()
// DeepSpace group always shows today's sun times
if (passes.any { it.isDeepSpace }) {
val riseSet = CelestialComputer.findSunRiseSet(stationPos, startOfDayMillis(System.currentTimeMillis(), tz))
val riseSet = CelestialComputer.findSunRiseSet(stationPos, System.currentTimeMillis())
val rise = if (riseSet.riseTimeMillis > 0) sdfTime.format(Date(riseSet.riseTimeMillis)) else "--:--"
val set = if (riseSet.setTimeMillis > 0) sdfTime.format(Date(riseSet.setTimeMillis)) else "--:--"
result["DeepSpace (period >225min)"] = rise to set
@@ -184,7 +180,7 @@ class PassesViewModel(
if (pass.isDeepSpace) continue
val label = sdfDate.format(Date(pass.aosTime))
if (label in result) continue
val riseSet = CelestialComputer.findSunRiseSet(stationPos, startOfDayMillis(pass.aosTime, tz))
val riseSet = CelestialComputer.findSunRiseSet(stationPos, pass.aosTime)
val rise = if (riseSet.riseTimeMillis > 0) sdfTime.format(Date(riseSet.riseTimeMillis)) else "--:--"
val set = if (riseSet.setTimeMillis > 0) sdfTime.format(Date(riseSet.setTimeMillis)) else "--:--"
result[label] = rise to set
@@ -192,16 +188,6 @@ class PassesViewModel(
return result
}
private fun startOfDayMillis(timeMillis: Long, tz: TimeZone): Long {
return Calendar.getInstance(tz, Locale.getDefault()).apply {
timeInMillis = timeMillis
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
}
private fun groupPasses(passes: List<OrbitalPass>, isUtc: Boolean): Map<String, List<OrbitalPass>> {
val tz = if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault()
val sdfDate = dateFormat(tz)
-2
View File
@@ -8,6 +8,4 @@ android {
dependencies {
implementation(project(":feature:mutual"))
implementation(project(":feature:cw"))
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
}
@@ -61,7 +61,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
import com.rtbishop.look4sat.core.domain.repository.MutualPassData
import com.rtbishop.look4sat.core.domain.utility.DopplerFrequencyCalculator
import com.rtbishop.look4sat.core.domain.utility.toDegrees
import com.rtbishop.look4sat.core.presentation.EmptyListCard
import com.rtbishop.look4sat.core.presentation.IconCard
@@ -79,7 +78,6 @@ import kotlin.math.PI
private enum class RadarPage(val title: String) {
Transceivers("Transceivers"),
Calculator("Calculator"),
Sstv("SSTV")
}
@@ -203,31 +201,16 @@ private fun PagerCard(
requestMicPermission: () -> Unit,
modifier: Modifier = Modifier
) {
val hasCalculatorPage = remember(uiState.transceivers.transmitters) {
uiState.transceivers.transmitters.any(DopplerFrequencyCalculator::isNamedLinearTransponder)
}
val pages = remember(hasCalculatorPage) {
buildList {
add(RadarPage.Transceivers)
if (hasCalculatorPage) add(RadarPage.Calculator)
add(RadarPage.Sstv)
}
}
val pages = RadarPage.entries
val pagerState = rememberPagerState(pageCount = { pages.size })
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(pages.size) {
val lastPage = pages.lastIndex
if (pagerState.currentPage > lastPage) pagerState.scrollToPage(lastPage)
}
ElevatedCard(modifier = modifier) {
Column(modifier = Modifier.fillMaxSize()) {
val selectedTabIndex = pagerState.currentPage.coerceIn(0, pages.lastIndex)
PrimaryTabRow(selectedTabIndex = selectedTabIndex) {
PrimaryTabRow(selectedTabIndex = pagerState.currentPage) {
pages.forEachIndexed { index, page ->
Tab(
selected = selectedTabIndex == index,
selected = pagerState.currentPage == index,
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } },
text = { Text(text = page.title, maxLines = 1, overflow = TextOverflow.Ellipsis) }
)
@@ -239,19 +222,14 @@ private fun PagerCard(
) { pageIndex ->
when (pages[pageIndex]) {
RadarPage.Transceivers -> TransceiversPage(
transceivers = uiState.transceivers.transmitters,
selectedUuid = uiState.transceivers.selectedUuid,
radioControl = uiState.radioControl,
onAction = onAction
)
RadarPage.Calculator -> CalculatorPage(
transceivers = uiState.transceivers.transmitters,
selectedUuid = uiState.transceivers.selectedUuid,
orbitalPos = uiState.orbitalPos,
cw = uiState.cw,
onAction = onAction,
requestMicPermission = requestMicPermission
)
transceivers = uiState.transceivers.transmitters,
selectedUuid = uiState.transceivers.selectedUuid,
orbitalPos = uiState.orbitalPos,
cw = uiState.cw,
radioControl = uiState.radioControl,
onAction = onAction,
requestMicPermission = requestMicPermission
)
RadarPage.Sstv -> SstvPage(
sstv = uiState.sstv,
dopplerFrequency = uiState.transceivers.selectedFrequency?.let { formatFrequency(it) },
@@ -94,14 +94,21 @@ fun RadarViewCompose(
animationSpec = infiniteRepeatable(tween(1000)),
label = "animScale"
)
// Drive the sweep from the animation framework to eliminate state mutation inside the draw block
val sweepTransition = rememberInfiniteTransition(label = "sweep")
val sweepDegrees by sweepTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(tween(SWEEP_DURATION_MS, easing = LinearEasing)),
label = "sweepDegrees"
)
// Drive the sweep from the animation framework to eliminate state mutation inside the draw block.
// Only create the infinite transition when the sweep is enabled — otherwise the canvas would
// be invalidated every frame for an effect that isn't visible, wasting GPU and battery.
val sweepDegrees = if (shouldShowSweep) {
val sweepTransition = rememberInfiniteTransition(label = "sweep")
val animatedSweep by sweepTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(tween(SWEEP_DURATION_MS, easing = LinearEasing)),
label = "sweepDegrees"
)
animatedSweep
} else {
0f
}
val measurer = rememberTextMeasurer()
val sunPainter = painterResource(R.drawable.ic_sun)
val moonPainter = painterResource(R.drawable.ic_moon)
@@ -181,7 +181,7 @@ class RadarViewModel(
orbitalPos = pos, sunPosition = cachedSunPos, moonPosition = cachedMoonPos
)
}
processRadios(allRadios, pass.orbitalObject, timeNow)
processRadios(allRadios, pos)
sendPassData(pos)
}
@@ -342,8 +342,8 @@ class RadarViewModel(
}
}
private suspend fun processRadios(radios: List<SatRadio>, orbitalObject: OrbitalObject, time: Long) {
val transmitters = satelliteRepo.getRadios(orbitalObject, stationPos, radios, time)
private suspend fun processRadios(radios: List<SatRadio>, satPos: OrbitalPos) {
val transmitters = satelliteRepo.getRadios(satPos, radios)
_uiState.update { state ->
val freq = if (state.transceivers.selectedUuid != null) {
val selectedRadio = transmitters.firstOrNull { it.uuid == state.transceivers.selectedUuid }
@@ -17,24 +17,18 @@
*/
package com.rtbishop.look4sat.feature.radar
import android.app.Activity
import android.view.LayoutInflater
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -45,57 +39,42 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.FilterChip
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.CardDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
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.draw.drawBehind
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset
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.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.constraintlayout.widget.ConstraintLayout
import com.rtbishop.look4sat.core.domain.model.SatRadio
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
import com.rtbishop.look4sat.core.domain.utility.DopplerFrequencyCalculator
import com.rtbishop.look4sat.core.domain.utility.TransponderMapper
import com.rtbishop.look4sat.core.presentation.CardButton
import com.rtbishop.look4sat.core.presentation.R
import com.rtbishop.look4sat.core.presentation.formatFrequency
import com.rtbishop.look4sat.core.presentation.infiniteMarquee
import com.rtbishop.look4sat.feature.cw.R as CwR
import com.ve3nea.morse_expert.MainActivity
import java.util.Locale
import kotlin.time.Duration.Companion.milliseconds
@@ -103,8 +82,11 @@ import kotlin.time.Duration.Companion.milliseconds
fun TransceiversPage(
transceivers: List<SatRadio>,
selectedUuid: String?,
orbitalPos: OrbitalPos?,
cw: CwSubState,
radioControl: RadioControlSubState,
onAction: (RadarAction) -> Unit,
requestMicPermission: () -> Unit = {},
modifier: Modifier = Modifier
) {
if (transceivers.isEmpty()) {
@@ -127,8 +109,11 @@ fun TransceiversPage(
TransceiverItem(
radio = radio,
isExpanded = isExpanded,
orbitalPos = orbitalPos,
cw = cw,
radioControl = radioControl,
onAction = onAction,
requestMicPermission = requestMicPermission,
onToggle = { onAction(RadarAction.SelectTransmitter(radio.uuid)) }
)
}
@@ -136,85 +121,6 @@ fun TransceiversPage(
}
}
@Composable
fun CalculatorPage(
transceivers: List<SatRadio>,
selectedUuid: String?,
orbitalPos: OrbitalPos?,
cw: CwSubState,
onAction: (RadarAction) -> Unit,
requestMicPermission: () -> Unit = {},
modifier: Modifier = Modifier
) {
val calculatorTransceivers = remember(transceivers) {
transceivers.filter(DopplerFrequencyCalculator::isNamedLinearTransponder)
}
val selectedTransceiver = remember(calculatorTransceivers, selectedUuid) {
calculatorTransceivers.firstOrNull { it.uuid == selectedUuid }
?: calculatorTransceivers.firstOrNull()
}
if (selectedTransceiver == null) {
EmptyTransceiversContent(modifier)
return
}
LazyColumn(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
if (calculatorTransceivers.size > 1) {
item {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.fillMaxWidth()
) {
calculatorTransceivers.forEach { radio ->
FilterChip(
selected = radio.uuid == selectedTransceiver.uuid,
onClick = {
if (radio.uuid != selectedUuid) onAction(RadarAction.SelectTransmitter(radio.uuid))
},
label = {
Text(
text = transceiverTitle(radio),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
}
}
}
}
item {
DopplerFrequencyCalculator(
transponder = selectedTransceiver,
orbitalPos = orbitalPos,
modifier = Modifier.fillMaxWidth()
)
}
item {
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
)
}
item {
CwDecoderPanel(
cw = cw,
onAction = onAction,
requestMicPermission = requestMicPermission,
modifier = Modifier.fillMaxWidth()
)
}
}
}
@Composable
private fun EmptyTransceiversContent(modifier: Modifier = Modifier) {
Box(contentAlignment = Alignment.Center, modifier = modifier.fillMaxSize()) {
@@ -242,8 +148,11 @@ private fun EmptyTransceiversContent(modifier: Modifier = Modifier) {
private fun TransceiverItem(
radio: SatRadio,
isExpanded: Boolean,
orbitalPos: OrbitalPos?,
cw: CwSubState,
radioControl: RadioControlSubState,
onAction: (RadarAction) -> Unit,
requestMicPermission: () -> Unit,
onToggle: () -> Unit
) {
val bgColor = if (isExpanded) MaterialTheme.colorScheme.surfaceContainerHighest
@@ -284,8 +193,11 @@ private fun TransceiverItem(
)
}
// Title with mode
val title = if (radio.isInverted) "INV: ${radio.info}" else radio.info
val mode = "${radio.downlinkMode ?: "--"}/${radio.uplinkMode ?: "--"}"
val fullTitle = "$title ($mode)"
Text(
text = transceiverTitle(radio),
text = fullTitle,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
@@ -333,8 +245,11 @@ private fun TransceiverItem(
) {
ExpandedRadioControl(
radio = radio,
orbitalPos = orbitalPos,
cw = cw,
radioControl = radioControl,
onAction = onAction
onAction = onAction,
requestMicPermission = requestMicPermission
)
}
@@ -403,8 +318,11 @@ private fun UnifiedFrequencyRow(
@Composable
private fun ExpandedRadioControl(
radio: SatRadio,
orbitalPos: OrbitalPos?,
cw: CwSubState,
radioControl: RadioControlSubState,
onAction: (RadarAction) -> Unit,
requestMicPermission: () -> Unit = {},
) {
Column(
modifier = Modifier
@@ -513,6 +431,23 @@ private fun ExpandedRadioControl(
}
}
// Doppler frequency calculator (linear transponders only)
DopplerFrequencyCalculator(
transponder = radio,
orbitalPos = orbitalPos,
modifier = Modifier.fillMaxWidth()
)
// CW decoder panel (linear transponders only)
if (DopplerFrequencyCalculator.isLinearTransponder(radio)) {
CwDecoderPanel(
cw = cw,
onAction = onAction,
requestMicPermission = requestMicPermission,
modifier = Modifier.fillMaxWidth()
)
}
// Control buttons
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
if (!radioControl.txPanel.isConnected && !radioControl.rxPanel.isConnected) {
@@ -544,8 +479,6 @@ private fun ExpandedRadioControl(
}
}
private enum class EditedField { TX, PASSBAND, RX }
@Composable
private fun DopplerFrequencyCalculator(
transponder: SatRadio,
@@ -554,351 +487,144 @@ private fun DopplerFrequencyCalculator(
) {
if (orbitalPos == null || !DopplerFrequencyCalculator.isLinearTransponder(transponder)) return
var lastEditedField by remember { mutableStateOf(EditedField.TX) }
var txFrequencyHz by remember { mutableStateOf(0L) }
var rxFrequencyHz by remember { mutableStateOf(0L) }
var passbandPosition by remember { mutableStateOf(0.5f) }
var txInputMHz by remember { mutableStateOf("") }
var rxInputMHz by remember { mutableStateOf("") }
var offsetKHz by remember { mutableStateOf("") }
var stepSizeKHz by remember { mutableIntStateOf(1) }
var lastEditedBy by remember { mutableStateOf(EditedField.TX) }
val offsetHz = offsetKHz.toDoubleOrNull()?.let { it * 1000 }?.toLong() ?: 0L
val txLow = transponder.uplinkLow ?: return
val txHigh = transponder.uplinkHigh ?: return
val rxLow = transponder.downlinkLow ?: return
val rxHigh = transponder.downlinkHigh ?: return
val txRange = txHigh - txLow
val rxRange = rxHigh - rxLow
if (txRange <= 0L || rxRange <= 0L) return
// 原始转发器值(逆转多普勒),用于 passband 映射计算
val rawTxLow = orbitalPos.getDownlinkFreq(txLow)
val rawTxHigh = orbitalPos.getDownlinkFreq(txHigh)
val rawRxLow = orbitalPos.getUplinkFreq(rxLow)
val rawRxHigh = orbitalPos.getUplinkFreq(rxHigh)
val rawTxRange = rawTxHigh - rawTxLow
val rawRxRange = rawRxHigh - rawRxLow
val rawTransponder = transponder.copy(
uplinkLow = rawTxLow, uplinkHigh = rawTxHigh,
downlinkLow = rawRxLow, downlinkHigh = rawRxHigh
)
// 初始化
LaunchedEffect(Unit) {
if (txFrequencyHz == 0L) {
txFrequencyHz = txLow + txRange / 2
rxFrequencyHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
txFrequencyHz, rawTransponder, orbitalPos, offsetHz
) ?: (rxLow + rxRange / 2)
}
}
// 实时重算:卫星移动时,锚点侧不变,重算另一侧
// Real-time refresh: when orbitalPos changes (1Hz), recompute the opposite field
LaunchedEffect(orbitalPos) {
if (lastEditedField == EditedField.TX) {
// TX 是锚点,不变;重算 RX
rxFrequencyHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
txFrequencyHz, rawTransponder, orbitalPos, offsetHz
) ?: rxFrequencyHz
} else if (lastEditedField == EditedField.RX) {
// RX 是锚点,不变;重算 TX
txFrequencyHz = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
rxFrequencyHz, rawTransponder, orbitalPos, offsetHz
) ?: txFrequencyHz
} else if (lastEditedField == EditedField.PASSBAND) {
// 相对位置不变(passbandPosition),TX 用地面频率,RX 经 Transceiver 完整路径计算
txFrequencyHz = txLow + (passbandPosition * txRange).toLong()
rxFrequencyHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
txFrequencyHz, rawTransponder, orbitalPos, offsetHz
) ?: rxFrequencyHz
}
}
val txSliderValue = if (lastEditedField == EditedField.PASSBAND) {
passbandPosition
} else {
((txFrequencyHz - txLow).toFloat() / txRange).coerceIn(0f, 1f)
}
val rxSliderValue = if (lastEditedField == EditedField.PASSBAND) {
((rxFrequencyHz - rxLow).toFloat() / rxRange).coerceIn(0f, 1f)
} else {
((rxFrequencyHz - rxLow).toFloat() / rxRange).coerceIn(0f, 1f)
}
fun updateTx(newTxHz: Long) {
when (lastEditedField) {
EditedField.PASSBAND -> {
// Passband 模式:滑块位置 → passbandPosition → 地面频率,RX 经 Transceiver 计算
val pos = ((newTxHz - txLow).toFloat() / txRange).coerceIn(0f, 1f)
passbandPosition = pos
txFrequencyHz = txLow + (pos * txRange).toLong()
rxFrequencyHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
txFrequencyHz, rawTransponder, orbitalPos, offsetHz
) ?: rxFrequencyHz
if (lastEditedBy == EditedField.TX) {
val txMHz = txInputMHz.toDoubleOrNull()
if (txMHz != null && txMHz > 0) {
val txHz = (txMHz * 1_000_000).toLong()
val rxHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
txHz, transponder, orbitalPos, offsetHz
)
if (rxHz != null) {
rxInputMHz = String.format(Locale.ENGLISH, "%.6f", rxHz / 1_000_000.0)
}
}
else -> {
txFrequencyHz = newTxHz.coerceIn(txLow, txHigh)
rxFrequencyHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
txFrequencyHz, rawTransponder, orbitalPos, offsetHz
) ?: rxFrequencyHz
}
}
}
fun updateRx(newRxHz: Long) {
when (lastEditedField) {
EditedField.PASSBAND -> {
// Passband 模式:从 RX 反推 TX 位置,TX 用地面频率
val pos = ((newRxHz - rxLow).toFloat() / rxRange).coerceIn(0f, 1f)
val txFromRx = TransponderMapper.mapDownlinkToUplink(newRxHz, rawTransponder)
passbandPosition = if (txFromRx != null) {
((txFromRx - rawTxLow).toFloat() / rawTxRange).coerceIn(0f, 1f)
} else pos
txFrequencyHz = txLow + (passbandPosition * txRange).toLong()
rxFrequencyHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
txFrequencyHz, rawTransponder, orbitalPos, offsetHz
) ?: rxFrequencyHz
}
else -> {
rxFrequencyHz = newRxHz.coerceIn(rxLow, rxHigh)
txFrequencyHz = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
rxFrequencyHz, rawTransponder, orbitalPos, offsetHz
) ?: txFrequencyHz
} else {
val rxMHz = rxInputMHz.toDoubleOrNull()
if (rxMHz != null && rxMHz > 0) {
val rxHz = (rxMHz * 1_000_000).toLong()
val txHz = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
rxHz, transponder, orbitalPos, offsetHz
)
if (txHz != null) {
txInputMHz = String.format(Locale.ENGLISH, "%.6f", txHz / 1_000_000.0)
}
}
}
}
Column(
modifier = modifier.fillMaxWidth().padding(top = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Row(
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
// 左半边: TX + RX 按钮
Row(
modifier = Modifier.weight(1f),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
FilterChip(
selected = lastEditedField == EditedField.TX,
onClick = {
when (lastEditedField) {
EditedField.TX -> {
passbandPosition = ((txFrequencyHz - txLow).toFloat() / txRange).coerceIn(0f, 1f)
lastEditedField = EditedField.PASSBAND
rxFrequencyHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
txFrequencyHz, rawTransponder, orbitalPos, offsetHz
) ?: rxFrequencyHz
}
EditedField.PASSBAND -> { lastEditedField = EditedField.TX }
else -> { lastEditedField = EditedField.TX }
}
},
label = {
Text("TX", fontSize = 13.sp, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth())
},
modifier = Modifier.weight(1f).fillMaxHeight()
)
FilterChip(
selected = lastEditedField == EditedField.RX,
onClick = {
when (lastEditedField) {
EditedField.RX -> {
txFrequencyHz = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
rxFrequencyHz, rawTransponder, orbitalPos, offsetHz
) ?: txFrequencyHz
passbandPosition = ((txFrequencyHz - txLow).toFloat() / txRange).coerceIn(0f, 1f)
lastEditedField = EditedField.PASSBAND
}
EditedField.PASSBAND -> {
lastEditedField = EditedField.RX
txFrequencyHz = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
rxFrequencyHz, rawTransponder, orbitalPos, offsetHz
) ?: txFrequencyHz
}
else -> { lastEditedField = EditedField.RX }
}
},
label = {
Text("RX", fontSize = 13.sp, textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth())
},
modifier = Modifier.weight(1f).fillMaxHeight()
)
}
// 右半边: Offset 输入框(带细线边框)
Box(
modifier = Modifier
.weight(1f)
.height(48.dp)
.border(1.dp, MaterialTheme.colorScheme.outline, MaterialTheme.shapes.small),
contentAlignment = Alignment.Center
) {
BasicTextField(
value = offsetKHz,
onValueChange = { offsetKHz = it },
singleLine = true,
textStyle = TextStyle(
fontSize = 16.sp,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurface
),
decorationBox = { innerTextField ->
if (offsetKHz.isEmpty()) {
Text(
text = "Offset (kHz)",
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
innerTextField()
}
)
}
}
Text(
text = stringResource(R.string.radar_doppler_calc),
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary
)
Text(
text = stringResource(R.string.radar_doppler_info),
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
// TX 行
Column(modifier = Modifier.fillMaxWidth()) {
Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedButton(
onClick = { updateTx(txFrequencyHz - stepSizeKHz * 1000L) },
modifier = Modifier.width(32.dp).height(28.dp),
contentPadding = PaddingValues(0.dp),
shape = MaterialTheme.shapes.extraSmall
) { Text("−", fontSize = 14.sp) }
Slider(
value = txSliderValue,
onValueChange = { norm ->
val rawFreq = txLow + (txRange * norm).toLong()
val snapped = ((rawFreq + 500) / 1000L) * 1000L
updateTx(snapped.coerceIn(txLow, txHigh))
},
valueRange = 0f..1f,
modifier = Modifier.weight(1f).height(20.dp).padding(horizontal = 4.dp)
)
OutlinedButton(
onClick = { updateTx(txFrequencyHz + stepSizeKHz * 1000L) },
modifier = Modifier.width(32.dp).height(28.dp),
contentPadding = PaddingValues(0.dp),
shape = MaterialTheme.shapes.extraSmall
) { Text("+", fontSize = 14.sp) }
}
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = String.format(Locale.ENGLISH, "%.4f", txLow / 1_000_000.0),
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.weight(1f)
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Text(
text = String.format(Locale.ENGLISH, "%.6f", txFrequencyHz / 1_000_000.0),
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Text(
text = " MHz",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
// Offset input
OutlinedTextField(
value = offsetKHz,
onValueChange = { newVal ->
offsetKHz = newVal
// Trigger recompute based on last edited field
if (lastEditedBy == EditedField.TX) {
val txMHz = txInputMHz.toDoubleOrNull()
if (txMHz != null && txMHz > 0) {
val txHz = (txMHz * 1_000_000).toLong()
val rxHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
txHz, transponder, orbitalPos, offsetHz
)
rxInputMHz = if (rxHz != null) String.format(Locale.ENGLISH, "%.6f", rxHz / 1_000_000.0) else ""
}
} else {
val rxMHz = rxInputMHz.toDoubleOrNull()
if (rxMHz != null && rxMHz > 0) {
val rxHz = (rxMHz * 1_000_000).toLong()
val txHz = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
rxHz, transponder, orbitalPos, offsetHz
)
txInputMHz = if (txHz != null) String.format(Locale.ENGLISH, "%.6f", txHz / 1_000_000.0) else ""
}
Text(
text = String.format(Locale.ENGLISH, "%.4f", txHigh / 1_000_000.0),
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.weight(1f),
textAlign = TextAlign.End
)
}
}
},
label = { Text(stringResource(R.string.radar_doppler_offset_hint)) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.fillMaxWidth()
)
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
// TX and RX inputs on the same row
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
// TX input → compute RX
OutlinedTextField(
value = txInputMHz,
onValueChange = { newVal ->
txInputMHz = newVal
lastEditedBy = EditedField.TX
val mhz = newVal.toDoubleOrNull()
if (mhz != null && mhz > 0) {
val txHz = (mhz * 1_000_000).toLong()
val rxHz = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
txHz, transponder, orbitalPos, offsetHz
)
rxInputMHz = if (rxHz != null) String.format(Locale.ENGLISH, "%.6f", rxHz / 1_000_000.0) else ""
} else if (newVal.isEmpty()) {
rxInputMHz = ""
}
},
label = { Text(stringResource(R.string.radar_doppler_tx_hint)) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.weight(1f)
)
// RX 行
Column(modifier = Modifier.fillMaxWidth()) {
Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedButton(
onClick = { updateRx(rxFrequencyHz - stepSizeKHz * 1000L) },
modifier = Modifier.width(32.dp).height(28.dp),
contentPadding = PaddingValues(0.dp),
shape = MaterialTheme.shapes.extraSmall
) { Text("−", fontSize = 14.sp) }
Slider(
value = rxSliderValue,
onValueChange = { norm ->
val rawFreq = rxLow + (rxRange * norm).toLong()
val snapped = ((rawFreq + 500) / 1000L) * 1000L
updateRx(snapped.coerceIn(rxLow, rxHigh))
},
valueRange = 0f..1f,
modifier = Modifier.weight(1f).height(20.dp).padding(horizontal = 4.dp)
)
OutlinedButton(
onClick = { updateRx(rxFrequencyHz + stepSizeKHz * 1000L) },
modifier = Modifier.width(32.dp).height(28.dp),
contentPadding = PaddingValues(0.dp),
shape = MaterialTheme.shapes.extraSmall
) { Text("+", fontSize = 14.sp) }
}
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = String.format(Locale.ENGLISH, "%.4f", rxLow / 1_000_000.0),
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.weight(1f)
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Text(
text = String.format(Locale.ENGLISH, "%.6f", rxFrequencyHz / 1_000_000.0),
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Text(
text = " MHz",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
// RX input → compute TX
OutlinedTextField(
value = rxInputMHz,
onValueChange = { newVal ->
rxInputMHz = newVal
lastEditedBy = EditedField.RX
val mhz = newVal.toDoubleOrNull()
if (mhz != null && mhz > 0) {
val rxHz = (mhz * 1_000_000).toLong()
val txHz = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
rxHz, transponder, orbitalPos, offsetHz
)
txInputMHz = if (txHz != null) String.format(Locale.ENGLISH, "%.6f", txHz / 1_000_000.0) else ""
} else if (newVal.isEmpty()) {
txInputMHz = ""
}
Text(
text = String.format(Locale.ENGLISH, "%.4f", rxHigh / 1_000_000.0),
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.weight(1f),
textAlign = TextAlign.End
)
}
}
},
label = { Text(stringResource(R.string.radar_doppler_rx_hint)) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.weight(1f)
)
}
}
}
private enum class EditedField { TX, RX }
@Composable
private fun CwDecoderPanel(
cw: CwSubState,
@@ -935,54 +661,19 @@ private fun CwDecoderPanel(
}
AnimatedVisibility(visible = cw.isExpanded) {
// PR #1 Morse Expert engine: mini layout with waterfall + decoded text.
// Keep the old Kotlin decoder state/actions as fallback code, but this panel no longer feeds it.
val context = LocalContext.current
val activity = remember(context) {
context as? Activity ?: error("CwDecoderPanel must be hosted in an Activity")
}
val controller = remember { MainActivity() }
val rootView = remember(context) {
LayoutInflater.from(context).inflate(CwR.layout.activity_main, null) as ConstraintLayout
}
var initialized by remember { mutableStateOf(false) }
var listening by remember { mutableStateOf(false) }
// 重要: 不在展开时初始化控制器(避免"一展开就崩溃")。
// controller.onCreate/onResume/onPermissionGranted 全部推迟到用户点 Start 才执行。
fun startDecoding() {
if (!initialized) {
controller.onCreate(activity, rootView)
controller.onPermissionGranted()
controller.onResume()
initialized = true
} else {
controller.onResume()
}
listening = true
}
DisposableEffect(Unit) {
onDispose {
if (initialized) {
controller.onPause()
controller.onDestroy()
}
}
}
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
// Control buttons row
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
if (!listening) {
if (cw.status == CwStatus.Idle) {
Button(
onClick = {
if (!cw.hasPermission) {
requestMicPermission()
} else {
startDecoding()
onAction(RadarAction.CwStartListening)
}
},
modifier = Modifier.weight(1f)
@@ -991,32 +682,57 @@ private fun CwDecoderPanel(
}
} else {
Button(
onClick = {
controller.onPause()
listening = false
},
onClick = { onAction(RadarAction.CwStopListening) },
modifier = Modifier.weight(1f)
) {
Text(stringResource(R.string.radar_cw_stop))
}
}
OutlinedButton(
onClick = { if (initialized) controller.clearDecoded() },
onClick = { onAction(RadarAction.CwReset) },
modifier = Modifier.weight(1f)
) {
Text(stringResource(R.string.radar_cw_reset))
}
}
Box(
// Signal strength indicator
if (cw.status == CwStatus.Listening && cw.signalStrength > 0f) {
val strengthPct = (cw.signalStrength * 100).toInt()
Text(
text = "Signal: $strengthPct%",
fontSize = 12.sp,
color = if (cw.signalStrength > 0.5f) Color(0xFF4CAF50)
else if (cw.signalStrength > 0.2f) Color(0xFFFFC107)
else MaterialTheme.colorScheme.onSurfaceVariant
)
}
// Decoded text output
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
.height(120.dp)
) {
AndroidView(
factory = { rootView },
modifier = Modifier.fillMaxSize()
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
) {
Text(
text = "Decoded:",
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = cw.decodedText.ifEmpty { "Waiting for CW signal..." },
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
)
}
}
}
}
@@ -1038,11 +754,5 @@ private fun FrequencyText(frequency: Long?, modifier: Modifier = Modifier) {
)
}
private fun transceiverTitle(radio: SatRadio): String {
val title = if (radio.isInverted) "INV: ${radio.info}" else radio.info
val mode = "${radio.downlinkMode ?: "--"}/${radio.uplinkMode ?: "--"}"
return "$title ($mode)"
}
private val FREQ_ADJUSTMENTS =
listOf(-10_000L to "-10k", -1_000L to "-1k", -100L to "-100", 100L to "+100", 1_000L to "+1k", 10_000L to "+10k")
+11
View File
@@ -0,0 +1,11 @@
plugins {
alias(libs.plugins.convention.featurePlugin)
}
android {
namespace = "com.rtbishop.look4sat.feature.roaming"
}
dependencies {
implementation(project(":core:data"))
}
@@ -0,0 +1,791 @@
/*
* 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.feature.roaming
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.shape.CircleShape
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* 漫游页 —— 原样照搬自 QTH定位器 2.0 (com.us1pm.gridsquarelocator)。
* UI: res/layout/main.xml (结构/尺寸/颜色逐项照搬, 不做任何更改)
* 逻辑: MainActivity.showLocation() / checkEnabled() / GPS 实时监听
* 红线: UI 与逻辑均不更改, 与成品截图逐像素对齐。
*/
@Composable
fun RoamingScreen() {
val context = LocalContext.current
var state by remember { mutableStateOf(RoamingState()) }
// 照搬 onCreate: 日期固定文本 + 小时前缀(定位时拼接分钟)
val dateText = remember { SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(Date()) }
var hourPrefix by remember { mutableStateOf("") }
// 照搬 onCreate: 初始 checkEnabled() -> 红点+设置按钮, 日期隐藏
var hasPermission by remember { mutableStateOf(false) }
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { }
LaunchedEffect(Unit) {
hourPrefix = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date()).substring(0, 3)
val fine = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
val coarse = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
hasPermission = fine || coarse
if (!hasPermission) {
permissionLauncher.launch(
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
)
} else {
// 照搬 onResume: 绿点+进度条+提示+日期显示, 红点/按钮隐藏
state = state.copy(gpsOn = true, gpsOff = false, showSettings = false, showProgress = true, showNotice = true, showDate = true)
}
}
// 照搬 onResume/onPause: GPS + network 监听, 10 秒 / 10 米
val locationManager = remember { context.getSystemService(Context.LOCATION_SERVICE) as LocationManager }
DisposableEffect(locationManager, hasPermission) {
if (hasPermission) {
@SuppressLint("MissingPermission")
val listener = object : LocationListener {
override fun onLocationChanged(location: Location) {
// 照搬 showLocation 开头: 只接受 gps / network provider
if (location.provider != "gps" && location.provider != "network") return
state = roamingStateFromLocation(location.latitude, location.longitude, location.time, hourPrefix)
.copy(date = dateText)
}
override fun onProviderDisabled(provider: String) {
// 照搬 checkEnabled: 红点+按钮, 日期隐藏
state = state.copy(gpsOn = false, gpsOff = true, showSettings = true, showProgress = false, showNotice = false, showDate = false)
}
override fun onProviderEnabled(provider: String) {
state = state.copy(gpsOn = true, gpsOff = false, showSettings = false, showProgress = true, showNotice = true, showDate = true)
}
@Deprecated("Deprecated in Java")
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) = Unit
}
try {
locationManager.requestLocationUpdates("gps", 10_000L, 10f, listener)
locationManager.requestLocationUpdates("network", 10_000L, 10f, listener)
} catch (_: SecurityException) {
}
onDispose { locationManager.removeUpdates(listener) }
} else {
onDispose { }
}
}
// 照搬 res/layout/main.xml: 浅色页面, 蓝色信息区, 43sp 定位码, 3x3 九宫格, 底部版权
// 按用户要求: 上下往内缩进系统栏安全区, 避免内容被状态栏/导航栏遮挡
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.windowInsetsPadding(WindowInsets.systemBars)
) {
// relativeLayout7: 顶部 GPS 状态条, 25dp 高, holo_blue_bright
Box(
modifier = Modifier
.fillMaxWidth()
.padding(2.dp)
.height(25.dp)
.background(MaterialTheme.colorScheme.surfaceVariant)
) {
Row(modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
// tvTitleGPS: "GPS" 14sp 黑, 左 8dp
Text(
text = "GPS",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 8.dp)
)
// imGps: toRightOf tvTitleGPS + marginLeft 15dp (成品 main.xml)
Spacer(modifier = Modifier.width(15.dp))
// imGps: 绿点 (15dp, 主题色 primary -> 夜间滤镜下可见)
if (state.gpsOn) {
Box(
modifier = Modifier
.size(15.dp)
.background(MaterialTheme.colorScheme.primary, CircleShape)
)
}
// imGpsOff: 红点叠层 (15dp, 主题色 error)
if (state.gpsOff) {
Box(
modifier = Modifier
.size(15.dp)
.background(MaterialTheme.colorScheme.error, CircleShape)
)
}
Spacer(modifier = Modifier.weight(1f))
// tvDate: 14sp 黑, 居中
if (state.showDate) {
Text(text = state.date.ifEmpty { dateText }, fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
Spacer(modifier = Modifier.weight(1f))
// tvTime: 14sp 黑, 右对齐
Text(
text = state.time,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(end = 8.dp)
)
}
// btnLocationSettings: 黄色按钮, 居中
// btnLocationSettings: 成品 #faf8f54a 半透明黄, 14sp 黑字, 居中
if (state.showSettings) {
Box(
modifier = Modifier
.align(Alignment.Center)
.background(MaterialTheme.colorScheme.primary)
.clickable { context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) }
.padding(horizontal = 5.dp),
contentAlignment = Alignment.Center
) {
Text(text = "设置启用GPS", fontSize = 14.sp, color = MaterialTheme.colorScheme.onPrimary)
}
}
}
// linearLayout4: 经纬度区, holo_blue_bright
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 2.dp)
.background(MaterialTheme.colorScheme.surfaceVariant)
) {
// 纬度行: tvLati 16sp 黑 左 8dp | tvLat 右对齐 3dp
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), verticalAlignment = Alignment.CenterVertically) {
Text(text = state.latLabel, fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 8.dp))
Spacer(modifier = Modifier.weight(1f))
Text(text = state.latValue, fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(end = 3.dp))
}
// 经度行
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), verticalAlignment = Alignment.CenterVertically) {
Text(text = state.lonLabel, fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 8.dp))
Spacer(modifier = Modifier.weight(1f))
Text(text = state.lonValue, fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(end = 3.dp))
}
}
// relativeLayout6: 定位码区, 43sp bold 黑居中
Box(
modifier = Modifier
.fillMaxWidth()
.padding(2.dp)
.background(MaterialTheme.colorScheme.surfaceVariant)
) {
Text(
text = state.loc,
fontSize = 43.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
// progressBar: 加载中, 居中
if (state.showProgress) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp).align(Alignment.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
strokeWidth = 2.dp
)
}
// tvNotice: 提示文字, 居中顶部
if (state.showNotice) {
Text(
text = "请稍等,加载可能需要几分钟的时间。",
fontSize = 16.sp,
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.align(Alignment.TopCenter).padding(top = 2.dp)
)
}
}
// relativeLayout: 九宫格, 占满剩余, 底部 15dp
// 用 Box 等价 RelativeLayout 布局: 左格贴左 / 中格水平居中 / 右格贴右 (成品 main.xml)
Column(modifier = Modifier.fillMaxWidth().weight(1f).padding(bottom = 15.dp)) {
// 行1: relativeLayout5 (上排: 纬度 +1)
Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
GridCell(text = state.grids[0], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterStart))
GridCell(text = state.grids[1], modifier = Modifier.width(200.dp).fillMaxHeight().align(Alignment.Center))
GridCell(text = state.grids[2], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterEnd))
}
// 行2: relativeLayout4, 205dp 高 (中排: 中央)
Box(modifier = Modifier.fillMaxWidth().height(205.dp)) {
GridCell(text = state.grids[3], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterStart))
// 中央列: tv22 (100x80dp centerInParent) 叠在 linearLayout3 (200x200dp) 之上
Box(modifier = Modifier.width(200.dp).fillMaxHeight().align(Alignment.Center)) {
// linearLayout3: 200x200dp, marginTop 2dp, holo_blue_bright
Box(
modifier = Modifier
.align(Alignment.TopCenter)
.offset(y = 2.dp)
.size(width = 200.dp, height = 200.dp)
.background(MaterialTheme.colorScheme.surfaceVariant)
) {
// iv: 红点 10x10dp, top|left, leftMargin/topMargin 查表绝对定位
Image(
painter = painterResource(R.drawable.roam_pnt),
contentDescription = null,
modifier = Modifier
.size(10.dp)
.offset(x = state.markerLeft.dp, y = state.markerTop.dp)
)
}
// tv22: 100x80dp centerInParent, 30sp bold, textColorHighlight(白)
Text(
text = state.grids[4],
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier
.size(width = 100.dp, height = 80.dp)
.align(Alignment.Center)
)
}
GridCell(text = state.grids[5], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterEnd))
}
// 行3: relativeLayout3 (下排: 纬度 -1)
Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
GridCell(text = state.grids[6], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterStart))
GridCell(text = state.grids[7], modifier = Modifier.width(200.dp).fillMaxHeight().align(Alignment.Center))
GridCell(text = state.grids[8], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterEnd))
}
}
// tv4: 底部版权, 10sp 黑 (往上拉, 底部预留空间避免被导航栏遮挡)
Text(
text = "制作:US1PM 汉化:BA7LCE",
fontSize = 10.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp)
)
}
}
// 周边格: 60/200/60dp, #0BACF1, bold 居中, 2dp margin (照搬 tv11/tv13/tv21/tv23/tv31/tv33)
@Composable
private fun GridCell(text: String, modifier: Modifier) {
Box(
modifier = modifier
.padding(2.dp)
.background(MaterialTheme.colorScheme.surface),
contentAlignment = Alignment.Center
) {
Text(
text = text,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center
)
}
}
/**
* 照搬 MainActivity.showLocation() 的状态。
* 网格/九宫格/红点查表与成品逐行一致。
*/
data class RoamingState(
val date: String = "",
val time: String = "",
val latLabel: String = "纬度",
val latValue: String = "",
val lonLabel: String = "经度",
val lonValue: String = "",
val loc: String = "",
val grids: List<String> = List(9) { "" },
/** 红点 leftMargin(dp), 经度第 3 对字符查表 */
val markerLeft: Int = 0,
/** 红点 topMargin(dp), 纬度第 3 对字符查表(屏幕 Y 反转) */
val markerTop: Int = 0,
val gpsOn: Boolean = false,
val gpsOff: Boolean = true,
val showSettings: Boolean = true,
val showProgress: Boolean = false,
val showNotice: Boolean = false,
val showDate: Boolean = false
)
/**
* 照搬 showLocation() 的完整计算: 时间/经纬度 DMS/8 位网格/九宫格/红点查表。
*/
fun roamingStateFromLocation(lat: Double, lon: Double, fixTime: Long, hourPrefix: String): RoamingState {
// 照搬: tvTime = 缓存小时前缀 + 定位时间分钟
val minute = SimpleDateFormat("mm", Locale.getDefault()).format(Date(fixTime))
val time = hourPrefix + minute
// 方向字符
val ew = if (lon >= 0.0) "E" else "W"
val ns = if (lat >= 0.0) "N" else "S"
// 照搬: 纬度 DMS (String.valueOf 无补零)
val latAbs = if (lat < 0.0) -lat else lat
val latDeg = latAbs.toInt()
val latMinFull = (latAbs - latDeg) * 60.0
val latMin = latMinFull.toInt()
val latSec = ((latMinFull - latMin) * 60.0).toInt()
val latLabel = "纬度 $latDeg° $latMin' $latSec\" $ns"
val latValue = "$lat° "
// 照搬: 经度 DMS
val lonAbs = if (lon < 0.0) -lon else lon
val lonDeg = lonAbs.toInt()
val lonMinFull = (lonAbs - lonDeg) * 60.0
val lonMin = lonMinFull.toInt()
val lonSec = ((lonMinFull - lonMin) * 60.0).toInt()
val lonLabel = "经度 $lonDeg° $lonMin' $lonSec\" $ew"
val lonValue = "$lon° "
// 照搬: 8 位网格编码 (区间查表)
val (str, str2, str3, str4) = encodeLon(lon)
val (str5, str6, str38, str9) = encodeLat(lat)
val loc = str + str5 + str2 + str6 + str3 + str38 + str4 + str9
// 照搬: 九宫格 parseInt3 五分支
val grids = buildGrids(str, str5, str2, str6, str3, str38)
// 照搬: 红点查表 (第 3 对字符)
val markerLeft = lonMargin[str3.firstOrNull()] ?: 0
val markerTop = latMargin[str38.firstOrNull()] ?: 0
return RoamingState(
time = time,
latLabel = latLabel,
latValue = latValue,
lonLabel = lonLabel,
lonValue = lonValue,
loc = loc,
grids = grids,
markerLeft = markerLeft,
markerTop = markerTop,
gpsOn = true,
gpsOff = false,
showSettings = false,
showProgress = false,
showNotice = false,
showDate = true
)
}
// 红点 leftMargin 查表 (MainActivity 1286-1386 行): 经度第 3 对 a..x -> -2..190 dp
private val lonMargin = mapOf(
'a' to -2, 'b' to 8, 'c' to 16, 'd' to 24, 'e' to 32, 'f' to 40, 'g' to 48, 'h' to 56,
'i' to 65, 'j' to 74, 'k' to 83, 'l' to 92, 'm' to 101, 'n' to 110, 'o' to 119, 'p' to 128,
'q' to 137, 'r' to 146, 's' to 155, 't' to 163, 'u' to 171, 'v' to 179, 'w' to 184, 'x' to 190
)
// 红点 topMargin 查表 (MainActivity 1315-1363 行): 纬度第 3 对 a..x -> 190..-2 dp (屏幕 Y 反转)
private val latMargin = mapOf(
'a' to 190, 'b' to 182, 'c' to 174, 'd' to 169, 'e' to 163, 'f' to 155, 'g' to 146, 'h' to 137,
'i' to 128, 'j' to 119, 'k' to 110, 'l' to 101, 'm' to 92, 'n' to 83, 'o' to 74, 'p' to 65,
'q' to 56, 'r' to 48, 's' to 40, 't' to 32, 'u' to 24, 'v' to 16, 'w' to 8, 'x' to -2
)
// 照搬 311-611 行: 经度 -> (20°区字母, 2°数字, 2'字母, 30"数字)
private fun encodeLon(inputLon: Double): List<String> {
var longitude = inputLon
var d = 0.0
val str = when {
(longitude >= -180.0) && (longitude <= -160.0) -> { longitude += 180.0; "A" }
(longitude >= -160.0) && (longitude <= -140.0) -> { longitude += 160.0; "B" }
(longitude >= -140.0) && (longitude <= -120.0) -> { longitude += 140.0; "C" }
(longitude >= -120.0) && (longitude <= -100.0) -> { longitude += 120.0; "D" }
(longitude >= -100.0) && (longitude <= -80.0) -> { longitude += 100.0; "E" }
(longitude >= -80.0) && (longitude <= -60.0) -> { longitude += 80.0; "F" }
(longitude >= -60.0) && (longitude <= -40.0) -> { longitude += 60.0; "G" }
(longitude >= -40.0) && (longitude <= -20.0) -> { longitude += 40.0; "H" }
(longitude >= -20.0) && (longitude <= 0.0) -> { longitude += 20.0; "I" }
(longitude >= 0.0) && (longitude <= 20.0) -> "J"
(longitude >= 20.0) && (longitude <= 40.0) -> { longitude -= 20.0; "K" }
(longitude >= 40.0) && (longitude <= 60.0) -> { longitude -= 40.0; "L" }
(longitude >= 60.0) && (longitude <= 80.0) -> { longitude -= 60.0; "M" }
(longitude >= 80.0) && (longitude <= 100.0) -> { longitude -= 80.0; "N" }
(longitude >= 100.0) && (longitude <= 120.0) -> { longitude -= 100.0; "O" }
(longitude >= 120.0) && (longitude <= 140.0) -> { longitude -= 120.0; "P" }
(longitude >= 140.0) && (longitude <= 160.0) -> { longitude -= 140.0; "Q" }
(longitude >= 160.0) && (longitude <= 180.0) -> { longitude -= 160.0; "R" }
else -> { longitude = 0.0; " " }
}
val str2 = when {
((longitude >= 0.0) && (longitude <= 2.0)) || ((longitude >= -20.0) && (longitude <= -18.0)) -> { d = longitude * 60.0; "0" }
((longitude >= 2.0) && (longitude <= 4.0)) || ((longitude >= -18.0) && (longitude <= -16.0)) -> { d = (longitude - 2.0) * 60.0; "1" }
((longitude >= 4.0) && (longitude <= 6.0)) || ((longitude >= -16.0) && (longitude <= -14.0)) -> { d = (longitude - 4.0) * 60.0; "2" }
((longitude >= 6.0) && (longitude <= 8.0)) || ((longitude >= -14.0) && (longitude <= -12.0)) -> { d = (longitude - 6.0) * 60.0; "3" }
((longitude >= 8.0) && (longitude <= 10.0)) || ((longitude >= -12.0) && (longitude <= -10.0)) -> { d = (longitude - 8.0) * 60.0; "4" }
((longitude >= 10.0) && (longitude <= 12.0)) || ((longitude >= -10.0) && (longitude <= -8.0)) -> { d = (longitude - 10.0) * 60.0; "5" }
((longitude >= 12.0) && (longitude <= 14.0)) || ((longitude >= -8.0) && (longitude <= -6.0)) -> { d = (longitude - 12.0) * 60.0; "6" }
((longitude >= 14.0) && (longitude <= 16.0)) || ((longitude >= -6.0) && (longitude <= -4.0)) -> { d = (longitude - 14.0) * 60.0; "7" }
((longitude >= 16.0) && (longitude <= 18.0)) || ((longitude >= -4.0) && (longitude <= -2.0)) -> { d = (longitude - 16.0) * 60.0; "8" }
((longitude >= 18.0) && (longitude <= 20.0)) || ((longitude >= -2.0) && (longitude <= 0.0)) -> { d = (longitude - 18.0) * 60.0; "9" }
else -> { d = 0.0; " " }
}
val str3 = when {
((d >= 0.0) && (d <= 5.0)) || ((d >= -120.0) && (d <= -115.0)) -> { longitude = d * 60.0; "a" }
((d >= 5.0) && (d <= 10.0)) || ((d >= -115.0) && (d <= -110.0)) -> { longitude = (d - 5.0) * 60.0; "b" }
((d >= 10.0) && (d <= 15.0)) || ((d >= -110.0) && (d <= -105.0)) -> { longitude = (d - 10.0) * 60.0; "c" }
((d >= 15.0) && (d <= 20.0)) || ((d >= -105.0) && (d <= -100.0)) -> { longitude = (d - 15.0) * 60.0; "d" }
((d >= 20.0) && (d <= 25.0)) || ((d >= -100.0) && (d <= -95.0)) -> { longitude = (d - 20.0) * 60.0; "e" }
((d >= 25.0) && (d <= 30.0)) || ((d >= -95.0) && (d <= -90.0)) -> { longitude = (d - 25.0) * 60.0; "f" }
((d >= 30.0) && (d <= 35.0)) || ((d >= -90.0) && (d <= -85.0)) -> { longitude = (d - 30.0) * 60.0; "g" }
((d >= 35.0) && (d <= 40.0)) || ((d >= -85.0) && (d <= -80.0)) -> { longitude = (d - 35.0) * 60.0; "h" }
((d >= 40.0) && (d <= 45.0)) || ((d >= -80.0) && (d <= -75.0)) -> { longitude = (d - 40.0) * 60.0; "i" }
((d >= 45.0) && (d <= 50.0)) || ((d >= -75.0) && (d <= -70.0)) -> { longitude = (d - 45.0) * 60.0; "j" }
((d >= 50.0) && (d <= 55.0)) || ((d >= -70.0) && (d <= -65.0)) -> { longitude = (d - 50.0) * 60.0; "k" }
((d >= 55.0) && (d <= 60.0)) || ((d >= -65.0) && (d <= -60.0)) -> { longitude = (d - 55.0) * 60.0; "l" }
((d >= 60.0) && (d <= 65.0)) || ((d >= -60.0) && (d <= -55.0)) -> { longitude = (d - 60.0) * 60.0; "m" }
((d >= 65.0) && (d <= 70.0)) || ((d >= -55.0) && (d <= -50.0)) -> { longitude = (d - 65.0) * 60.0; "n" }
((d >= 70.0) && (d <= 75.0)) || ((d >= -50.0) && (d <= -45.0)) -> { longitude = (d - 70.0) * 60.0; "o" }
((d >= 75.0) && (d <= 80.0)) || ((d >= -45.0) && (d <= -40.0)) -> { longitude = (d - 75.0) * 60.0; "p" }
((d >= 80.0) && (d <= 85.0)) || ((d >= -40.0) && (d <= -35.0)) -> { longitude = (d - 80.0) * 60.0; "q" }
((d >= 85.0) && (d <= 90.0)) || ((d >= -35.0) && (d <= -30.0)) -> { longitude = (d - 85.0) * 60.0; "r" }
((d >= 90.0) && (d <= 95.0)) || ((d >= -30.0) && (d <= -25.0)) -> { longitude = (d - 90.0) * 60.0; "s" }
((d >= 95.0) && (d <= 100.0)) || ((d >= -25.0) && (d <= -20.0)) -> { longitude = (d - 95.0) * 60.0; "t" }
((d >= 100.0) && (d <= 105.0)) || ((d >= -20.0) && (d <= -15.0)) -> { longitude = (d - 100.0) * 60.0; "u" }
((d >= 105.0) && (d <= 110.0)) || ((d >= -15.0) && (d <= -10.0)) -> { longitude = (d - 105.0) * 60.0; "v" }
((d >= 110.0) && (d <= 115.0)) || ((d >= -10.0) && (d <= -5.0)) -> { longitude = (d - 110.0) * 60.0; "w" }
((d >= 115.0) && (d <= 120.0)) || ((d >= -5.0) && (d <= 0.0)) -> { longitude = (d - 115.0) * 60.0; "x" }
else -> { longitude = 0.0; " " }
}
val str4 = when {
((longitude >= 0.0) && (longitude <= 30.0)) || ((longitude >= -300.0) && (longitude <= -270.0)) -> "0"
((longitude >= 30.0) && (longitude <= 60.0)) || ((longitude >= -270.0) && (longitude <= -240.0)) -> "1"
((longitude >= 60.0) && (longitude <= 90.0)) || ((longitude >= -240.0) && (longitude <= -210.0)) -> "2"
((longitude >= 90.0) && (longitude <= 120.0)) || ((longitude >= -210.0) && (longitude <= -180.0)) -> "3"
((longitude >= 120.0) && (longitude <= 150.0)) || ((longitude >= -180.0) && (longitude <= -150.0)) -> "4"
((longitude >= 150.0) && (longitude <= 180.0)) || ((longitude >= -150.0) && (longitude <= -120.0)) -> "5"
((longitude >= 180.0) && (longitude <= 210.0)) || ((longitude >= -120.0) && (longitude <= -90.0)) -> "6"
((longitude >= 210.0) && (longitude <= 240.0)) || ((longitude >= -90.0) && (longitude <= -60.0)) -> "7"
((longitude >= 240.0) && (longitude <= 270.0)) || ((longitude >= -60.0) && (longitude <= -30.0)) -> "8"
((longitude >= 270.0) && (longitude <= 300.0)) || ((longitude >= -30.0) && (longitude <= 0.0)) -> "9"
else -> " "
}
return listOf(str, str2, str3, str4)
}
// 照搬 612-914 行: 纬度 -> (10°区字母, 1°数字, 1'字母, 15"数字)
private fun encodeLat(inputLat: Double): List<String> {
var latitude = inputLat
var d2 = 0.0
var d3 = 0.0
val str5 = when {
(latitude >= -90.0) && (latitude <= -80.0) -> { d2 = latitude + 90.0; "A" }
(latitude >= -80.0) && (latitude <= -70.0) -> { d2 = latitude + 80.0; "B" }
(latitude >= -70.0) && (latitude <= -60.0) -> { d2 = latitude + 70.0; "C" }
(latitude >= -60.0) && (latitude <= -50.0) -> { d2 = latitude + 60.0; "D" }
(latitude >= -50.0) && (latitude <= -40.0) -> { d2 = latitude + 50.0; "E" }
(latitude >= -40.0) && (latitude <= -30.0) -> { d2 = latitude + 40.0; "F" }
(latitude >= -30.0) && (latitude <= -20.0) -> { d2 = latitude + 30.0; "G" }
(latitude >= -20.0) && (latitude <= -10.0) -> { d2 = latitude + 20.0; "H" }
(latitude >= -10.0) && (latitude <= 0.0) -> { d2 = latitude + 10.0; "I" }
(latitude >= 0.0) && (latitude <= 10.0) -> { d2 = latitude; "J" }
(latitude >= 10.0) && (latitude <= 20.0) -> { d2 = latitude - 10.0; "K" }
(latitude >= 20.0) && (latitude <= 30.0) -> { d2 = latitude - 20.0; "L" }
(latitude >= 30.0) && (latitude <= 40.0) -> { d2 = latitude - 30.0; "M" }
(latitude >= 40.0) && (latitude <= 50.0) -> { d2 = latitude - 40.0; "N" }
(latitude >= 50.0) && (latitude <= 60.0) -> { d2 = latitude - 50.0; "O" }
(latitude >= 60.0) && (latitude <= 70.0) -> { d2 = latitude - 60.0; "P" }
(latitude >= 70.0) && (latitude <= 80.0) -> { d2 = latitude - 70.0; "Q" }
(latitude >= 80.0) && (latitude <= 90.0) -> { d2 = latitude - 80.0; "R" }
else -> { d2 = 0.0; " " }
}
var str6 = " "
when {
((d2 >= 0.0) && (d2 <= 1.0)) || ((d2 >= -10.0) && (d2 <= -9.0)) -> { d3 = d2 * 60.0; str6 = "0" }
((d2 >= 1.0) && (d2 <= 2.0)) || ((d2 >= -9.0) && (d2 <= -8.0)) -> { d3 = (d2 - 1.0) * 60.0; str6 = "1" }
((d2 >= 2.0) && (d2 <= 3.0)) || ((d2 >= -8.0) && (d2 <= -7.0)) -> { d3 = (d2 - 2.0) * 60.0; str6 = "2" }
((d2 >= 3.0) && (d2 <= 4.0)) || ((d2 >= -7.0) && (d2 <= -6.0)) -> { d3 = (d2 - 3.0) * 60.0; str6 = "3" }
((d2 >= 4.0) && (d2 <= 5.0)) || ((d2 >= -6.0) && (d2 <= -5.0)) -> { d3 = (d2 - 4.0) * 60.0; str6 = "4" }
((d2 >= 5.0) && (d2 <= 6.0)) || ((d2 >= -5.0) && (d2 <= -4.0)) -> { d3 = (d2 - 5.0) * 60.0; str6 = "5" }
((d2 >= 6.0) && (d2 <= 7.0)) || ((d2 >= -4.0) && (d2 <= -3.0)) -> { d3 = (d2 - 6.0) * 60.0; str6 = "6" }
((d2 >= 7.0) && (d2 <= 8.0)) || ((d2 >= -3.0) && (d2 <= -2.0)) -> { d3 = (d2 - 7.0) * 60.0; str6 = "7" }
((d2 >= 8.0) && (d2 <= 9.0)) || ((d2 >= -2.0) && (d2 <= -1.0)) -> { d3 = (d2 - 8.0) * 60.0; str6 = "8" }
((d2 >= 9.0) && (d2 <= 10.0)) || ((d2 >= -1.0) && (d2 <= 0.0)) -> { d3 = (d2 - 9.0) * 60.0; str6 = "9" }
}
val str38 = when {
((d3 >= 0.0) && (d3 <= 2.5)) || ((d3 >= -60.0) && (d3 <= -57.5)) -> { d2 = d3 * 60.0; "a" }
((d3 >= 2.5) && (d3 <= 5.0)) || ((d3 >= -57.5) && (d3 <= -55.0)) -> { d2 = (d3 - 2.5) * 60.0; "b" }
((d3 >= 5.0) && (d3 <= 7.5)) || ((d3 >= -55.0) && (d3 <= -52.5)) -> { d2 = (d3 - 5.0) * 60.0; "c" }
((d3 >= 7.5) && (d3 <= 10.0)) || ((d3 >= -52.5) && (d3 <= -50.0)) -> { d2 = (d3 - 7.5) * 60.0; "d" }
((d3 >= 10.0) && (d3 <= 12.5)) || ((d3 >= -50.0) && (d3 <= -47.5)) -> { d2 = (d3 - 10.0) * 60.0; "e" }
((d3 >= 12.5) && (d3 <= 15.0)) || ((d3 >= -47.5) && (d3 <= -45.0)) -> { d2 = (d3 - 12.5) * 60.0; "f" }
((d3 >= 15.0) && (d3 <= 17.5)) || ((d3 >= -45.0) && (d3 <= -42.5)) -> { d2 = (d3 - 15.0) * 60.0; "g" }
((d3 >= 17.5) && (d3 <= 20.0)) || ((d3 >= -42.5) && (d3 <= -40.0)) -> { d2 = (d3 - 17.5) * 60.0; "h" }
((d3 >= 20.0) && (d3 <= 22.5)) || ((d3 >= -40.0) && (d3 <= -37.5)) -> { d2 = (d3 - 20.0) * 60.0; "i" }
((d3 >= 22.5) && (d3 <= 25.0)) || ((d3 >= -37.5) && (d3 <= -35.0)) -> { d2 = (d3 - 22.5) * 60.0; "j" }
((d3 >= 25.0) && (d3 <= 27.5)) || ((d3 >= -35.0) && (d3 <= -32.5)) -> { d2 = (d3 - 25.0) * 60.0; "k" }
((d3 >= 27.5) && (d3 <= 30.0)) || ((d3 >= -32.5) && (d3 <= -30.0)) -> { d2 = (d3 - 27.5) * 60.0; "l" }
((d3 >= 30.0) && (d3 <= 32.5)) || ((d3 >= -30.0) && (d3 <= -27.5)) -> { d2 = (d3 - 30.0) * 60.0; "m" }
((d3 >= 32.5) && (d3 <= 35.0)) || ((d3 >= -27.5) && (d3 <= -25.0)) -> { d2 = (d3 - 32.5) * 60.0; "n" }
((d3 >= 35.0) && (d3 <= 37.5)) || ((d3 >= -25.0) && (d3 <= -22.5)) -> { d2 = (d3 - 35.0) * 60.0; "o" }
((d3 >= 37.5) && (d3 <= 40.0)) || ((d3 >= -22.5) && (d3 <= -20.0)) -> { d2 = (d3 - 37.5) * 60.0; "p" }
((d3 >= 40.0) && (d3 <= 42.5)) || ((d3 >= -20.0) && (d3 <= -17.5)) -> { d2 = (d3 - 40.0) * 60.0; "q" }
((d3 >= 42.5) && (d3 <= 45.0)) || ((d3 >= -17.5) && (d3 <= -15.0)) -> { d2 = (d3 - 42.5) * 60.0; "r" }
((d3 >= 45.0) && (d3 <= 47.5)) || ((d3 >= -15.0) && (d3 <= -12.5)) -> { d2 = (d3 - 45.0) * 60.0; "s" }
((d3 >= 47.5) && (d3 <= 50.0)) || ((d3 >= -12.5) && (d3 <= -10.0)) -> { d2 = (d3 - 47.5) * 60.0; "t" }
((d3 >= 50.0) && (d3 <= 52.5)) || ((d3 >= -10.0) && (d3 <= -7.5)) -> { d2 = (d3 - 50.0) * 60.0; "u" }
((d3 >= 52.5) && (d3 <= 55.0)) || ((d3 >= -7.5) && (d3 <= -5.0)) -> { d2 = (d3 - 52.5) * 60.0; "v" }
((d3 >= 55.0) && (d3 <= 57.5)) || ((d3 >= -5.0) && (d3 <= -2.5)) -> { d2 = (d3 - 55.0) * 60.0; "w" }
((d3 >= 57.5) && (d3 <= 60.0)) || ((d3 >= -2.5) && (d3 <= 0.0)) -> { d2 = (d3 - 57.5) * 60.0; "x" }
else -> { d2 = 0.0; " " }
}
val str9 = when {
((d2 >= 0.0) && (d2 <= 15.0)) || ((d2 >= -150.0) && (d2 <= -135.0)) -> "0"
((d2 >= 15.0) && (d2 <= 30.0)) || ((d2 >= -135.0) && (d2 <= -120.0)) -> "1"
((d2 >= 30.0) && (d2 <= 45.0)) || ((d2 >= -120.0) && (d2 <= -105.0)) -> "2"
((d2 >= 45.0) && (d2 <= 60.0)) || ((d2 >= -105.0) && (d2 <= -90.0)) -> "3"
((d2 >= 60.0) && (d2 <= 75.0)) || ((d2 >= -90.0) && (d2 <= -75.0)) -> "4"
((d2 >= 75.0) && (d2 <= 90.0)) || ((d2 >= -75.0) && (d2 <= -60.0)) -> "5"
((d2 >= 90.0) && (d2 <= 105.0)) || ((d2 >= -60.0) && (d2 <= -45.0)) -> "6"
((d2 >= 105.0) && (d2 <= 120.0)) || ((d2 >= -45.0) && (d2 <= -30.0)) -> "7"
((d2 >= 120.0) && (d2 <= 135.0)) || ((d2 >= -30.0) && (d2 <= -15.0)) -> "8"
((d2 >= 135.0) && (d2 <= 150.0)) || ((d2 >= -15.0) && (d2 <= 0.0)) -> "9"
else -> " "
}
return listOf(str5, str6, str38, str9)
}
/**
* 照搬 917-1283 行: 九宫格 parseInt3 五分支(内部/西界/东界/北界/南界 + 四角)。
* 返回 9 格文本: [上左, 上中, 上右, 左中, 中央, 右中, 下左, 下中, 下右]。
*/
private fun buildGrids(str: String, str5: String, str2: String, str6: String, str3: String, str38: String): List<String> {
val parseInt = str2.toInt()
val parseInt2 = str6.toInt()
val str39 = str + str5
val str40 = str2 + str6
val parseInt3 = str40.toInt()
val str41 = str39 + str40
val g = MutableList(9) { "" }
fun fmt2(n: Int) = if (n <= 9) "0$n" else n.toString()
if ((parseInt > 0) && (parseInt < 9) && (parseInt2 > 0) && (parseInt2 < 9)) {
// 内部: 数字对 ±1
val i7 = parseInt3 + 1
val i8 = i7 - 10
val valueOf9 = fmt2(i8)
val str11 = i7.toString()
val valueOf18 = (i7 + 10).toString()
val i9 = parseInt3 - 10
val valueOf10 = fmt2(i9)
val valueOf19 = (parseInt3 + 10).toString()
val i10 = parseInt3 - 1
val i11 = i10 - 10
val valueOf11 = fmt2(i11)
val str14 = i10.toString()
val str15 = (i10 + 10).toString()
g[0] = str39 + valueOf9
g[1] = str39 + str11
g[2] = str39 + valueOf18
g[3] = str39 + valueOf10
g[4] = str41
g[5] = str39 + valueOf19
g[6] = str39 + valueOf11
g[7] = str39 + str14
g[8] = str39 + str15
} else if ((parseInt == 0) && (parseInt2 > 0) && (parseInt2 < 9)) {
// 西边界: 经度 0 -> 9, 经度字母 -1
val str43 = (str[0] - 1).toString() + str5
val str44 = "9" + (parseInt2 + 1)
val i12 = parseInt3 + 1
val valueOf7 = fmt2(i12)
val str12 = (i12 + 10).toString()
val sb4 = "9" + parseInt2
val valueOf20 = (parseInt3 + 10).toString()
val str46 = "9" + (parseInt2 - 1)
val i13 = parseInt3 - 1
val valueOf8 = fmt2(i13)
val str15 = (i13 + 10).toString()
g[0] = str43 + str44
g[1] = str39 + valueOf7
g[2] = str39 + str12
g[3] = str43 + sb4
g[4] = str41
g[5] = str39 + valueOf20
g[6] = str43 + str46
g[7] = str39 + valueOf8
g[8] = str39 + str15
} else if ((parseInt == 9) && (parseInt2 > 0) && (parseInt2 < 9)) {
// 东边界: 经度 9 -> 0, 经度字母 +1
val str47 = (str[0] + 1).toString() + str5
val i14 = parseInt3 + 1
val valueOf21 = (i14 - 10).toString()
val valueOf22 = i14.toString()
val str48 = "0" + (parseInt2 + 1)
val valueOf23 = (parseInt3 - 10).toString()
val str49 = "0" + parseInt2
val i15 = parseInt3 - 1
val valueOf24 = (i15 - 10).toString()
val valueOf25 = i15.toString()
val str15 = "0" + (parseInt2 - 1)
g[0] = str39 + valueOf21
g[1] = str39 + valueOf22
g[2] = str47 + str48
g[3] = str39 + valueOf23
g[4] = str41
g[5] = str47 + str49
g[6] = str39 + valueOf24
g[7] = str39 + valueOf25
g[8] = str47 + str15
} else if ((parseInt < 9) && (parseInt > 0) && (parseInt2 == 9)) {
// 北边界: 纬度 9 -> 0, 纬度字母 +1
val str50 = str + (str5[0] + 1).toString()
val i16 = parseInt3 - 9
val i17 = i16 - 10
val valueOf4 = fmt2(i17)
val valueOf26 = i16.toString()
val valueOf27 = (i16 + 10).toString()
val i18 = parseInt3 - 10
val valueOf5 = fmt2(i18)
val valueOf28 = (parseInt3 + 10).toString()
val i19 = parseInt3 - 1
val i20 = i19 - 10
val valueOf6 = fmt2(i20)
val str14 = i19.toString()
val str15 = (i19 + 10).toString()
g[0] = str39 + valueOf4
g[1] = str50 + valueOf26
g[2] = str39 + valueOf27
g[3] = str39 + valueOf5
g[4] = str41
g[5] = str39 + valueOf28
g[6] = str39 + valueOf6
g[7] = str39 + str14
g[8] = str39 + str15
} else if ((parseInt < 9) && (parseInt > 0) && (parseInt2 == 0)) {
// 南边界: 纬度 0 -> 9, 纬度字母 -1
val str52 = str + (str5[0] - 1).toString()
val i21 = parseInt3 + 1
val i22 = i21 - 10
val valueOf = fmt2(i22)
val valueOf29 = i21.toString()
val valueOf30 = (i21 + 10).toString()
val i23 = parseInt3 - 10
val valueOf2 = fmt2(i23)
val valueOf31 = (parseInt3 + 10).toString()
val i24 = parseInt3 - 1
val valueOf3 = fmt2(i24)
val str14 = (i24 + 10).toString()
val str15 = (i24 + 20).toString()
g[0] = str39 + valueOf
g[1] = str39 + valueOf29
g[2] = str39 + valueOf30
g[3] = str39 + valueOf2
g[4] = str41
g[5] = str39 + valueOf31
g[6] = str52 + valueOf3
g[7] = str52 + str14
g[8] = str52 + str15
} else if (parseInt3 == 0) {
// 四角 00: 经纬度数字均 -1 进位
val v32 = (str[0] - 1).toString()
val v33 = (str5[0] - 1).toString()
g[0] = v32 + str5 + "91"
g[1] = str39 + "01"
g[2] = str39 + "11"
g[3] = v32 + str5 + "90"
g[4] = str41
g[5] = v32 + str5 + "10"
g[6] = v32 + v33 + "99"
g[7] = str + v33 + "09"
g[8] = str + v33 + "19"
} else if (parseInt3 == 9) {
// 四角 09: 经度 -1, 纬度 +1
val v34 = (str[0] - 1).toString()
val v35 = (str5[0] + 1).toString()
g[0] = v34 + v35 + "90"
g[1] = str + v35 + "00"
g[2] = str + v35 + "10"
g[3] = v34 + str5 + "99"
g[4] = str41
g[5] = v34 + str5 + "19"
g[6] = v34 + str5 + "98"
g[7] = v34 + str5 + "08"
g[8] = v34 + str5 + "18"
} else if (parseInt3 == 99) {
// 四角 99: 经纬度数字均 +1 进位
val v36 = (str[0] + 1).toString()
val v37 = (str5[0] + 1).toString()
g[0] = str + v37 + "80"
g[1] = str + v37 + "90"
g[2] = v36 + v37 + "00"
g[3] = str39 + "89"
g[4] = str41
g[5] = v36 + str5 + "09"
g[6] = str39 + "88"
g[7] = str39 + "98"
g[8] = v36 + str5 + "08"
} else if (parseInt3 == 90) {
// 四角 90: 经度 +1, 纬度 -1
val v38 = (str[0] + 1).toString()
val v39 = (str5[0] - 1).toString()
g[0] = str39 + "81"
g[1] = str39 + "91"
g[2] = v38 + str5 + "01"
g[3] = str39 + "80"
g[4] = str41
g[5] = v38 + str5 + "00"
g[6] = str + v39 + "89"
g[7] = str + v39 + "99"
g[8] = v38 + v39 + "09"
}
return g
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

@@ -0,0 +1,124 @@
/*
* 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.feature.roaming
import org.junit.Assert.assertEquals
import org.junit.Test
class RoamingStateTest {
// 用户实测坐标 (QTH定位器 2.0 成品截图): 22.314066 / 108.706575
private fun stateOf(lat: Double, lon: Double) =
roamingStateFromLocation(lat, lon, System.currentTimeMillis(), "21:")
@Test
fun `user coordinates produce OL42ih45`() {
val state = stateOf(22.314066, 108.706575)
assertEquals("OL42ih45", state.loc)
}
@Test
fun `red marker from ih pair matches reference lookup`() {
val state = stateOf(22.314066, 108.706575)
// 成品查表: 经度第3对 'i' -> leftMargin 65dp, 纬度第3对 'h' -> topMargin 137dp
assertEquals(65, state.markerLeft)
assertEquals(137, state.markerTop)
}
@Test
fun `grid nine from OL42 matches reference`() {
val state = stateOf(22.314066, 108.706575)
val expected = listOf(
"OL33", "OL43", "OL53",
"OL32", "OL42", "OL52",
"OL31", "OL41", "OL51"
)
assertEquals(expected, state.grids)
}
@Test
fun `latitude dms formatting matches reference`() {
val state = stateOf(22.314066, 108.706575)
assertEquals("纬度 22° 18' 50\" N", state.latLabel)
assertEquals("22.314066° ", state.latValue)
assertEquals("经度 108° 42' 23\" E", state.lonLabel)
assertEquals("108.706575° ", state.lonValue)
}
@Test
fun `time is hour prefix plus fix minutes`() {
// fixTime = 2026-08-03 21:12:13 UTC+8 -> minutes 12
val fix = 1785820333000L // 2026-08-03 21:12:13 +0800
val state = roamingStateFromLocation(22.314066, 108.706575, fix, "21:")
assertEquals("21:12", state.time)
}
@Test
fun `west edge borrows lon letter`() {
// 经度 0.5 纬度 22.314066: 数字对 02, 西边界 parseInt==0
val state = stateOf(22.314066, 0.5)
assertEquals(8, state.loc.length)
// 左列经度 0->9, 经度字母 J-1=I: IL92
assertEquals("IL92", state.grids[3])
}
@Test
fun `north edge carries lat letter`() {
// 纬度 29.5 经度 108.706575: 数字对 49? -> 纬度数字 9 -> 北边界
val state = stateOf(29.5, 108.706575)
assertEquals(8, state.loc.length)
// 上排纬度 +1 -> 0, 纬度字母 +1 (M)
assertEquals("OM40", state.grids[1])
}
@Test
fun `east edge carries lon letter`() {
// 经度 119.5 纬度 22.314066: 经度数字 9 -> 东边界
val state = stateOf(22.314066, 119.5)
assertEquals(8, state.loc.length)
// 右上 经度 9->0, 经度字母 O+1=P, 纬度 2+1=3: PL03
assertEquals("PL03", state.grids[2])
}
@Test
fun `south edge borrows lat letter`() {
// 纬度 0.5 经度 108.706575: 纬度数字 0 -> 南边界
val state = stateOf(0.5, 108.706575)
assertEquals(8, state.loc.length)
// 下中 纬度 0->9, 纬度字母 J-1=I: OI49
assertEquals("OI49", state.grids[7])
}
@Test
fun `boundary values wrap like reference`() {
// (90, 180) -> RR99xx99 (成品区间查表行为)
val state = stateOf(90.0, 180.0)
assertEquals("RR99xx99", state.loc)
}
@Test
fun `gps state after fix matches showLocation`() {
val state = stateOf(22.314066, 108.706575)
assertEquals(true, state.gpsOn)
assertEquals(false, state.gpsOff)
assertEquals(false, state.showSettings)
assertEquals(false, state.showProgress)
assertEquals(false, state.showNotice)
assertEquals(true, state.showDate)
}
}
@@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
@@ -339,6 +340,8 @@ private fun LocationCard(
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Text(text = "Lat: ${settings.stationPos.latitude}°")
Text(text = "Lon: ${settings.stationPos.longitude}°")
}
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Text(text = "Qth: ${settings.stationPos.qthLocator}")
}
Spacer(modifier = Modifier.height(1.dp))
@@ -484,11 +487,12 @@ private fun OtherCardPreview() = MainTheme {
@Composable
private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Unit) {
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.height(268.dp)
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
Column(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = stringResource(id = R.string.prefs_other_title),
color = MaterialTheme.colorScheme.primary
@@ -556,15 +560,11 @@ private fun CardCreditsPreview() = MainTheme { CardCredits() }
@Composable
private fun CardCredits(modifier: Modifier = Modifier) {
ElevatedCard(
modifier = modifier
.fillMaxWidth()
.height(268.dp)
modifier = modifier.fillMaxWidth()
) {
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 4.dp)
.fillMaxHeight()
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
) {
Text(
text = stringResource(id = R.string.prefs_outro_title),
@@ -573,6 +573,8 @@ private fun CardCredits(modifier: Modifier = Modifier) {
Text(
text = stringResource(id = R.string.prefs_outro_thanks)
)
// 感谢列表与保修声明之间保留约两行间距 (用户要求)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(id = R.string.prefs_outro_license),
color = MaterialTheme.colorScheme.primary
@@ -588,7 +590,7 @@ private fun TopCard(onClick: () -> Unit, modifier: Modifier = Modifier, version:
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.height(48.dp)
.heightIn(min = 48.dp)
.clickable { onClick() }) {
Spacer(Modifier)
Icon(
@@ -603,7 +605,6 @@ private fun TopCard(onClick: () -> Unit, modifier: Modifier = Modifier, version:
modifier = Modifier
.weight(1f)
.padding(end = 6.dp)
.infiniteMarquee()
)
}
}
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff. Show more