mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbf9a2a65f | ||
|
|
ddecaa6017 | ||
|
|
418a05e343 | ||
|
|
c80e351212 | ||
|
|
a333192df5 | ||
|
|
a7b41c95c7 | ||
|
|
2298d8eea4 | ||
|
|
1813e1bfa4 | ||
|
|
bd09ad22ff | ||
|
|
7e90071c4d | ||
|
|
d1cc944be2 | ||
|
|
fb28283679 | ||
|
|
81ae8772d2 | ||
|
|
571145838d | ||
|
|
017b178f2e | ||
|
|
670444858c | ||
|
|
7bdff5df81 | ||
|
|
df083840ac | ||
|
|
a0279bac17 | ||
|
|
3d630041e1 | ||
|
|
ea92d3c150 | ||
|
|
86ee3dcbac | ||
|
|
43788ba7a7 | ||
|
|
1a3f1bb34c | ||
|
|
998c98267b | ||
|
|
28fa373db0 | ||
|
|
4ca33c37ad | ||
|
|
bd42e643ea | ||
|
|
4baf3821e5 | ||
|
|
d7e1ce11c9 | ||
|
|
5de370983d | ||
|
|
f272e268ab | ||
|
|
e59507a4d3 | ||
|
|
0bae306312 | ||
|
|
65f61a5147 | ||
|
|
1064dc739e | ||
|
|
8aedd38b51 | ||
|
|
ad8f08a577 | ||
|
|
2afce54472 | ||
|
|
8e50fcf8fb | ||
|
|
e11b03d56b | ||
|
|
fa55b82a5a | ||
|
|
d57832b0d4 | ||
|
|
1d0e8921a9 | ||
|
|
23d95b8241 | ||
|
|
8af80866bc | ||
|
|
b9e5ff70f5 | ||
|
|
c176d6ad88 | ||
|
|
5a6b162e4f | ||
|
|
33712c29fc | ||
|
|
eac1e6e273 |
No files matched your search
@@ -1,6 +1,9 @@
|
||||
# Built application files
|
||||
*.ap_
|
||||
|
||||
# Hermes AI plans and metadata
|
||||
.hermes/
|
||||
|
||||
# Files for the ART/Dalvik VM
|
||||
*.dex
|
||||
|
||||
|
||||
@@ -1,45 +1,26 @@
|
||||
# Look4Sat: Satellite tracker
|
||||
# Look4Sat-BA7OPF
|
||||
|
||||
[](https://github.com/rt-bishop/Look4Sat/actions/workflows/release.yml)
|
||||
[](https://github.com/atsunatsu/Look4Sat/actions/workflows/release.yml)
|
||||
|
||||
[<img src="https://play.google.com/intl/en_gb/badges/static/images/badges/en_badge_web_generic.png" alt="Get it on Google Play" height="80">](https://play.google.com/store/apps/details?id=com.rtbishop.look4sat)
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80">](https://f-droid.org/packages/com.rtbishop.look4sat/)
|
||||
**BA7OPF 定制版** — 基于 [rt-bishop/Look4Sat](https://github.com/rt-bishop/Look4Sat) 的业余无线电卫星追踪器,增加了线性卫星频率计算器等功能。
|
||||
|
||||
### Radio satellite tracker and pass predictor for Android, inspired by Gpredict
|
||||
## 本仓库特色功能
|
||||
|
||||
<p float="left">
|
||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" width="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>
|
||||
- **线性卫星转发器频率计算器** — 在雷达页的 Calculator 标签页中,支持 TX/RX 双向多普勒频率计算,以及下行频率偏移(offset)输入,方便操作带偏移的线性卫星
|
||||
- **CW 解码器** — 集成 Morse Expert 解码引擎,支持瀑布图、实时解码文本
|
||||
- **Passband 模式** — 支持通过位置滑块(Passband)自动计算 TX/RX 频率,避免切换时跳变
|
||||
- **中文界面优化** — 翻译修正、UI 布局调整
|
||||
|
||||
### Track satellite passes with ease!
|
||||
## 上游仓库
|
||||
|
||||
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.
|
||||
本仓库是 [rt-bishop/Look4Sat](https://github.com/rt-bishop/Look4Sat) 的分支,上游仓库的原始功能包括:
|
||||
|
||||
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.
|
||||
- 基于 Celestrak / SatNOGS 数据的 9000+ 活跃卫星追踪
|
||||
- SGP4/SDP4 轨道预测,10 天过境预报
|
||||
- 极坐标雷达图、地面轨迹图
|
||||
- SSTV 图像解码
|
||||
- 无广告、无跟踪、完全离线
|
||||
|
||||
The application is built using Kotlin, Coroutines, Jetpack Compose and Navigation.\
|
||||
It is now and always will be completely ad-free and open-source.
|
||||
## 许可证
|
||||
|
||||
## 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>
|
||||
GNU General Public License v3.0。详见 [LICENSE](LICENSE)。
|
||||
@@ -1,3 +1,24 @@
|
||||
plugins {
|
||||
alias(libs.plugins.convention.applicationPlugin)
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Look4Sat.SplashScreen">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -63,13 +63,16 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||
import com.rtbishop.look4sat.core.domain.repository.MutualPassData
|
||||
import com.rtbishop.look4sat.core.presentation.DeeplinkResolver
|
||||
import com.rtbishop.look4sat.core.presentation.ElevationThresholds
|
||||
import com.rtbishop.look4sat.core.presentation.LocalElevationThresholds
|
||||
@@ -78,6 +81,8 @@ import com.rtbishop.look4sat.core.presentation.Screen
|
||||
import com.rtbishop.look4sat.core.presentation.hasEnoughHeight
|
||||
import com.rtbishop.look4sat.core.presentation.hasEnoughWidth
|
||||
import com.rtbishop.look4sat.feature.map.MapDestination
|
||||
import com.rtbishop.look4sat.feature.mutual.MutualScreen
|
||||
import com.rtbishop.look4sat.feature.mutual.MutualViewModel
|
||||
import com.rtbishop.look4sat.feature.passes.PassesDestination
|
||||
import com.rtbishop.look4sat.feature.radar.RadarDestination
|
||||
import com.rtbishop.look4sat.feature.satellites.SatellitesDestination
|
||||
@@ -125,12 +130,17 @@ 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.Map, Screen.Settings)
|
||||
val navItems = listOf(Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Mutual, Screen.Map, Screen.Settings)
|
||||
|
||||
val context = LocalContext.current
|
||||
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
||||
val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
|
||||
val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle()
|
||||
// Activity-scoped so the mutual query results survive navigation to Radar and back
|
||||
val mutualViewModel: MutualViewModel = viewModel(
|
||||
viewModelStoreOwner = context as ViewModelStoreOwner,
|
||||
factory = MutualViewModel.factory(container)
|
||||
)
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalElevationThresholds provides ElevationThresholds(
|
||||
@@ -145,6 +155,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
||||
is Screen.Satellites -> screen is Screen.Satellites
|
||||
is Screen.Passes -> screen is Screen.Passes
|
||||
is Screen.Radar -> screen is Screen.Radar
|
||||
is Screen.Mutual -> screen is Screen.Mutual
|
||||
is Screen.Map -> screen is Screen.Map
|
||||
is Screen.Settings -> screen is Screen.Settings
|
||||
else -> false
|
||||
@@ -190,6 +201,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
||||
}
|
||||
entry<Screen.Passes> {
|
||||
PassesDestination { catNum, aosTime ->
|
||||
container.setMutualPassData(MutualPassData())
|
||||
container.satelliteRepo.selectPass(catNum, aosTime)
|
||||
backStack.add(Screen.Radar)
|
||||
// navigateToRadar()
|
||||
@@ -201,6 +213,17 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
||||
entry<Screen.Map> {
|
||||
MapDestination()
|
||||
}
|
||||
entry<Screen.Mutual> {
|
||||
MutualScreen(
|
||||
viewModel = mutualViewModel,
|
||||
navigateUp = navigateBack,
|
||||
navigateToRadar = { catNum, aosTime, pass ->
|
||||
container.setMutualPassData(pass ?: MutualPassData())
|
||||
container.satelliteRepo.selectPass(catNum, aosTime)
|
||||
backStack.add(Screen.Radar)
|
||||
}
|
||||
)
|
||||
}
|
||||
entry<Screen.Settings> {
|
||||
SettingsDestination()
|
||||
}
|
||||
@@ -224,6 +247,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
||||
.clickable {
|
||||
val pass = trackingState.currentPass
|
||||
if (pass != null) {
|
||||
container.setMutualPassData(MutualPassData())
|
||||
container.satelliteRepo.selectPass(pass.catNum, pass.aosTime)
|
||||
backStack.add(Screen.Radar)
|
||||
}
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ internal class ApplicationPlugin : Plugin<Project> {
|
||||
implementation(project(":core:domain"))
|
||||
implementation(project(":core:presentation"))
|
||||
implementation(project(":feature:map"))
|
||||
implementation(project(":feature:mutual"))
|
||||
implementation(project(":feature:passes"))
|
||||
implementation(project(":feature:radar"))
|
||||
implementation(project(":feature:satellites"))
|
||||
|
||||
@@ -50,6 +50,7 @@ import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISelectionRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.MutualPassData
|
||||
import com.rtbishop.look4sat.core.domain.source.ILocalSource
|
||||
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar
|
||||
@@ -61,6 +62,9 @@ import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class MainContainer(private val context: Context) : IMainContainer {
|
||||
@@ -77,6 +81,13 @@ class MainContainer(private val context: Context) : IMainContainer {
|
||||
RadioTrackingService(appScope, manager, satelliteRepo, settingsRepo)
|
||||
}
|
||||
|
||||
private val _mutualPassData = MutableStateFlow(MutualPassData())
|
||||
override val mutualPassData: StateFlow<MutualPassData> = _mutualPassData.asStateFlow()
|
||||
|
||||
override fun setMutualPassData(data: MutualPassData) {
|
||||
_mutualPassData.value = data
|
||||
}
|
||||
|
||||
override fun provideAddToCalendar(): IAddToCalendar = AddToCalendar(context)
|
||||
|
||||
override fun provideShowToast(): IShowToast = ShowToast(context)
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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.core.domain.cw
|
||||
|
||||
/**
|
||||
* Bayesian Morse timing decoder.
|
||||
* Replaces hard thresholds with probability-based decision making.
|
||||
*
|
||||
* Inspired by VE3NEA's CW Skimmer approach:
|
||||
* "Instead of making a hard decision at every input sample whether the signal
|
||||
* is present or not, compute the probability that the signal is present."
|
||||
*
|
||||
* Uses Gaussian probability density centered on expected durations:
|
||||
* P(dit | duration) = exp(-(duration - dotMs)^2 / (2 * variance^2))
|
||||
* P(dash | duration) = exp(-(duration - 3*dotMs)^2 / (2 * variance^2))
|
||||
*/
|
||||
internal class CwBayesianDecoder {
|
||||
|
||||
// Morse timing parameters
|
||||
private var dotDurationMs = 60f // initial 20 WPM
|
||||
private var speedWpm = 20f
|
||||
|
||||
// Current symbol being accumulated
|
||||
private var currentSymbol = StringBuilder()
|
||||
private var textBuffer = StringBuilder()
|
||||
|
||||
// Recent dit lengths for speed estimation
|
||||
private val recentDits = mutableListOf<Float>()
|
||||
|
||||
// Output
|
||||
private var _decodedText = ""
|
||||
val decodedText: String get() = _decodedText
|
||||
|
||||
/** Gaussian probability. */
|
||||
private fun gaussianProb(durationMs: Float, expectedMs: Float, varianceMs: Float): Float {
|
||||
if (varianceMs <= 0f) return 0f
|
||||
val diff = durationMs - expectedMs
|
||||
return kotlin.math.exp(-(diff * diff) / (2 * varianceMs * varianceMs))
|
||||
}
|
||||
|
||||
/** Process a tone duration. Returns the symbol type with highest probability. */
|
||||
fun processTone(durationMs: Float): ToneResult {
|
||||
val ditProb = gaussianProb(durationMs, dotDurationMs, dotDurationMs * 0.4f)
|
||||
val dashProb = gaussianProb(durationMs, dotDurationMs * 3f, dotDurationMs * 0.6f)
|
||||
|
||||
return if (ditProb > dashProb && ditProb > 0.05f) {
|
||||
currentSymbol.append('0')
|
||||
recentDits.add(durationMs)
|
||||
updateSpeed()
|
||||
ToneResult('0', ditProb)
|
||||
} else if (dashProb > 0.05f) {
|
||||
currentSymbol.append('1')
|
||||
ToneResult('1', dashProb)
|
||||
} else {
|
||||
ToneResult(null, 0f)
|
||||
}
|
||||
}
|
||||
|
||||
/** Process a gap duration. Returns decoded character or null. */
|
||||
fun processGap(durationMs: Float): Char? {
|
||||
if (currentSymbol.isEmpty()) {
|
||||
val wordProb = gaussianProb(durationMs, dotDurationMs * 7f, dotDurationMs * 1.2f)
|
||||
if (wordProb > 0.2f) {
|
||||
textBuffer.append(' ')
|
||||
_decodedText = textBuffer.toString()
|
||||
return ' '
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val interCharProb = gaussianProb(durationMs, dotDurationMs * 3f, dotDurationMs * 0.6f)
|
||||
val wordProb = gaussianProb(durationMs, dotDurationMs * 7f, dotDurationMs * 1.2f)
|
||||
|
||||
if (wordProb > interCharProb && wordProb > 0.2f) {
|
||||
val char = flushSymbol()
|
||||
textBuffer.append(' ')
|
||||
_decodedText = textBuffer.toString()
|
||||
return char
|
||||
}
|
||||
if (interCharProb > 0.15f) {
|
||||
val char = flushSymbol()
|
||||
_decodedText = textBuffer.toString()
|
||||
return char
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun flushSymbol(): Char? {
|
||||
if (currentSymbol.isEmpty()) return null
|
||||
val morse = currentSymbol.toString()
|
||||
currentSymbol.clear()
|
||||
val char = morseToChar(morse)
|
||||
if (char != null) textBuffer.append(char)
|
||||
return char
|
||||
}
|
||||
|
||||
private fun updateSpeed() {
|
||||
if (recentDits.size < 3) return
|
||||
val sorted = recentDits.sorted()
|
||||
val median = sorted[sorted.size / 2]
|
||||
if (median > 0f) {
|
||||
dotDurationMs = dotDurationMs * 0.7f + median * 0.3f
|
||||
val wpm = 60.0f / (50.0f * dotDurationMs / 1000.0f)
|
||||
if (wpm in 5f..55f) speedWpm = wpm
|
||||
}
|
||||
}
|
||||
|
||||
fun getSpeed(): Float = speedWpm
|
||||
|
||||
fun reset() {
|
||||
dotDurationMs = 60f
|
||||
speedWpm = 20f
|
||||
recentDits.clear()
|
||||
currentSymbol.clear()
|
||||
textBuffer.clear()
|
||||
_decodedText = ""
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val MORSE_TABLE = mapOf(
|
||||
"01" to 'A', "1000" to 'B', "1010" to 'C', "100" to 'D', "0" to 'E',
|
||||
"0010" to 'F', "110" to 'G', "0000" to 'H', "00" to 'I', "0111" to 'J',
|
||||
"101" to 'K', "0100" to 'L', "11" to 'M', "10" to 'N', "111" to 'O',
|
||||
"0110" to 'P', "1101" to 'Q', "010" to 'R', "000" to 'S', "1" to 'T',
|
||||
"001" to 'U', "0001" to 'V', "011" to 'W', "1001" to 'X', "1011" to 'Y',
|
||||
"1100" to 'Z', "01111" to '1', "00111" to '2', "00011" to '3',
|
||||
"00001" to '4', "00000" to '5', "10000" to '6', "11000" to '7',
|
||||
"11100" to '8', "11110" to '9', "11111" to '0',
|
||||
"010101" to '.', "110011" to ',', "001100" to '?', "011110" to '\'',
|
||||
"101011" to '!', "10010" to '/', "10110" to '(', "101101" to ')',
|
||||
"01000" to '&', "111000" to ':', "101010" to ';', "10001" to '=',
|
||||
"01010" to '+', "100001" to '-', "001101" to '_', "010010" to '"',
|
||||
"0001001" to '$', "011010" to '@'
|
||||
)
|
||||
fun morseToChar(morse: String): Char? = MORSE_TABLE[morse]
|
||||
}
|
||||
}
|
||||
|
||||
data class ToneResult(val symbol: Char?, val probability: Float)
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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.core.domain.cw
|
||||
|
||||
/**
|
||||
* Multi-channel CW signal tracker.
|
||||
* Monitors the spectrogram for active frequency bins and extracts
|
||||
* energy envelopes for each detected signal.
|
||||
*
|
||||
* Inspired by CW Skimmer's multi-channel approach:
|
||||
* tracks all active signals in the passband simultaneously,
|
||||
* selects the best one for decoded output.
|
||||
*/
|
||||
internal class CwChannelTracker(
|
||||
private val spectrogram: CwSpectrogram,
|
||||
private val maxChannels: Int = 3
|
||||
) {
|
||||
data class Channel(
|
||||
val bin: Int,
|
||||
val frequency: Float,
|
||||
var active: Boolean = false,
|
||||
var energy: Float = 0f,
|
||||
val history: MutableList<Float> = mutableListOf(),
|
||||
var confidence: Float = 0f
|
||||
)
|
||||
|
||||
private val channels = Array(maxChannels) { Channel(0, 0f) }
|
||||
|
||||
/** Scan the current spectrogram column and update channel tracking. */
|
||||
fun update(): List<Channel> {
|
||||
val col = spectrogram.getCurrentColumn()
|
||||
val peaks = findPeaks(col, threshold = 0.3f, minDistance = 2)
|
||||
|
||||
// Update existing channels
|
||||
for (ch in channels) {
|
||||
if (ch.active) {
|
||||
if (peaks.contains(ch.bin)) {
|
||||
ch.energy = col[ch.bin]
|
||||
ch.history.add(ch.energy)
|
||||
if (ch.history.size > 40) ch.history.removeAt(0)
|
||||
ch.confidence = computeConfidence(ch.history)
|
||||
} else {
|
||||
// Signal lost — decay confidence
|
||||
ch.history.add(0f)
|
||||
if (ch.history.size > 40) ch.history.removeAt(0)
|
||||
ch.confidence *= 0.9f
|
||||
if (ch.confidence < 0.1f) ch.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assign new peaks to inactive channels
|
||||
var peakIdx = 0
|
||||
for (ch in channels) {
|
||||
if (!ch.active && peakIdx < peaks.size) {
|
||||
val bin = peaks[peakIdx]
|
||||
val freq = spectrogram.binToFreq(bin)
|
||||
// Re-initialize channel
|
||||
channels[peakIdx] = Channel(bin, freq, true, col[bin], mutableListOf(), 0.5f)
|
||||
peakIdx++
|
||||
}
|
||||
}
|
||||
|
||||
return channels.filter { it.active }
|
||||
}
|
||||
|
||||
/** Find peak bins in the spectrum. */
|
||||
private fun findPeaks(spectrum: FloatArray, threshold: Float, minDistance: Int): List<Int> {
|
||||
val peaks = mutableListOf<Int>()
|
||||
for (i in 1 until spectrum.size - 1) {
|
||||
if (spectrum[i] > spectrum[i - 1] && spectrum[i] > spectrum[i + 1] && spectrum[i] > threshold) {
|
||||
if (peaks.isEmpty() || i - peaks.last() >= minDistance) {
|
||||
peaks.add(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
return peaks.sortedByDescending { spectrum[it] }
|
||||
}
|
||||
|
||||
/** Compute confidence from energy history. Lower variance = higher confidence. */
|
||||
private fun computeConfidence(history: List<Float>): Float {
|
||||
if (history.size < 10) return 0.3f
|
||||
val recent = history.takeLast(10)
|
||||
val mean = recent.average().toFloat()
|
||||
val variance = recent.map { (it - mean) * (it - mean) }.average().toFloat()
|
||||
return if (mean > 0f) (mean / (mean + variance + 0.1f)).coerceIn(0f, 1f) else 0f
|
||||
}
|
||||
|
||||
/** Get the channel with highest confidence. */
|
||||
fun getBestChannel(): Channel? {
|
||||
return channels.filter { it.active }.maxByOrNull { it.confidence }
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
for (i in channels.indices) {
|
||||
channels[i] = Channel(0, 0f)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* 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.core.domain.cw
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* CW (Morse code) decoder v3 — Spectrogram-based multi-channel Bayesian decoder.
|
||||
*
|
||||
* Architecture inspired by Morse Expert / CW Skimmer (VE3NEA):
|
||||
* 1. FFT spectrogram creates a frequency×time matrix
|
||||
* 2. Multi-channel peak detector finds all active signals
|
||||
* 3. Per-channel energy envelope extraction
|
||||
* 4. Bayesian probability for symbol timing (Gaussian likelihood)
|
||||
* 5. Best channel selected for output
|
||||
*
|
||||
* Timing analysis is performed per spectrogram column (hop).
|
||||
* Each column represents hopSize/sampleRate seconds of audio.
|
||||
*/
|
||||
class CwDecoder(
|
||||
val sampleRate: Int = 8000,
|
||||
cwToneFreq: Float = -1f // ignored in v3 (auto-detect via spectrogram)
|
||||
) {
|
||||
companion object {
|
||||
private const val FFT_SIZE = 256
|
||||
private const val HOP_SIZE = 64
|
||||
}
|
||||
|
||||
private val spectrogram = CwSpectrogram(
|
||||
fftSize = FFT_SIZE,
|
||||
hopSize = HOP_SIZE,
|
||||
sampleRate = sampleRate,
|
||||
minBin = 6,
|
||||
maxBin = 38,
|
||||
historyCols = 40
|
||||
)
|
||||
private val channelTracker = CwChannelTracker(spectrogram, maxChannels = 3)
|
||||
private val bayesianDecoder = CwBayesianDecoder()
|
||||
|
||||
// Timing state per channel
|
||||
private data class ChannelTiming(
|
||||
var isSignal: Boolean = false,
|
||||
var toneTicks: Int = 0,
|
||||
var gapTicks: Int = 0
|
||||
)
|
||||
private val timingStates = Array(3) { ChannelTiming() }
|
||||
|
||||
// Time per spectrogram column in milliseconds
|
||||
private val tickMs = 1000f * HOP_SIZE / sampleRate
|
||||
|
||||
// Output flows
|
||||
private val _decodedTextFlow = MutableStateFlow("")
|
||||
val decodedTextFlow: StateFlow<String> = _decodedTextFlow
|
||||
|
||||
private val _signalStrength = MutableStateFlow(0f)
|
||||
val signalStrength: StateFlow<Float> = _signalStrength
|
||||
|
||||
private val _estimatedPitch = MutableStateFlow<Float?>(null)
|
||||
val estimatedPitch: StateFlow<Float?> = _estimatedPitch
|
||||
|
||||
private val _estimatedSpeed = MutableStateFlow<Float?>(null)
|
||||
val estimatedSpeed: StateFlow<Float?> = _estimatedSpeed
|
||||
|
||||
private var frameCount = 0
|
||||
|
||||
init {
|
||||
if (cwToneFreq > 0f) {
|
||||
_estimatedPitch.value = cwToneFreq
|
||||
}
|
||||
}
|
||||
|
||||
fun processBuffer(buffer: FloatArray) {
|
||||
// 1. Feed samples to spectrogram
|
||||
spectrogram.addSamples(buffer)
|
||||
|
||||
// 2. Get number of new columns generated
|
||||
val newCols = spectrogram.getNewColumns()
|
||||
if (newCols == 0) return
|
||||
|
||||
// 3. Update channel tracker (uses latest column for peak detection)
|
||||
val activeChannels = channelTracker.update()
|
||||
|
||||
// 4. Process each new column for timing analysis
|
||||
// Columns are indexed 0..historyCols-1, where historyCols-1 is the newest
|
||||
val baseIdx = (spectrogram.historyCols - newCols).coerceAtLeast(0)
|
||||
for (colOffset in 0 until newCols) {
|
||||
val col = spectrogram.getColumn(baseIdx + colOffset)
|
||||
|
||||
for ((idx, channel) in activeChannels.withIndex()) {
|
||||
if (idx >= timingStates.size) break
|
||||
val state = timingStates[idx]
|
||||
val energy = if (channel.bin in col.indices) col[channel.bin] else 0f
|
||||
|
||||
// Adaptive threshold
|
||||
val threshold = 0.3f + (energy - 0.3f) * 0.3f
|
||||
|
||||
if (energy > threshold) {
|
||||
if (!state.isSignal) {
|
||||
if (state.gapTicks > 0) {
|
||||
val gapMs = state.gapTicks * tickMs
|
||||
bayesianDecoder.processGap(gapMs)
|
||||
}
|
||||
state.gapTicks = 0
|
||||
state.isSignal = true
|
||||
}
|
||||
state.toneTicks++
|
||||
} else {
|
||||
if (state.isSignal) {
|
||||
if (state.toneTicks > 0) {
|
||||
val toneMs = state.toneTicks * tickMs
|
||||
bayesianDecoder.processTone(toneMs)
|
||||
}
|
||||
state.toneTicks = 0
|
||||
state.isSignal = false
|
||||
}
|
||||
state.gapTicks++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Update outputs
|
||||
frameCount++
|
||||
if (frameCount % 5 == 0) {
|
||||
val bestChannel = channelTracker.getBestChannel()
|
||||
if (bestChannel != null) {
|
||||
_estimatedPitch.value = bestChannel.frequency
|
||||
_signalStrength.value = bestChannel.confidence
|
||||
_estimatedSpeed.value = bayesianDecoder.getSpeed()
|
||||
}
|
||||
_decodedTextFlow.value = bayesianDecoder.decodedText
|
||||
}
|
||||
}
|
||||
|
||||
fun resetDecoder() {
|
||||
spectrogram.reset()
|
||||
channelTracker.reset()
|
||||
bayesianDecoder.reset()
|
||||
for (state in timingStates) {
|
||||
state.isSignal = false
|
||||
state.toneTicks = 0
|
||||
state.gapTicks = 0
|
||||
}
|
||||
frameCount = 0
|
||||
_decodedTextFlow.value = ""
|
||||
_signalStrength.value = 0f
|
||||
_estimatedPitch.value = null
|
||||
_estimatedSpeed.value = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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.core.domain.cw
|
||||
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* DSP utilities for CW (Morse code) decoding.
|
||||
* Pure Kotlin, no NDK required.
|
||||
*/
|
||||
internal object CwDsp {
|
||||
|
||||
/**
|
||||
* Design a simple bandpass FIR filter coefficients using windowed sinc method.
|
||||
* @param lowCutoff lower cutoff frequency (Hz) as fraction of sampleRate
|
||||
* @param highCutoff upper cutoff frequency (Hz) as fraction of sampleRate
|
||||
* @param taps filter length (must be odd)
|
||||
*/
|
||||
fun bandpassFir(lowCutoff: Double, highCutoff: Double, taps: Int): FloatArray {
|
||||
val n = if (taps % 2 == 0) taps + 1 else taps
|
||||
val half = n / 2
|
||||
val coeffs = FloatArray(n)
|
||||
for (i in 0 until n) {
|
||||
val idx = i - half
|
||||
if (idx == 0) {
|
||||
coeffs[i] = (2.0 * (highCutoff - lowCutoff)).toFloat()
|
||||
} else {
|
||||
val x = PI * idx
|
||||
coeffs[i] = ((sin(2 * highCutoff * x) - sin(2 * lowCutoff * x)) / x).toFloat()
|
||||
}
|
||||
// Hamming window
|
||||
coeffs[i] = (coeffs[i] * (0.54 - 0.46 * cos(2 * PI * i / (n - 1)))).toFloat()
|
||||
}
|
||||
// Normalize
|
||||
val sum = coeffs.sum()
|
||||
if (sum != 0f) for (i in 0 until n) coeffs[i] /= sum
|
||||
return coeffs
|
||||
}
|
||||
|
||||
/** Apply FIR filter to a buffer. */
|
||||
fun applyFir(buffer: FloatArray, coeffs: FloatArray): FloatArray {
|
||||
val out = FloatArray(buffer.size)
|
||||
for (i in buffer.indices) {
|
||||
var sum = 0f
|
||||
for (j in coeffs.indices) {
|
||||
val idx = i - j
|
||||
if (idx >= 0) sum += buffer[idx] * coeffs[j]
|
||||
}
|
||||
out[i] = sum
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** Simple envelope detector: abs + low-pass smoothing. */
|
||||
fun envelope(signal: FloatArray, alpha: Float = 0.1f): FloatArray {
|
||||
val env = FloatArray(signal.size)
|
||||
var s = 0f
|
||||
for (i in signal.indices) {
|
||||
s = alpha * kotlin.math.abs(signal[i]) + (1 - alpha) * s
|
||||
env[i] = s
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
/** Estimate noise floor from envelope for adaptive thresholding. */
|
||||
fun noiseFloor(env: FloatArray, fraction: Float = 0.3f): Float {
|
||||
val sorted = env.sortedArray()
|
||||
val median = sorted[sorted.size / 2]
|
||||
return median + (sorted[sorted.size * 9 / 10] - median) * fraction
|
||||
}
|
||||
|
||||
/** Simple Goertzel to detect a specific tone frequency. */
|
||||
fun goertzel(buffer: FloatArray, targetFreq: Float, sampleRate: Int): Float {
|
||||
val omega = 2.0 * PI * targetFreq / sampleRate
|
||||
val coeff = 2.0 * cos(omega)
|
||||
var s0 = 0.0; var s1 = 0.0; var s2 = 0.0
|
||||
for (sample in buffer) {
|
||||
s0 = sample.toDouble() + coeff * s1 - s2
|
||||
s2 = s1; s1 = s0
|
||||
}
|
||||
val power = s2 * s2 + s1 * s1 - coeff * s1 * s2
|
||||
return sqrt(kotlin.math.abs(power)).toFloat()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.core.domain.cw
|
||||
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* Radix-2 FFT for real-valued input.
|
||||
* Produces magnitude spectrum for the first N/2+1 bins.
|
||||
* Used by CwSpectrogram for time-frequency analysis.
|
||||
*/
|
||||
internal class CwFFT(private val n: Int) {
|
||||
init {
|
||||
require(n > 0 && n and (n - 1) == 0) { "FFT size must be power of 2, got $n" }
|
||||
}
|
||||
|
||||
private val cosTable = FloatArray(n / 2)
|
||||
private val sinTable = FloatArray(n / 2)
|
||||
|
||||
init {
|
||||
for (i in 0 until n / 2) {
|
||||
val angle = -2.0 * kotlin.math.PI * i / n
|
||||
cosTable[i] = cos(angle).toFloat()
|
||||
sinTable[i] = kotlin.math.sin(angle).toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
/** Compute magnitude spectrum for real input. Returns array of size n/2+1. */
|
||||
fun magnitudeSpectrum(input: FloatArray): FloatArray {
|
||||
require(input.size == n) { "Input size must be $n, got ${input.size}" }
|
||||
|
||||
val real = input.copyOf()
|
||||
val imag = FloatArray(n)
|
||||
|
||||
// Bit-reversal permutation
|
||||
var j = 0
|
||||
for (i in 1 until n) {
|
||||
var bit = n shr 1
|
||||
while (j and bit != 0) { j = j xor bit; bit = bit shr 1 }
|
||||
j = j xor bit
|
||||
if (i < j) {
|
||||
var tmp = real[i]; real[i] = real[j]; real[j] = tmp
|
||||
}
|
||||
}
|
||||
|
||||
// Radix-2 Cooley-Tukey FFT
|
||||
var len = 2
|
||||
while (len <= n) {
|
||||
val half = len / 2
|
||||
val step = n / len
|
||||
for (i in 0 until n step len) {
|
||||
for (k in 0 until half) {
|
||||
val tReal = real[i + k + half] * cosTable[k * step] - imag[i + k + half] * sinTable[k * step]
|
||||
val tImag = real[i + k + half] * sinTable[k * step] + imag[i + k + half] * cosTable[k * step]
|
||||
real[i + k + half] = real[i + k] - tReal
|
||||
imag[i + k + half] = imag[i + k] - tImag
|
||||
real[i + k] += tReal
|
||||
imag[i + k] += tImag
|
||||
}
|
||||
}
|
||||
len = len shl 1
|
||||
}
|
||||
|
||||
// Magnitude spectrum (first N/2+1 bins)
|
||||
val mag = FloatArray(n / 2 + 1)
|
||||
for (i in 0..n / 2) {
|
||||
mag[i] = sqrt(real[i] * real[i] + imag[i] * imag[i]) / n
|
||||
}
|
||||
return mag
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.core.domain.cw
|
||||
|
||||
/**
|
||||
* First-order IIR filters.
|
||||
* Ported from ggmorse/src/filter.h
|
||||
*/
|
||||
internal class CwFilter {
|
||||
private var z1 = 0f
|
||||
|
||||
companion object {
|
||||
private const val PI_F = 3.141592653589793f
|
||||
}
|
||||
|
||||
fun highPass(sample: Float, cutoffHz: Float, sampleRate: Float): Float {
|
||||
val rc = 1.0f / (2f * PI_F * cutoffHz)
|
||||
val dt = 1.0f / sampleRate
|
||||
val alpha = dt / (rc + dt)
|
||||
z1 = alpha * (z1 + sample - z1)
|
||||
return sample - z1
|
||||
}
|
||||
|
||||
fun lowPass(sample: Float, cutoffHz: Float, sampleRate: Float): Float {
|
||||
val rc = 1.0f / (2f * PI_F * cutoffHz)
|
||||
val dt = 1.0f / sampleRate
|
||||
val alpha = dt / (rc + dt)
|
||||
z1 += alpha * (sample - z1)
|
||||
return z1
|
||||
}
|
||||
|
||||
fun reset() { z1 = 0f }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.core.domain.cw
|
||||
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* Running Goertzel filter for CW tone detection.
|
||||
* Tracks a specific frequency over time with a sliding window.
|
||||
* Ported from ggmorse/src/goertzel.h
|
||||
*/
|
||||
internal class CwGoertzel {
|
||||
private var s1 = 0.0
|
||||
private var s2 = 0.0
|
||||
private var coeff = 0.0
|
||||
|
||||
fun init(sampleRate: Float, targetFreq: Float) {
|
||||
val omega = 2.0 * kotlin.math.PI * targetFreq / sampleRate
|
||||
coeff = 2.0 * cos(omega)
|
||||
s1 = 0.0
|
||||
s2 = 0.0
|
||||
}
|
||||
|
||||
fun process(sample: Float) {
|
||||
val s0 = sample.toDouble() + coeff * s1 - s2
|
||||
s2 = s1
|
||||
s1 = s0
|
||||
}
|
||||
|
||||
fun getPower(): Float {
|
||||
return sqrt(s2 * s2 + s1 * s1 - coeff * s1 * s2).toFloat()
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
s1 = 0.0
|
||||
s2 = 0.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.core.domain.cw
|
||||
|
||||
/**
|
||||
* Simple linear resampler.
|
||||
* Downsamples from input sample rate to output sample rate.
|
||||
* Ported from ggmorse/src/resampler.h
|
||||
*/
|
||||
internal class CwResampler(private val inputRate: Float, private val outputRate: Float) {
|
||||
private val ratio = inputRate / outputRate
|
||||
private var lastSample = 0f
|
||||
|
||||
fun process(input: FloatArray): FloatArray {
|
||||
if (ratio <= 0f || input.isEmpty()) return input
|
||||
val outputLen = (input.size / ratio).toInt() + 1
|
||||
val output = FloatArray(outputLen)
|
||||
var idx = 0f
|
||||
for (i in output.indices) {
|
||||
val intIdx = idx.toInt()
|
||||
val frac = idx - intIdx
|
||||
if (intIdx + 1 < input.size) {
|
||||
output[i] = input[intIdx] * (1 - frac) + input[intIdx + 1] * frac
|
||||
} else if (intIdx < input.size) {
|
||||
output[i] = input[intIdx] * (1 - frac) + lastSample * frac
|
||||
} else {
|
||||
output[i] = lastSample
|
||||
}
|
||||
idx += ratio
|
||||
}
|
||||
lastSample = input.lastOrNull() ?: lastSample
|
||||
return output
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
lastSample = 0f
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.core.domain.cw
|
||||
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* Lightweight pitch detector using DFT at specific frequency bins.
|
||||
* Only scans [200, 1200] Hz in configurable steps — much faster than full FFT.
|
||||
* Ported from ggmorse/src/stfft.h (simplified for CW use case).
|
||||
*/
|
||||
internal class CwPitchDetector(
|
||||
private val sampleRate: Float,
|
||||
private val minFreq: Float = 200f,
|
||||
private val maxFreq: Float = 1200f,
|
||||
private val stepHz: Float = 10f
|
||||
) {
|
||||
/**
|
||||
* Find the dominant pitch frequency in the buffer.
|
||||
* Returns null if no significant pitch found.
|
||||
*/
|
||||
fun findPitch(buffer: FloatArray): Float? {
|
||||
if (buffer.isEmpty()) return null
|
||||
var bestFreq = 0f
|
||||
var bestPower = 0f
|
||||
var freq = minFreq
|
||||
while (freq <= maxFreq) {
|
||||
var real = 0.0
|
||||
var imag = 0.0
|
||||
val omega = 2.0 * kotlin.math.PI * freq / sampleRate
|
||||
for (i in buffer.indices) {
|
||||
real += buffer[i] * cos(omega * i)
|
||||
imag += buffer[i] * -sin(omega * i)
|
||||
}
|
||||
val power = (real * real + imag * imag).toFloat()
|
||||
if (power > bestPower) {
|
||||
bestPower = power
|
||||
bestFreq = freq
|
||||
}
|
||||
freq += stepHz
|
||||
}
|
||||
return if (bestPower > 0.001f) bestFreq else null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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.core.domain.cw
|
||||
|
||||
/**
|
||||
* Sliding-window spectrogram for CW decoding.
|
||||
* Maintains a time-frequency matrix updated with each audio frame.
|
||||
*
|
||||
* FFT size: 256, hop size: 64, sample rate: 4000 (or native)
|
||||
* Frequency bins: 6..38 (187-1187 Hz, covers typical CW range)
|
||||
* History: 40 columns (320 ms window)
|
||||
* Time resolution: 64/4000 = 16 ms, Frequency resolution: 4000/256 = 15.625 Hz
|
||||
*/
|
||||
internal class CwSpectrogram(
|
||||
private val fftSize: Int = 256,
|
||||
private val hopSize: Int = 64,
|
||||
private val sampleRate: Int = 4000,
|
||||
private val minBin: Int = 6,
|
||||
private val maxBin: Int = 38,
|
||||
val historyCols: Int = 40
|
||||
) {
|
||||
private val fft = CwFFT(fftSize)
|
||||
val numBins: Int get() = maxBin - minBin + 1
|
||||
|
||||
// Hanning window
|
||||
private val hanning = FloatArray(fftSize) {
|
||||
(0.5 - 0.5 * kotlin.math.cos(2.0 * kotlin.math.PI * it / (fftSize - 1))).toFloat()
|
||||
}
|
||||
|
||||
// Spectrogram data: [timeCol][freqBin]
|
||||
private val spectrogram = Array(historyCols) { FloatArray(numBins) }
|
||||
private var currentCol = 0
|
||||
private var samplesBuffered = 0
|
||||
private val buffer = FloatArray(fftSize)
|
||||
|
||||
// Per-bin running energy for normalization
|
||||
private val binEnergy = FloatArray(numBins) { 1f }
|
||||
private val alpha = 0.95f
|
||||
|
||||
// Counter for new columns generated since last check
|
||||
private var newColumnCount = 0
|
||||
|
||||
/** Add audio samples, compute FFTs for each complete hop. */
|
||||
fun addSamples(samples: FloatArray) {
|
||||
var offset = 0
|
||||
while (offset < samples.size) {
|
||||
val needed = fftSize - samplesBuffered
|
||||
val copyLen = minOf(needed, samples.size - offset)
|
||||
System.arraycopy(samples, offset, buffer, samplesBuffered, copyLen)
|
||||
samplesBuffered += copyLen
|
||||
offset += copyLen
|
||||
|
||||
if (samplesBuffered >= fftSize) {
|
||||
processFrame()
|
||||
newColumnCount++
|
||||
// Shift buffer: keep last (fftSize - hopSize) samples
|
||||
System.arraycopy(buffer, hopSize, buffer, 0, fftSize - hopSize)
|
||||
samplesBuffered = fftSize - hopSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Get number of new columns generated since the last call to this method. */
|
||||
fun getNewColumns(): Int {
|
||||
val count = newColumnCount
|
||||
newColumnCount = 0
|
||||
return count
|
||||
}
|
||||
|
||||
private fun processFrame() {
|
||||
// Apply Hanning window
|
||||
val windowed = FloatArray(fftSize) { buffer[it] * hanning[it] }
|
||||
|
||||
// Compute FFT magnitude spectrum
|
||||
val mag = fft.magnitudeSpectrum(windowed)
|
||||
|
||||
// Update spectrogram column
|
||||
val col = spectrogram[currentCol]
|
||||
for (b in 0 until numBins) {
|
||||
val binIdx = minBin + b
|
||||
val rawMag = mag[binIdx]
|
||||
// Running energy normalization
|
||||
binEnergy[b] = alpha * binEnergy[b] + (1 - alpha) * rawMag
|
||||
col[b] = if (binEnergy[b] > 1e-6f) rawMag / binEnergy[b] else 0f
|
||||
}
|
||||
|
||||
currentCol = (currentCol + 1) % historyCols
|
||||
}
|
||||
|
||||
/** Get the current spectrogram as a 2D array in chronological order. */
|
||||
fun getSpectrogram(): Array<FloatArray> {
|
||||
val result = Array(historyCols) { i ->
|
||||
val srcIdx = (currentCol + i) % historyCols
|
||||
spectrogram[srcIdx].copyOf()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** Get the most recent column (current energy across all frequencies). */
|
||||
fun getCurrentColumn(): FloatArray {
|
||||
val prevCol = (currentCol - 1 + historyCols) % historyCols
|
||||
return spectrogram[prevCol].copyOf()
|
||||
}
|
||||
|
||||
/** Get a column by index from the history (0 = oldest, historyCols-1 = newest). */
|
||||
fun getColumn(index: Int): FloatArray {
|
||||
val clamped = index.coerceIn(0, historyCols - 1)
|
||||
val srcIdx = (currentCol - historyCols + clamped + historyCols) % historyCols
|
||||
return spectrogram[srcIdx].copyOf()
|
||||
}
|
||||
|
||||
/** Find the frequency bin with peak energy. Returns -1 if no significant signal. */
|
||||
fun findPeakBin(): Int {
|
||||
val col = getCurrentColumn()
|
||||
var maxBin = -1
|
||||
var maxVal = 0f
|
||||
for (i in col.indices) {
|
||||
if (col[i] > maxVal) {
|
||||
maxVal = col[i]
|
||||
maxBin = i
|
||||
}
|
||||
}
|
||||
return if (maxVal > 0.3f) maxBin else -1
|
||||
}
|
||||
|
||||
/** Get energy at a specific bin over the last N columns in chronological order. */
|
||||
fun getBinEnergy(bin: Int, numCols: Int): FloatArray {
|
||||
val clamped = minOf(numCols, historyCols)
|
||||
val result = FloatArray(clamped)
|
||||
for (i in 0 until clamped) {
|
||||
val colIdx = (currentCol - clamped + i + historyCols) % historyCols
|
||||
result[i] = spectrogram[colIdx][bin]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** Get the bin index for a frequency in Hz. */
|
||||
fun freqToBin(freqHz: Float): Int {
|
||||
val bin = (freqHz * fftSize / sampleRate).toInt()
|
||||
return (bin - minBin).coerceIn(0, numBins - 1)
|
||||
}
|
||||
|
||||
/** Get the center frequency for a bin. */
|
||||
fun binToFreq(bin: Int): Float {
|
||||
return (minBin + bin).toFloat() * sampleRate / fftSize
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
for (col in spectrogram) col.fill(0f)
|
||||
currentCol = 0
|
||||
samplesBuffered = 0
|
||||
buffer.fill(0f)
|
||||
binEnergy.fill(1f)
|
||||
}
|
||||
}
|
||||
+23
-1
@@ -16,12 +16,13 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.domain.repository
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IAddToCalendar
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IAudioCapture
|
||||
import com.rtbishop.look4sat.core.domain.usecase.ISaveImage
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface IMainContainer {
|
||||
val appScope: CoroutineScope
|
||||
@@ -30,6 +31,8 @@ interface IMainContainer {
|
||||
val satelliteRepo: ISatelliteRepo
|
||||
val databaseRepo: IDatabaseRepo
|
||||
val radioTrackingService: IRadioTrackingService
|
||||
val mutualPassData: StateFlow<MutualPassData>
|
||||
fun setMutualPassData(data: MutualPassData)
|
||||
fun provideAddToCalendar(): IAddToCalendar
|
||||
fun provideShowToast(): IShowToast
|
||||
fun provideBluetoothReporter(): IReporter
|
||||
@@ -41,6 +44,25 @@ interface IMainContainer {
|
||||
fun provideSaveImage(): ISaveImage
|
||||
}
|
||||
|
||||
data class MutualPassData(
|
||||
val samples: List<Pair<Long, Pair<Double, Double>>> = emptyList(),
|
||||
val trackSamples: List<TrackSampleData> = emptyList(),
|
||||
val startTime: Long = 0L,
|
||||
val endTime: Long = 0L,
|
||||
val maxElev: Double = 10.0,
|
||||
val labelA: String = "你",
|
||||
val labelB: String = "友台"
|
||||
)
|
||||
|
||||
/** Minimal track sample for cross-module sharing (angles in degrees). */
|
||||
data class TrackSampleData(
|
||||
val time: Long = 0L,
|
||||
val azimuthA: Double,
|
||||
val elevationA: Double,
|
||||
val azimuthB: Double,
|
||||
val elevationB: Double
|
||||
)
|
||||
|
||||
interface IContainerProvider {
|
||||
fun getMainContainer(): IMainContainer
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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.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:
|
||||
*
|
||||
* 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 (逆上行多普勒)
|
||||
*
|
||||
* 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: ④→③→②→①
|
||||
*/
|
||||
fun computeUplinkFromDownlink(
|
||||
downlinkHz: Long,
|
||||
transponder: SatRadio,
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
fun computeUplinkFromDownlinkWithOffset(
|
||||
downlinkHz: Long,
|
||||
transponder: SatRadio,
|
||||
orbitalPos: OrbitalPos,
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an uplink frequency (what the user transmits), compute the
|
||||
* downlink frequency the user will hear.
|
||||
* Full path: ①→②→③→④
|
||||
*/
|
||||
fun computeDownlinkFromUplink(
|
||||
uplinkHz: Long,
|
||||
transponder: SatRadio,
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: ①→②→③→④
|
||||
*/
|
||||
fun computeDownlinkFromUplinkWithOffset(
|
||||
uplinkHz: Long,
|
||||
transponder: SatRadio,
|
||||
orbitalPos: OrbitalPos,
|
||||
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)
|
||||
}
|
||||
|
||||
/** True if this transponder supports linear passband mapping. */
|
||||
fun isLinearTransponder(transponder: SatRadio): Boolean {
|
||||
val upLow = transponder.uplinkLow
|
||||
val upHigh = transponder.uplinkHigh
|
||||
val downLow = transponder.downlinkLow
|
||||
val downHigh = transponder.downlinkHigh
|
||||
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)
|
||||
}
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
package com.rtbishop.look4sat.core.domain
|
||||
|
||||
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 org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
class DopplerFrequencyCalculatorTest {
|
||||
|
||||
private fun linearTransponder(
|
||||
upLow: Long = 145_000_000L,
|
||||
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"
|
||||
) = SatRadio(
|
||||
uuid = "linear", info = info, isAlive = true,
|
||||
downlinkLow = downLow, downlinkHigh = downHigh,
|
||||
downlinkMode = downlinkMode, uplinkLow = upLow, uplinkHigh = upHigh,
|
||||
uplinkMode = uplinkMode, isInverted = inverted, catnum = 12345
|
||||
)
|
||||
|
||||
private fun fmTransponder() = SatRadio(
|
||||
uuid = "fm", info = "FM Repeater", isAlive = true,
|
||||
downlinkLow = 435_600_000L, downlinkHigh = null,
|
||||
downlinkMode = "FM", uplinkLow = 145_900_000L, uplinkHigh = null,
|
||||
uplinkMode = "FM", isInverted = false, catnum = 99999
|
||||
)
|
||||
|
||||
private fun pos(distanceRateKmS: Double = 0.0) = OrbitalPos().apply {
|
||||
this.distanceRate = distanceRateKmS
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isLinearTransponder_returnsTrueForLinear() {
|
||||
assertTrue(DopplerFrequencyCalculator.isLinearTransponder(linearTransponder()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isLinearTransponder_returnsFalseForFM() {
|
||||
assertFalse(DopplerFrequencyCalculator.isLinearTransponder(fmTransponder()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isLinearTransponder_returnsFalseForNullDownlinkHigh() {
|
||||
val xpdr = linearTransponder(downHigh = null)
|
||||
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()
|
||||
val orbitalPos = pos(0.0)
|
||||
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlink(435_200_000L, xpdr, orbitalPos)
|
||||
assertNotNull(uplink)
|
||||
assertTrue(uplink!! > 0)
|
||||
// With zero Doppler, result equals mapDownlinkToUplink output
|
||||
assertEquals(145_200_000L, uplink)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeDownlinkFromUplink_linear_noDoppler() {
|
||||
val xpdr = linearTransponder()
|
||||
val orbitalPos = pos(0.0)
|
||||
val downlink = DopplerFrequencyCalculator.computeDownlinkFromUplink(145_200_000L, xpdr, orbitalPos)
|
||||
assertNotNull(downlink)
|
||||
assertEquals(435_200_000L, downlink)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeUplinkFromDownlink_withDoppler_positiveRangeRate() {
|
||||
// Satellite receding (positive range rate) → ground must transmit higher freq to compensate
|
||||
val xpdr = linearTransponder()
|
||||
val orbitalPos = pos(7.0) // ~7 km/s receding
|
||||
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlink(435_200_000L, xpdr, orbitalPos)
|
||||
assertNotNull(uplink)
|
||||
// Uplink freq should be Doppler shifted UP (compensating for receding)
|
||||
assertTrue(uplink!! > 145_200_000L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeUplinkFromDownlink_fm_transponder_returnsNull() {
|
||||
val orbitalPos = pos()
|
||||
val result = DopplerFrequencyCalculator.computeUplinkFromDownlink(435_600_000L, fmTransponder(), orbitalPos)
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeDownlinkFromUplink_fm_transponder_returnsNull() {
|
||||
val orbitalPos = pos()
|
||||
val result = DopplerFrequencyCalculator.computeDownlinkFromUplink(145_900_000L, fmTransponder(), orbitalPos)
|
||||
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)
|
||||
val orbitalPos = pos(0.0)
|
||||
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlink(435_200_000L, xpdr, orbitalPos)
|
||||
assertNotNull(uplink)
|
||||
// Inverted: offset from high end → maps to high end of uplink
|
||||
assertEquals(145_300_000L, uplink)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeUplinkFromDownlink_roundTrip() {
|
||||
// downlink → uplink → downlink should round-trip
|
||||
val xpdr = linearTransponder()
|
||||
val orbitalPos = pos(3.5)
|
||||
val originalDownlink = 435_250_000L
|
||||
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlink(originalDownlink, xpdr, orbitalPos)
|
||||
assertNotNull(uplink)
|
||||
val roundTripDownlink = DopplerFrequencyCalculator.computeDownlinkFromUplink(uplink!!, xpdr, orbitalPos)
|
||||
assertNotNull(roundTripDownlink)
|
||||
// Doppler round-trip: small residual due to freq-dependent Doppler
|
||||
val error = kotlin.math.abs(roundTripDownlink!! - originalDownlink)
|
||||
assertTrue("Round-trip error too large: $error", error < 10000)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* 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.core.domain.cw
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.sin
|
||||
|
||||
class CwDecoderTest {
|
||||
|
||||
// --- Morse table ---
|
||||
|
||||
@Test
|
||||
fun morseToChar_basicLetters() {
|
||||
assertEquals('A', CwBayesianDecoder.morseToChar("01"))
|
||||
assertEquals('S', CwBayesianDecoder.morseToChar("000"))
|
||||
assertEquals('O', CwBayesianDecoder.morseToChar("111"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun morseToChar_numbers() {
|
||||
assertEquals('1', CwBayesianDecoder.morseToChar("01111"))
|
||||
assertEquals('0', CwBayesianDecoder.morseToChar("11111"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun morseToChar_unknown_returnsNull() {
|
||||
assertNull(CwBayesianDecoder.morseToChar("......."))
|
||||
assertNull(CwBayesianDecoder.morseToChar(""))
|
||||
}
|
||||
|
||||
// --- FFT ---
|
||||
|
||||
@Test
|
||||
fun fft_magnitudeSpectrum_detectsTone() {
|
||||
val fft = CwFFT(256)
|
||||
val sampleRate = 8000f
|
||||
val freq = 700f
|
||||
val buffer = FloatArray(256) { (sin(2.0 * PI * freq * it / sampleRate)).toFloat() }
|
||||
val mag = fft.magnitudeSpectrum(buffer)
|
||||
// Peak should be at bin around 700 * 256 / 8000 ≈ 22.4
|
||||
var maxBin = 0
|
||||
var maxVal = 0f
|
||||
for (i in mag.indices) {
|
||||
if (mag[i] > maxVal) { maxVal = mag[i]; maxBin = i }
|
||||
}
|
||||
assertTrue("Peak bin $maxBin should be near 22", maxBin in 18..26)
|
||||
assertTrue("Peak value $maxVal should be positive", maxVal > 0.01f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fft_magnitudeSpectrum_silence_isFlat() {
|
||||
val fft = CwFFT(256)
|
||||
val buffer = FloatArray(256) { 0f }
|
||||
val mag = fft.magnitudeSpectrum(buffer)
|
||||
for (v in mag) assertEquals("Silence spectrum should be 0, got $v", 0f, v, 1e-6f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fft_rejectsWrongSize() {
|
||||
assertThrows(IllegalArgumentException::class.java) { CwFFT(100) }
|
||||
}
|
||||
|
||||
// --- Spectrogram ---
|
||||
|
||||
@Test
|
||||
fun spectrogram_addSamples_updatesEnergy() {
|
||||
val spec = CwSpectrogram(sampleRate = 8000)
|
||||
val freq = 700f
|
||||
// Feed multiple frames to stabilize energy normalization
|
||||
for (i in 0..5) {
|
||||
val buffer = FloatArray(256) { (sin(2.0 * PI * freq * it / 8000.0)).toFloat() }
|
||||
spec.addSamples(buffer)
|
||||
}
|
||||
val col = spec.getCurrentColumn()
|
||||
val peakBin = spec.findPeakBin()
|
||||
assertTrue("Peak bin $peakBin should be >= 0", peakBin >= 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun spectrogram_findPeakBin_returnsValidBin() {
|
||||
val spec = CwSpectrogram(sampleRate = 8000)
|
||||
// Add multiple frames of 700 Hz tone
|
||||
for (i in 0..5) {
|
||||
val buffer = FloatArray(256) { (sin(2.0 * PI * 700.0 * it / 8000.0)).toFloat() }
|
||||
spec.addSamples(buffer)
|
||||
}
|
||||
val peakBin = spec.findPeakBin()
|
||||
assertTrue("Peak bin should be >= 0, got $peakBin", peakBin >= 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun spectrogram_freqToBin_roundtrip() {
|
||||
val spec = CwSpectrogram(sampleRate = 8000)
|
||||
val freq = 700f
|
||||
val bin = spec.freqToBin(freq)
|
||||
val backFreq = spec.binToFreq(bin)
|
||||
assertTrue("Freq $freq → bin $bin → freq $backFreq", backFreq > 600f && backFreq < 800f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun spectrogram_getBinEnergy_returnsCorrectLength() {
|
||||
val spec = CwSpectrogram(sampleRate = 8000)
|
||||
val energy = spec.getBinEnergy(0, 10)
|
||||
assertEquals(10, energy.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun spectrogram_reset() {
|
||||
val spec = CwSpectrogram(sampleRate = 8000)
|
||||
spec.addSamples(FloatArray(256) { 1f })
|
||||
spec.reset()
|
||||
assertEquals(-1, spec.findPeakBin())
|
||||
}
|
||||
|
||||
// --- Bayesian decoder ---
|
||||
|
||||
@Test
|
||||
fun bayesian_processTone_dit() {
|
||||
val decoder = CwBayesianDecoder()
|
||||
// At 20 WPM, dot = 60 ms
|
||||
val result = decoder.processTone(60f)
|
||||
assertEquals('0', result.symbol)
|
||||
assertTrue("Dit probability should be positive", result.probability > 0.1f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bayesian_processTone_dash() {
|
||||
val decoder = CwBayesianDecoder()
|
||||
// Dash = 3 * dot = 180 ms
|
||||
val result = decoder.processTone(180f)
|
||||
assertEquals('1', result.symbol)
|
||||
assertTrue("Dash probability should be positive", result.probability > 0.1f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bayesian_processTone_unknown_returnsNull() {
|
||||
val decoder = CwBayesianDecoder()
|
||||
// Very long tone — low probability for both dit and dash
|
||||
val result = decoder.processTone(5000f)
|
||||
assertNull(result.symbol)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bayesian_processGap_interChar_returnsChar() {
|
||||
val decoder = CwBayesianDecoder()
|
||||
decoder.processTone(60f) // dit
|
||||
decoder.processTone(60f) // dit
|
||||
decoder.processTone(60f) // dit
|
||||
// 3 dots = "000" = 'S'
|
||||
val char = decoder.processGap(180f) // 3 * dot = inter-char gap
|
||||
assertEquals('S', char)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bayesian_processGap_wordGap_addsSpace() {
|
||||
val decoder = CwBayesianDecoder()
|
||||
decoder.processTone(60f) // dit = 'E'
|
||||
decoder.processGap(180f) // inter-char gap
|
||||
// Now word gap
|
||||
val space = decoder.processGap(420f) // 7 * dot
|
||||
assertEquals(' ', space)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bayesian_decodedText_accumulates() {
|
||||
val decoder = CwBayesianDecoder()
|
||||
decoder.processTone(60f) // dit = 'E'
|
||||
decoder.processGap(180f) // inter-char
|
||||
assertTrue(decoder.decodedText.isNotEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bayesian_reset() {
|
||||
val decoder = CwBayesianDecoder()
|
||||
decoder.processTone(60f)
|
||||
decoder.reset()
|
||||
assertEquals("", decoder.decodedText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bayesian_getSpeed() {
|
||||
val decoder = CwBayesianDecoder()
|
||||
// Send 3 dits at 20 WPM (60 ms each)
|
||||
decoder.processTone(60f)
|
||||
decoder.processTone(60f)
|
||||
decoder.processTone(60f)
|
||||
val speed = decoder.getSpeed()
|
||||
assertTrue("Speed should be ~20 WPM, got $speed", speed > 15f && speed < 30f)
|
||||
}
|
||||
|
||||
// --- Channel tracker ---
|
||||
|
||||
@Test
|
||||
fun channelTracker_initialState() {
|
||||
val spec = CwSpectrogram(sampleRate = 8000)
|
||||
val tracker = CwChannelTracker(spec)
|
||||
val channels = tracker.update()
|
||||
assertTrue("No channels should be active initially", channels.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun channelTracker_detectsTone() {
|
||||
val spec = CwSpectrogram(sampleRate = 8000)
|
||||
// Feed a tone
|
||||
for (i in 0..5) {
|
||||
spec.addSamples(FloatArray(256) { (sin(2.0 * PI * 700.0 * it / 8000.0)).toFloat() })
|
||||
}
|
||||
val tracker = CwChannelTracker(spec)
|
||||
val channels = tracker.update()
|
||||
assertTrue("Should detect at least 1 channel", channels.isNotEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun channelTracker_bestChannel() {
|
||||
val spec = CwSpectrogram(sampleRate = 8000)
|
||||
for (i in 0..5) {
|
||||
spec.addSamples(FloatArray(256) { (sin(2.0 * PI * 700.0 * it / 8000.0)).toFloat() })
|
||||
}
|
||||
val tracker = CwChannelTracker(spec)
|
||||
tracker.update()
|
||||
val best = tracker.getBestChannel()
|
||||
assertNotNull("Best channel should exist", best)
|
||||
if (best != null) assertTrue(best.frequency in 600f..800f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun channelTracker_reset() {
|
||||
val spec = CwSpectrogram(sampleRate = 8000)
|
||||
for (i in 0..5) {
|
||||
spec.addSamples(FloatArray(256) { (sin(2.0 * PI * 700.0 * it / 8000.0)).toFloat() })
|
||||
}
|
||||
val tracker = CwChannelTracker(spec)
|
||||
tracker.update()
|
||||
tracker.reset()
|
||||
assertNull(tracker.getBestChannel())
|
||||
}
|
||||
|
||||
// --- Full decoder ---
|
||||
|
||||
@Test
|
||||
fun decoder_initialState() {
|
||||
val decoder = CwDecoder()
|
||||
assertEquals("", decoder.decodedTextFlow.value)
|
||||
assertEquals(0f, decoder.signalStrength.value, 0.001f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decoder_processSilence_doesNotCrash() {
|
||||
val decoder = CwDecoder()
|
||||
decoder.processBuffer(FloatArray(256) { 0f })
|
||||
assertEquals("", decoder.decodedTextFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decoder_processNoise_doesNotCrash() {
|
||||
val decoder = CwDecoder()
|
||||
decoder.processBuffer(FloatArray(256) { (Math.random() * 2 - 1).toFloat() * 0.1f })
|
||||
assertNotNull(decoder.decodedTextFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decoder_processTone_doesNotCrash() {
|
||||
val decoder = CwDecoder()
|
||||
for (i in 0..20) {
|
||||
decoder.processBuffer(FloatArray(256) { (sin(2.0 * PI * 700.0 * it / 8000.0)).toFloat() })
|
||||
}
|
||||
assertNotNull(decoder.decodedTextFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decoder_reset() {
|
||||
val decoder = CwDecoder()
|
||||
decoder.processBuffer(FloatArray(256) { 1f })
|
||||
decoder.resetDecoder()
|
||||
assertEquals("", decoder.decodedTextFlow.value)
|
||||
assertEquals(0f, decoder.signalStrength.value, 0.001f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decoder_withFixedPitch() {
|
||||
val decoder = CwDecoder(sampleRate = 8000, cwToneFreq = 700f)
|
||||
assertEquals(700f, decoder.estimatedPitch.value)
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -157,16 +157,13 @@ fun RowScope.NextPassRow(pass: OrbitalPass, modifier: Modifier = Modifier, isUtc
|
||||
.padding(start = 6.dp, top = 1.dp, end = 6.dp, bottom = 0.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "${stringResource(R.string.pass_satId, pass.catNum)} - ",
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = pass.name,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 6.dp)
|
||||
.infiniteMarquee(),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
|
||||
@@ -35,6 +35,9 @@ sealed class Screen(val iconResId: Int, val titleResId: Int) : NavKey {
|
||||
@Serializable
|
||||
data object Map : Screen(R.drawable.ic_map, R.string.nav_map)
|
||||
|
||||
@Serializable
|
||||
data object Mutual : Screen(R.drawable.ic_match, R.string.nav_mutual)
|
||||
|
||||
@Serializable
|
||||
data object Settings : Screen(R.drawable.ic_settings, R.string.nav_prefs)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="96"
|
||||
android:viewportHeight="96">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="6"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M 8 76 C 8 44, 21 24, 39 24 C 57 24, 70 44, 70 76" />
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="@android:color/white"
|
||||
android:strokeWidth="6"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:pathData="M 30 76 C 30 54, 44 42, 62 42 C 80 42, 94 54, 94 76" />
|
||||
</vector>
|
||||
@@ -9,6 +9,7 @@
|
||||
<string name="nav_sat">卫星</string>
|
||||
<string name="nav_pass">过境</string>
|
||||
<string name="nav_radar">雷达</string>
|
||||
<string name="nav_mutual">匹配</string>
|
||||
<string name="nav_map">地图</string>
|
||||
<string name="nav_prefs">设置</string>
|
||||
|
||||
@@ -57,6 +58,17 @@
|
||||
<string name="radar_string_yes">是</string>
|
||||
<string name="radar_visible">日照中</string>
|
||||
|
||||
<string name="radar_doppler_calc">多普勒频率计算器</string>
|
||||
<string name="radar_doppler_tx_hint">输入上行频率 (MHz)</string>
|
||||
<string name="radar_doppler_rx_hint">输入下行频率 (MHz)</string>
|
||||
<string name="radar_doppler_offset_hint">偏移 (kHz)</string>
|
||||
<string name="radar_doppler_info">线性转发器:输入一个频率即可算出另一个</string>
|
||||
|
||||
<string name="radar_cw_decoder">CW 解码器</string>
|
||||
<string name="radar_cw_start">开始</string>
|
||||
<string name="radar_cw_stop">停止</string>
|
||||
<string name="radar_cw_reset">清空</string>
|
||||
|
||||
<!-- Map screen -->
|
||||
<string name="map_prev">上一个</string>
|
||||
<string name="map_next">下一个</string>
|
||||
@@ -80,7 +92,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>
|
||||
@@ -88,11 +100,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>
|
||||
@@ -103,6 +115,9 @@
|
||||
<string name="prefs_data_clear_success">数据清空成功</string>
|
||||
<string name="prefs_data_update_success">更新成功</string>
|
||||
|
||||
<string name="prefs_data_import_satellites_error">未导入卫星。请选择有效的 TLE/3LE (.txt) 或 OMM (.csv) 文件。</string>
|
||||
<string name="prefs_data_import_transceivers_error">未导入收发器。请选择有效的 SatNOGS (.json) 文件。</string>
|
||||
|
||||
<string name="prefs_data_sources_title">自定义数据源</string>
|
||||
<string name="prefs_data_sources_tle_switch">自定义TLE URL</string>
|
||||
<string name="prefs_data_sources_transceivers_switch">自定义收发器URL</string>
|
||||
@@ -110,6 +125,7 @@
|
||||
<string name="prefs_data_output_title">数据输出</string>
|
||||
<string name="prefs_net_output">网络</string>
|
||||
<string name="prefs_bt_output">蓝牙</string>
|
||||
<string name="prefs_cat_output">CAT</string>
|
||||
|
||||
<string name="prefs_net_title">网络数据输出</string>
|
||||
<string name="prefs_net_rotator_switch">启用方位俯仰角输出</string>
|
||||
@@ -129,6 +145,16 @@
|
||||
<string name="prefs_bt_perm_error">请检查蓝牙权限</string>
|
||||
<string name="prefs_net_perm_error">请检查网络权限</string>
|
||||
|
||||
<!-- Radio Control -->
|
||||
<string name="nav_radiocontrol">电台控制</string>
|
||||
<string name="rc_settings_title">CAT 电台控制</string>
|
||||
<string name="rc_radio_model">电台型号</string>
|
||||
<string name="rc_tx_device_hint">发射电台蓝牙地址</string>
|
||||
<string name="rc_rx_device_hint">接收电台蓝牙地址</string>
|
||||
<string name="rc_tx_name_hint">发射电台名称</string>
|
||||
<string name="rc_rx_name_hint">接收电台名称</string>
|
||||
<string name="rc_enable_switch">启用 CAT 控制</string>
|
||||
|
||||
<string name="prefs_other_title">其他设置</string>
|
||||
<string name="prefs_other_switch_utc">以 UTC 时间显示</string>
|
||||
<string name="prefs_other_switch_update">启用卫星数据自动更新</string>
|
||||
@@ -148,7 +174,8 @@
|
||||
\n• Dave Moten (predict4java)
|
||||
\n• Alexandru Csete (Gpredict)
|
||||
\n• Dr T.S. Kelso (Celestrak)
|
||||
\n• Libre Space Foundation (SatNOGS)</string>
|
||||
\n• Libre Space Foundation (SatNOGS)
|
||||
\n• xdsopl 和 Robot36 贡献者!</string>
|
||||
<string name="prefs_outro_license">该应用程序不提供任何保修.</string>
|
||||
|
||||
</resources>
|
||||
@@ -10,6 +10,7 @@
|
||||
<string name="nav_sat">Satellites</string>
|
||||
<string name="nav_pass">Passes</string>
|
||||
<string name="nav_radar">Radar</string>
|
||||
<string name="nav_mutual">Match</string>
|
||||
<string name="nav_map">Map</string>
|
||||
<string name="nav_prefs">Settings</string>
|
||||
|
||||
@@ -80,6 +81,16 @@
|
||||
<string name="radar_string_no">No</string>
|
||||
<string name="radar_string_yes">Yes</string>
|
||||
<string name="radar_visible">Visible</string>
|
||||
<string name="radar_doppler_calc">Doppler Frequency Calculator</string>
|
||||
<string name="radar_doppler_tx_hint">Enter TX freq (MHz)</string>
|
||||
<string name="radar_doppler_rx_hint">Enter RX freq (MHz)</string>
|
||||
<string name="radar_doppler_offset_hint">Offset (kHz)</string>
|
||||
<string name="radar_doppler_info">For linear transponders, type one frequency to see the other</string>
|
||||
|
||||
<string name="radar_cw_decoder">CW Decoder</string>
|
||||
<string name="radar_cw_start">Start</string>
|
||||
<string name="radar_cw_stop">Stop</string>
|
||||
<string name="radar_cw_reset">Clear</string>
|
||||
|
||||
<!-- Map screen -->
|
||||
<string name="map_prev">Prev</string>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
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"
|
||||
}
|
||||
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
# 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 { *; }
|
||||
@@ -0,0 +1,63 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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(死代码, 永不执行) —— 照搬语义, 不执行任何计算
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* 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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
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() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
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"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
|
||||
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", "general_text");
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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--;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,507 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
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}};
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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
@@ -0,0 +1,42 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
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
@@ -0,0 +1,11 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
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)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package k3;
|
||||
|
||||
/* loaded from: classes.dex */
|
||||
public final class l extends q {
|
||||
public l() {
|
||||
super("LONG", 5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package k3;
|
||||
|
||||
/* loaded from: classes.dex */
|
||||
public final class m extends q {
|
||||
public m() {
|
||||
super("FLOAT", 6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package k3;
|
||||
|
||||
/* loaded from: classes.dex */
|
||||
public final class n extends q {
|
||||
public n() {
|
||||
super("DOUBLE", 7);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
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};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
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()");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,55 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.convention.featurePlugin)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.rtbishop.look4sat.feature.mutual"
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* 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.mutual
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
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.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
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 java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Draggable dual-station elevation curve chart.
|
||||
* Controlled component: [progress] (0..1) and [onProgressChange] are owned by the parent,
|
||||
* so the same time cursor can be shared with the radar track view.
|
||||
*/
|
||||
@Composable
|
||||
fun ElevationCurveChart(
|
||||
samples: List<Pair<Long, Pair<Double, Double>>>,
|
||||
startTime: Long,
|
||||
endTime: Long,
|
||||
maxElev: Double,
|
||||
progress: Float = 0.5f,
|
||||
onProgressChange: (Float) -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (samples.isEmpty()) return
|
||||
|
||||
val colorA = MaterialTheme.colorScheme.primary
|
||||
val colorB = MaterialTheme.colorScheme.tertiary
|
||||
val gridColor = MaterialTheme.colorScheme.outlineVariant
|
||||
val textColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
val onSurfaceColor = MaterialTheme.colorScheme.onSurface
|
||||
val colorAArgb = colorA.toArgb()
|
||||
val colorBArgb = colorB.toArgb()
|
||||
val textColorArgb = textColor.toArgb()
|
||||
val onSurfaceArgb = onSurfaceColor.toArgb()
|
||||
val gridColorArgb = gridColor.toArgb()
|
||||
val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
|
||||
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures { offset ->
|
||||
onProgressChange((offset.x / size.width.toFloat()).coerceIn(0f, 1f))
|
||||
}
|
||||
}
|
||||
.pointerInput(Unit) {
|
||||
detectDragGestures { change, _ ->
|
||||
onProgressChange((change.position.x / size.width.toFloat()).coerceIn(0f, 1f))
|
||||
}
|
||||
}
|
||||
) {
|
||||
val chartWidth = size.width
|
||||
val chartHeight = size.height
|
||||
val padding = 40f
|
||||
val plotLeft = padding
|
||||
val plotRight = chartWidth - 10f
|
||||
val plotTop = 10f
|
||||
val plotBottom = chartHeight - padding
|
||||
val plotWidth = plotRight - plotLeft
|
||||
val plotHeight = plotBottom - plotTop
|
||||
|
||||
if (plotWidth <= 0f || plotHeight <= 0f) return@Canvas
|
||||
|
||||
val minTime = startTime
|
||||
val maxTime = endTime
|
||||
val timeRange = (maxTime - minTime).toFloat()
|
||||
if (timeRange <= 0f) return@Canvas
|
||||
|
||||
// Grid lines
|
||||
val gridSteps = 4
|
||||
for (i in 0..gridSteps) {
|
||||
val y = plotBottom - (plotHeight * i / gridSteps)
|
||||
drawLine(gridColor, Offset(plotLeft, y), Offset(plotRight, y), strokeWidth = 1f)
|
||||
val elev = (maxElev * i / gridSteps).roundToInt()
|
||||
drawContext.canvas.nativeCanvas.drawText(
|
||||
"$elev°", 2f, y + 4f,
|
||||
android.graphics.Paint().apply {
|
||||
color = textColorArgb
|
||||
textSize = 24f
|
||||
textAlign = android.graphics.Paint.Align.LEFT
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Time axis
|
||||
val timeSteps = 4
|
||||
for (i in 0..timeSteps) {
|
||||
val x = plotLeft + (plotWidth * i / timeSteps)
|
||||
val t = startTime + ((endTime - startTime) * i / timeSteps)
|
||||
drawContext.canvas.nativeCanvas.drawText(
|
||||
timeFormat.format(Date(t)), x - 20f, chartHeight - 2f,
|
||||
android.graphics.Paint().apply {
|
||||
color = textColorArgb
|
||||
textSize = 22f
|
||||
textAlign = android.graphics.Paint.Align.LEFT
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Elevation curves (smooth cubic Bezier)
|
||||
val pathA = Path()
|
||||
val pathB = Path()
|
||||
val ptsA = mutableListOf<Offset>()
|
||||
val ptsB = mutableListOf<Offset>()
|
||||
|
||||
for ((time, elev) in samples) {
|
||||
val x = plotLeft + ((time - minTime).toFloat() / timeRange) * plotWidth
|
||||
val yA = plotBottom - ((elev.first.toFloat() / maxElev.toFloat()) * plotHeight).coerceIn(0f, plotHeight)
|
||||
val yB = plotBottom - ((elev.second.toFloat() / maxElev.toFloat()) * plotHeight).coerceIn(0f, plotHeight)
|
||||
ptsA.add(Offset(x, yA))
|
||||
ptsB.add(Offset(x, yB))
|
||||
}
|
||||
|
||||
if (ptsA.size >= 2) {
|
||||
pathA.moveTo(ptsA[0].x, ptsA[0].y)
|
||||
pathB.moveTo(ptsB[0].x, ptsB[0].y)
|
||||
for (i in 1 until ptsA.size) {
|
||||
val p0a = ptsA.getOrNull(i - 2) ?: ptsA[i - 1]
|
||||
val p1a = ptsA[i - 1]
|
||||
val p2a = ptsA[i]
|
||||
val p3a = ptsA.getOrNull(i + 1) ?: ptsA[i]
|
||||
val cp1a = Offset(p1a.x + (p2a.x - p0a.x) / 6f, p1a.y + (p2a.y - p0a.y) / 6f)
|
||||
val cp2a = Offset(p2a.x - (p3a.x - p1a.x) / 6f, p2a.y - (p3a.y - p1a.y) / 6f)
|
||||
pathA.cubicTo(cp1a.x, cp1a.y, cp2a.x, cp2a.y, p2a.x, p2a.y)
|
||||
|
||||
val p0b = ptsB.getOrNull(i - 2) ?: ptsB[i - 1]
|
||||
val p1b = ptsB[i - 1]
|
||||
val p2b = ptsB[i]
|
||||
val p3b = ptsB.getOrNull(i + 1) ?: ptsB[i]
|
||||
val cp1b = Offset(p1b.x + (p2b.x - p0b.x) / 6f, p1b.y + (p2b.y - p0b.y) / 6f)
|
||||
val cp2b = Offset(p2b.x - (p3b.x - p1b.x) / 6f, p2b.y - (p3b.y - p1b.y) / 6f)
|
||||
pathB.cubicTo(cp1b.x, cp1b.y, cp2b.x, cp2b.y, p2b.x, p2b.y)
|
||||
}
|
||||
}
|
||||
|
||||
drawPath(pathA, colorA, style = Stroke(width = 2.5f))
|
||||
drawPath(pathB, colorB, style = Stroke(width = 2.5f))
|
||||
|
||||
// Drag indicator
|
||||
val dragX = plotLeft + progress * plotWidth
|
||||
val dragTime = startTime + (progress * (endTime - startTime)).toLong()
|
||||
|
||||
drawLine(
|
||||
onSurfaceColor,
|
||||
Offset(dragX, plotTop), Offset(dragX, plotBottom),
|
||||
strokeWidth = 2f
|
||||
)
|
||||
|
||||
val dragElev = getElevationAtTime(samples, dragTime)
|
||||
val dragElevA = (dragElev?.first?.let { (it * 10).roundToInt() / 10.0 } ?: 0.0)
|
||||
val dragElevB = (dragElev?.second?.let { (it * 10).roundToInt() / 10.0 } ?: 0.0)
|
||||
|
||||
drawContext.canvas.nativeCanvas.drawText(
|
||||
timeFormat.format(Date(dragTime)),
|
||||
dragX - 24f, plotBottom + 16f,
|
||||
android.graphics.Paint().apply {
|
||||
color = onSurfaceArgb
|
||||
textSize = 24f
|
||||
textAlign = android.graphics.Paint.Align.LEFT
|
||||
}
|
||||
)
|
||||
drawContext.canvas.nativeCanvas.drawText(
|
||||
"A:${dragElevA}°",
|
||||
dragX + 6f, plotTop + 16f,
|
||||
android.graphics.Paint().apply {
|
||||
color = colorAArgb
|
||||
textSize = 24f
|
||||
textAlign = android.graphics.Paint.Align.LEFT
|
||||
}
|
||||
)
|
||||
drawContext.canvas.nativeCanvas.drawText(
|
||||
"B:${dragElevB}°",
|
||||
dragX + 6f, plotTop + 42f,
|
||||
android.graphics.Paint().apply {
|
||||
color = colorBArgb
|
||||
textSize = 24f
|
||||
textAlign = android.graphics.Paint.Align.LEFT
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Legend
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
Text("● 站点A", color = colorA, fontSize = 12.sp)
|
||||
Text("● 站点B", color = colorB, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getElevationAtTime(
|
||||
samples: List<Pair<Long, Pair<Double, Double>>>,
|
||||
time: Long
|
||||
): Pair<Double, Double>? {
|
||||
if (samples.isEmpty()) return null
|
||||
if (samples.size == 1) return samples[0].second
|
||||
if (time <= samples[0].first) return samples[0].second
|
||||
if (time >= samples.last().first) return samples.last().second
|
||||
|
||||
var lo = 0
|
||||
var hi = samples.size - 1
|
||||
while (hi - lo > 1) {
|
||||
val mid = (lo + hi) / 2
|
||||
if (samples[mid].first <= time) lo = mid
|
||||
else hi = mid
|
||||
}
|
||||
|
||||
val t0 = samples[lo].first
|
||||
val t1 = samples[hi].first
|
||||
val frac = if (t1 > t0) (time - t0).toFloat() / (t1 - t0).toFloat() else 0f
|
||||
|
||||
val e0 = samples[lo].second
|
||||
val e1 = samples[hi].second
|
||||
return Pair(
|
||||
e0.first + (e1.first - e0.first) * frac,
|
||||
e0.second + (e1.second - e0.second) * frac
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.mutual
|
||||
|
||||
/**
|
||||
* A mutual pass where a satellite is visible from two stations simultaneously.
|
||||
*
|
||||
* @param catNum Satellite catalog number
|
||||
* @param name Satellite name
|
||||
* @param startTime Common AOS time (millis)
|
||||
* @param endTime Common LOS time (millis)
|
||||
* @param maxElevationA Peak elevation for station A
|
||||
* @param maxElevationB Peak elevation for station B
|
||||
* @param elevationSamples List of (time, (elevationA, elevationB)) pairs
|
||||
* @param trackSamples List of radar-track samples (azimuth/elevation for both stations, degrees)
|
||||
*/
|
||||
data class MutualPass(
|
||||
val catNum: Int,
|
||||
val name: String,
|
||||
val startTime: Long,
|
||||
val endTime: Long,
|
||||
val maxElevationA: Double,
|
||||
val maxElevationB: Double,
|
||||
val elevationSamples: List<Pair<Long, Pair<Double, Double>>>,
|
||||
val trackSamples: List<TrackSample> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* One radar-track sample: satellite position as seen from both stations.
|
||||
* All angles are in degrees.
|
||||
*/
|
||||
data class TrackSample(
|
||||
val time: Long,
|
||||
val azimuthA: Double,
|
||||
val elevationA: Double,
|
||||
val azimuthB: Double,
|
||||
val elevationB: Double
|
||||
)
|
||||
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* 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.mutual
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.text.TextMeasurer
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.drawText
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
private const val CIRCLES = 3 // 30°, 60°, 90° rings
|
||||
private const val STROKE_WIDTH = 2.5f
|
||||
|
||||
/**
|
||||
* Polar radar chart showing the satellite track as seen from both stations
|
||||
* on one plot. Station A is drawn solid, station B dashed.
|
||||
* Controlled: [progress] (0..1) and [onProgressChange] are owned by the parent
|
||||
* so the time cursor can be shared with the elevation curve chart.
|
||||
*/
|
||||
@Composable
|
||||
fun MutualRadarView(
|
||||
trackSamples: List<TrackSample>,
|
||||
progress: Float = 0.5f,
|
||||
labelA: String = "站点A",
|
||||
labelB: String = "站点B",
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (trackSamples.isEmpty()) return
|
||||
|
||||
val colorA = MaterialTheme.colorScheme.primary
|
||||
val colorB = MaterialTheme.colorScheme.tertiary
|
||||
val gridColor = MaterialTheme.colorScheme.outlineVariant
|
||||
val textColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
val measurer = rememberTextMeasurer()
|
||||
|
||||
// Current-time samples (both stations' position at the shared cursor)
|
||||
val t0 = trackSamples.first().time
|
||||
val t1 = trackSamples.last().time
|
||||
val selectedTime = t0 + ((t1 - t0) * progress).toLong()
|
||||
val visibleSamples = trackSamples.filter { it.time <= selectedTime }
|
||||
val currentSample = visibleSamples.lastOrNull() ?: trackSamples.first()
|
||||
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.padding(4.dp)
|
||||
) {
|
||||
val radius = size.minDimension / 2f * 0.92f
|
||||
val center = Offset(size.width / 2f, size.height / 2f)
|
||||
|
||||
// Grid: elevation rings + azimuth spokes + cardinal labels
|
||||
drawRadarGrid(center, radius, gridColor, textColor, measurer)
|
||||
|
||||
// Track paths (only the above-horizon arc, split at the 0/360° azimuth wrap)
|
||||
val pathA = Path()
|
||||
val pathB = Path()
|
||||
var firstVisibleA = true
|
||||
var firstVisibleB = true
|
||||
var lastAzimA: Double? = null
|
||||
var lastAzimB: Double? = null
|
||||
trackSamples.forEach { sample ->
|
||||
if (sample.elevationA > 0f) {
|
||||
val p = sph2Cart(center, sample.azimuthA, sample.elevationA, radius)
|
||||
val wrapA = lastAzimA != null && kotlin.math.abs(azimuthDelta(sample.azimuthA - lastAzimA!!)) > 180.0
|
||||
if (firstVisibleA || wrapA) {
|
||||
pathA.moveTo(p.x, p.y)
|
||||
firstVisibleA = false
|
||||
} else {
|
||||
pathA.lineTo(p.x, p.y)
|
||||
}
|
||||
lastAzimA = sample.azimuthA
|
||||
}
|
||||
if (sample.elevationB > 0f) {
|
||||
val p = sph2Cart(center, sample.azimuthB, sample.elevationB, radius)
|
||||
val wrapB = lastAzimB != null && kotlin.math.abs(azimuthDelta(sample.azimuthB - lastAzimB!!)) > 180.0
|
||||
if (firstVisibleB || wrapB) {
|
||||
pathB.moveTo(p.x, p.y)
|
||||
firstVisibleB = false
|
||||
} else {
|
||||
pathB.lineTo(p.x, p.y)
|
||||
}
|
||||
lastAzimB = sample.azimuthB
|
||||
}
|
||||
}
|
||||
|
||||
// Solid line for station A, dashed for station B
|
||||
drawPath(pathA, colorA, style = Stroke(STROKE_WIDTH))
|
||||
drawPath(
|
||||
pathB, colorB,
|
||||
style = Stroke(STROKE_WIDTH, pathEffect = PathEffect.dashPathEffect(floatArrayOf(14f, 10f)))
|
||||
)
|
||||
|
||||
// AOS / LOS markers on both tracks
|
||||
val first = trackSamples.first()
|
||||
val last = trackSamples.last()
|
||||
val aosA = sph2Cart(center, first.azimuthA, first.elevationA, radius)
|
||||
val aosB = sph2Cart(center, first.azimuthB, first.elevationB, radius)
|
||||
val losA = sph2Cart(center, last.azimuthA, last.elevationA, radius)
|
||||
val losB = sph2Cart(center, last.azimuthB, last.elevationB, radius)
|
||||
// AOS: hollow circle, LOS: filled circle
|
||||
if (first.elevationA > 0f) drawCircle(colorA, 7f, aosA, style = Stroke(2.5f))
|
||||
if (first.elevationB > 0f) drawCircle(colorB, 7f, aosB, style = Stroke(2.5f))
|
||||
if (last.elevationA > 0f) drawCircle(colorA, 5f, losA)
|
||||
if (last.elevationB > 0f) drawCircle(colorB, 5f, losB)
|
||||
|
||||
// Shared time-cursor positions (both stations at selectedTime)
|
||||
val cursorA = sph2Cart(center, currentSample.azimuthA, currentSample.elevationA, radius)
|
||||
val cursorB = sph2Cart(center, currentSample.azimuthB, currentSample.elevationB, radius)
|
||||
drawCircle(colorA, 14f, cursorA, style = Stroke(3f))
|
||||
drawCircle(colorA, 6f, cursorA)
|
||||
drawCircle(colorB, 14f, cursorB, style = Stroke(3f))
|
||||
drawCircle(colorB, 6f, cursorB)
|
||||
|
||||
// Center dot
|
||||
drawCircle(gridColor, 4f, center)
|
||||
}
|
||||
|
||||
// Legend
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
Text("● $labelA", color = colorA, fontSize = 12.sp)
|
||||
Text("- - $labelB", color = colorB, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DrawScope.drawRadarGrid(
|
||||
center: Offset,
|
||||
radius: Float,
|
||||
color: Color,
|
||||
textColor: Color,
|
||||
measurer: TextMeasurer
|
||||
) {
|
||||
// Elevation rings: 30°, 60°, 90°(center) — outer edge is horizon (0°)
|
||||
val step = radius / CIRCLES
|
||||
for (i in 0 until CIRCLES) {
|
||||
drawCircle(color, radius - step * i, center, style = Stroke(1.5f))
|
||||
}
|
||||
// Cardinal spokes
|
||||
drawLine(color, Offset(center.x - radius, center.y), Offset(center.x + radius, center.y), 1.5f)
|
||||
drawLine(color, Offset(center.x, center.y - radius), Offset(center.x, center.y + radius), 1.5f)
|
||||
// Diagonal spokes (lighter)
|
||||
val diag = radius * 0.7071f
|
||||
drawLine(
|
||||
color.copy(alpha = 0.5f),
|
||||
Offset(center.x - diag, center.y - diag), Offset(center.x + diag, center.y + diag), 1f
|
||||
)
|
||||
drawLine(
|
||||
color.copy(alpha = 0.5f),
|
||||
Offset(center.x - diag, center.y + diag), Offset(center.x + diag, center.y - diag), 1f
|
||||
)
|
||||
|
||||
// Elevation ring labels: 30° on the outer ring, 60° on the middle ring, 90° at center
|
||||
// (outer edge is the 0° horizon). Labels sit just above their ring.
|
||||
val style = TextStyle(color = textColor, fontSize = 11.sp)
|
||||
drawText(measurer, "30°", Offset(center.x + 6f, (center.y - (radius - step)) - 24f), style = style)
|
||||
drawText(measurer, "60°", Offset(center.x + 6f, (center.y - (radius - 2 * step)) - 24f), style = style)
|
||||
drawText(measurer, "90°", Offset(center.x + 6f, center.y - 18f), style = style)
|
||||
// Cardinal labels
|
||||
drawText(measurer, "N", Offset(center.x - 8f, center.y - radius - 20f), style = style)
|
||||
drawText(measurer, "E", Offset(center.x + radius + 4f, center.y - 10f), style = style)
|
||||
drawText(measurer, "S", Offset(center.x - 6f, center.y + radius + 2f), style = style)
|
||||
drawText(measurer, "W", Offset(center.x - radius - 24f, center.y - 10f), style = style)
|
||||
}
|
||||
|
||||
/** Normalize an azimuth delta (degrees) into the [-180, 180] range. */
|
||||
private fun azimuthDelta(deltaDeg: Double): Double {
|
||||
var d = deltaDeg % 360.0
|
||||
if (d > 180.0) d -= 360.0
|
||||
if (d < -180.0) d += 360.0
|
||||
return d
|
||||
}
|
||||
|
||||
/** Convert azimuth (deg, 0=N, clockwise) and elevation (deg) to canvas offset. */
|
||||
private fun sph2Cart(center: Offset, azimDeg: Double, elevDeg: Double, r: Float): Offset {
|
||||
val azimRad = azimDeg * PI / 180.0
|
||||
val elevRad = elevDeg * PI / 180.0
|
||||
val radius = r * (PI / 2 - elevRad) / (PI / 2)
|
||||
return Offset(
|
||||
x = center.x + (radius * cos(PI / 2 - azimRad)).toFloat(),
|
||||
y = center.y - (radius * sin(PI / 2 - azimRad)).toFloat()
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,622 @@
|
||||
/*
|
||||
* 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.mutual
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
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.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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
|
||||
import com.rtbishop.look4sat.core.domain.repository.TrackSampleData
|
||||
import com.rtbishop.look4sat.core.presentation.IconCard
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
import com.rtbishop.look4sat.core.presentation.ScreenColumn
|
||||
import com.rtbishop.look4sat.core.presentation.TopBar
|
||||
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun MutualScreen(
|
||||
viewModel: MutualViewModel,
|
||||
navigateUp: () -> Unit = {},
|
||||
navigateToRadar: (Int, Long, MutualPassData?) -> Unit = { _, _, _ -> },
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
ScreenColumn(
|
||||
topBar = { isVertical ->
|
||||
TopBar(
|
||||
isVerticalLayout = isVertical,
|
||||
startAction = {
|
||||
IconCard(action = navigateUp, resId = R.drawable.ic_back)
|
||||
},
|
||||
topInfo = {
|
||||
Text(
|
||||
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)
|
||||
)
|
||||
},
|
||||
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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { isVertical ->
|
||||
MutualContent(
|
||||
state = state,
|
||||
isVertical = isVertical,
|
||||
onQuery = viewModel::queryMutualPasses,
|
||||
onSelectPass = viewModel::onSelectPass,
|
||||
onNavigateToRadar = navigateToRadar,
|
||||
onStationALat = viewModel::onStationALat,
|
||||
onStationALon = viewModel::onStationALon,
|
||||
onStationAGrid = viewModel::onStationAGrid,
|
||||
onStationAMinElev = viewModel::onStationAMinElev,
|
||||
onStationBLat = viewModel::onStationBLat,
|
||||
onStationBLon = viewModel::onStationBLon,
|
||||
onStationBGrid = viewModel::onStationBGrid,
|
||||
onStationBMinElev = viewModel::onStationBMinElev,
|
||||
onUseCurrentPosition = viewModel::onUseCurrentPosition,
|
||||
onHoursAhead = viewModel::onHoursAhead,
|
||||
onClearError = viewModel::clearError
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutualContent(
|
||||
state: MutualUiState,
|
||||
isVertical: Boolean,
|
||||
onQuery: () -> Unit,
|
||||
onSelectPass: (Int) -> Unit,
|
||||
onNavigateToRadar: (Int, Long, MutualPassData?) -> Unit,
|
||||
onStationALat: (String) -> Unit,
|
||||
onStationALon: (String) -> Unit,
|
||||
onStationAGrid: (String) -> Unit,
|
||||
onStationAMinElev: (Double) -> Unit,
|
||||
onStationBLat: (String) -> Unit,
|
||||
onStationBLon: (String) -> Unit,
|
||||
onStationBGrid: (String) -> Unit,
|
||||
onStationBMinElev: (Double) -> Unit,
|
||||
onUseCurrentPosition: () -> Unit,
|
||||
onHoursAhead: (Int) -> Unit,
|
||||
onClearError: () -> Unit
|
||||
) {
|
||||
val timeFormat = remember { SimpleDateFormat("MM/dd HH:mm", Locale.getDefault()) }
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
// Error message
|
||||
val errorMsg = state.errorMessage
|
||||
if (errorMsg != null) {
|
||||
item {
|
||||
ElevatedCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.elevatedCardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = errorMsg,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
modifier = Modifier.padding(12.dp),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Input form
|
||||
item {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
if (isVertical) {
|
||||
StationInputCard(
|
||||
title = "你的位置",
|
||||
titleColor = MaterialTheme.colorScheme.primary,
|
||||
lat = state.stationALat,
|
||||
lon = state.stationALon,
|
||||
grid = state.stationAGrid,
|
||||
minElev = state.stationAMinElev,
|
||||
latPlaceholder = "39.9042",
|
||||
lonPlaceholder = "116.4074",
|
||||
gridPlaceholder = "ON79uj",
|
||||
onLatChange = onStationALat,
|
||||
onLonChange = onStationALon,
|
||||
onGridChange = onStationAGrid,
|
||||
onMinElevChange = onStationAMinElev,
|
||||
currentPositionAction = onUseCurrentPosition
|
||||
)
|
||||
StationInputCard(
|
||||
title = "友台位置",
|
||||
titleColor = MaterialTheme.colorScheme.tertiary,
|
||||
lat = state.stationBLat,
|
||||
lon = state.stationBLon,
|
||||
grid = state.stationBGrid,
|
||||
minElev = state.stationBMinElev,
|
||||
latPlaceholder = "34.0522",
|
||||
lonPlaceholder = "-118.2437",
|
||||
gridPlaceholder = "PM01tv",
|
||||
onLatChange = onStationBLat,
|
||||
onLonChange = onStationBLon,
|
||||
onGridChange = onStationBGrid,
|
||||
onMinElevChange = onStationBMinElev
|
||||
)
|
||||
} else {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
StationInputCard(
|
||||
title = "你的位置",
|
||||
titleColor = MaterialTheme.colorScheme.primary,
|
||||
lat = state.stationALat,
|
||||
lon = state.stationALon,
|
||||
grid = state.stationAGrid,
|
||||
minElev = state.stationAMinElev,
|
||||
latPlaceholder = "39.9042",
|
||||
lonPlaceholder = "116.4074",
|
||||
gridPlaceholder = "ON79uj",
|
||||
onLatChange = onStationALat,
|
||||
onLonChange = onStationALon,
|
||||
onGridChange = onStationAGrid,
|
||||
onMinElevChange = onStationAMinElev,
|
||||
currentPositionAction = onUseCurrentPosition,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
StationInputCard(
|
||||
title = "友台位置",
|
||||
titleColor = MaterialTheme.colorScheme.tertiary,
|
||||
lat = state.stationBLat,
|
||||
lon = state.stationBLon,
|
||||
grid = state.stationBGrid,
|
||||
minElev = state.stationBMinElev,
|
||||
latPlaceholder = "34.0522",
|
||||
lonPlaceholder = "-118.2437",
|
||||
gridPlaceholder = "PM01tv",
|
||||
onLatChange = onStationBLat,
|
||||
onLonChange = onStationBLon,
|
||||
onGridChange = onStationBGrid,
|
||||
onMinElevChange = onStationBMinElev,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MatchSearchCard(
|
||||
hoursAhead = state.hoursAhead,
|
||||
isCalculating = state.isCalculating,
|
||||
onHoursAhead = onHoursAhead,
|
||||
onQuery = onQuery
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Results
|
||||
itemsIndexed(state.mutualPasses) { index, pass ->
|
||||
val mutualData = MutualPassData(
|
||||
samples = pass.elevationSamples,
|
||||
trackSamples = pass.trackSamples.map {
|
||||
TrackSampleData(
|
||||
time = it.time,
|
||||
azimuthA = it.azimuthA,
|
||||
elevationA = it.elevationA,
|
||||
azimuthB = it.azimuthB,
|
||||
elevationB = it.elevationB
|
||||
)
|
||||
},
|
||||
startTime = pass.startTime,
|
||||
endTime = pass.endTime,
|
||||
maxElev = maxOf(pass.maxElevationA, pass.maxElevationB, 10.0),
|
||||
labelA = "你",
|
||||
labelB = "友台"
|
||||
)
|
||||
MutualPassCard(
|
||||
pass = pass,
|
||||
isExpanded = state.selectedPassIndex == index,
|
||||
timeFormat = timeFormat,
|
||||
minElevA = state.stationAMinElev,
|
||||
minElevB = state.stationBMinElev,
|
||||
onClick = { onSelectPass(if (state.selectedPassIndex == index) -1 else index) },
|
||||
onNavigateToRadar = { onNavigateToRadar(pass.catNum, pass.startTime, mutualData) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mutualStatusText(state: MutualUiState): String = when {
|
||||
state.isCalculating -> "正在计算双方共同可见窗口…"
|
||||
state.errorMessage != null -> "请修正提示后重新查询"
|
||||
state.mutualPasses.isNotEmpty() -> "找到 ${state.mutualPasses.size} 个匹配 · 点击卡片展开曲线"
|
||||
state.hasSearched -> "未找到共同可见窗口,调整时间或仰角后重试"
|
||||
else -> "输入双方位置后点击查询"
|
||||
}
|
||||
|
||||
private fun mutualStatusChipText(state: MutualUiState): String = when {
|
||||
state.isCalculating -> "计算中"
|
||||
state.errorMessage != null -> "有错误"
|
||||
state.mutualPasses.isNotEmpty() -> "${state.mutualPasses.size}个匹配"
|
||||
state.hasSearched -> "无匹配"
|
||||
else -> "待查询"
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StationInputCard(
|
||||
title: String,
|
||||
titleColor: androidx.compose.ui.graphics.Color,
|
||||
lat: String,
|
||||
lon: String,
|
||||
grid: String,
|
||||
minElev: Double,
|
||||
latPlaceholder: String,
|
||||
lonPlaceholder: String,
|
||||
gridPlaceholder: String,
|
||||
onLatChange: (String) -> Unit,
|
||||
onLonChange: (String) -> Unit,
|
||||
onGridChange: (String) -> Unit,
|
||||
onMinElevChange: (Double) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
currentPositionAction: (() -> Unit)? = null
|
||||
) {
|
||||
ElevatedCard(modifier = modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = titleColor
|
||||
)
|
||||
if (currentPositionAction != null) {
|
||||
OutlinedButton(onClick = currentPositionAction) {
|
||||
Text("填入当前位置", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = lat,
|
||||
onValueChange = onLatChange,
|
||||
label = { Text("纬度") },
|
||||
placeholder = { Text(latPlaceholder) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = lon,
|
||||
onValueChange = onLonChange,
|
||||
label = { Text("经度") },
|
||||
placeholder = { Text(lonPlaceholder) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = grid,
|
||||
onValueChange = onGridChange,
|
||||
label = { Text("网格") },
|
||||
placeholder = { Text(gridPlaceholder) },
|
||||
supportingText = { Text("支持 4/6/8 位 Maidenhead 网格;也可直接输入经纬度") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "最小仰角:${minElev.toInt()}°",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Slider(
|
||||
value = minElev.toFloat(),
|
||||
onValueChange = { onMinElevChange(it.toDouble()) },
|
||||
valueRange = 0f..90f,
|
||||
steps = 17
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MatchSearchCard(
|
||||
hoursAhead: Int,
|
||||
isCalculating: Boolean,
|
||||
onHoursAhead: (Int) -> Unit,
|
||||
onQuery: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
ElevatedCard(modifier = modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "时间范围",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
listOf(6, 12, 24, 48, 72).forEach { hours ->
|
||||
FilterChip(
|
||||
selected = hoursAhead == hours,
|
||||
onClick = { onHoursAhead(hours) },
|
||||
label = { Text("${hours}h") }
|
||||
)
|
||||
}
|
||||
}
|
||||
Button(
|
||||
onClick = onQuery,
|
||||
enabled = !isCalculating,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (isCalculating) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.height(18.dp)
|
||||
.width(18.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
}
|
||||
Text(if (isCalculating) "计算中..." else "查询匹配")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutualPassCard(
|
||||
pass: MutualPass,
|
||||
isExpanded: Boolean,
|
||||
timeFormat: SimpleDateFormat,
|
||||
minElevA: Double,
|
||||
minElevB: Double,
|
||||
onClick: () -> Unit,
|
||||
onNavigateToRadar: () -> Unit
|
||||
) {
|
||||
// Shared time cursor: both the elevation curve and the radar track view
|
||||
// are controlled by this single progress value for bidirectional drag sync.
|
||||
var dragProgress by remember(pass) { mutableFloatStateOf(0.5f) }
|
||||
|
||||
// Filter to only show the portion where both stations are above their minElev
|
||||
val visibleSamples = remember(pass, minElevA, minElevB) {
|
||||
pass.elevationSamples.filter { (_, elev) ->
|
||||
elev.first >= minElevA && elev.second >= minElevB
|
||||
}
|
||||
}
|
||||
val visibleTracks = remember(pass, minElevA, minElevB) {
|
||||
pass.trackSamples.filter { it.elevationA >= minElevA && it.elevationB >= minElevB }
|
||||
}
|
||||
val visibleStart = visibleSamples.firstOrNull()?.first ?: pass.startTime
|
||||
val visibleEnd = visibleSamples.lastOrNull()?.first ?: pass.endTime
|
||||
val adjustedMaxElev = maxOf(
|
||||
visibleSamples.maxOfOrNull { (_, elev) -> elev.first } ?: 10.0,
|
||||
visibleSamples.maxOfOrNull { (_, elev) -> elev.second } ?: 10.0,
|
||||
10.0
|
||||
)
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = pass.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Text(
|
||||
text = "${timeFormat.format(Date(pass.startTime))} - ${timeFormat.format(Date(pass.endTime))}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = "你: ${pass.maxElevationA}°",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = "友台: ${pass.maxElevationB}°",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
enter = expandVertically(),
|
||||
exit = shrinkVertically()
|
||||
) {
|
||||
Column {
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
// Dual-station radar track (polar plot)
|
||||
if (visibleTracks.isNotEmpty()) {
|
||||
Text(
|
||||
text = "双方轨迹",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
MutualRadarView(
|
||||
trackSamples = visibleTracks,
|
||||
progress = dragProgress,
|
||||
labelA = "你",
|
||||
labelB = "友台"
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
if (visibleSamples.isNotEmpty()) {
|
||||
ElevationCurveChart(
|
||||
samples = visibleSamples,
|
||||
startTime = visibleStart,
|
||||
endTime = visibleEnd,
|
||||
maxElev = adjustedMaxElev,
|
||||
progress = dragProgress,
|
||||
onProgressChange = { dragProgress = it }
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
OutlinedButton(
|
||||
onClick = onNavigateToRadar,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_radar),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(16.dp).width(16.dp)
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text("查看雷达")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,569 @@
|
||||
/*
|
||||
* 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.mutual
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalObject
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPass
|
||||
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
data class MutualUiState(
|
||||
val stationALat: String = "",
|
||||
val stationALon: String = "",
|
||||
val stationAGrid: String = "",
|
||||
val stationAMinElev: Double = 10.0,
|
||||
val stationBLat: String = "",
|
||||
val stationBLon: String = "",
|
||||
val stationBGrid: String = "",
|
||||
val stationBMinElev: Double = 10.0,
|
||||
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
|
||||
)
|
||||
|
||||
class MutualViewModel(
|
||||
private val satelliteRepo: ISatelliteRepo,
|
||||
private val settingsRepo: ISettingsRepo
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(MutualUiState())
|
||||
val uiState: StateFlow<MutualUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
// Pre-fill station A with the user's current station position (as grid),
|
||||
// and default min elevation to the same value used by the main radar passes
|
||||
val pos = settingsRepo.stationPosition.value
|
||||
val grid = latLonToGrid(pos.latitude, pos.longitude)
|
||||
_uiState.update { it.copy(
|
||||
stationAGrid = grid,
|
||||
stationALat = "%.4f".format(pos.latitude),
|
||||
stationALon = "%.4f".format(pos.longitude),
|
||||
stationAMinElev = 0.0,
|
||||
stationBMinElev = 0.0
|
||||
) }
|
||||
}
|
||||
|
||||
fun onStationALat(value: String) {
|
||||
_uiState.update { it.copy(stationALat = value) }
|
||||
val lat = value.toDoubleOrNull()
|
||||
val lon = _uiState.value.stationALon.toDoubleOrNull()
|
||||
if (lat != null && lon != null) {
|
||||
_uiState.update { it.copy(stationAGrid = latLonToGrid(lat, lon)) }
|
||||
}
|
||||
}
|
||||
|
||||
fun onStationALon(value: String) {
|
||||
_uiState.update { it.copy(stationALon = value) }
|
||||
val lat = _uiState.value.stationALat.toDoubleOrNull()
|
||||
val lon = value.toDoubleOrNull()
|
||||
if (lat != null && lon != null) {
|
||||
_uiState.update { it.copy(stationAGrid = latLonToGrid(lat, lon)) }
|
||||
}
|
||||
}
|
||||
|
||||
fun onStationAGrid(value: String) {
|
||||
val old = _uiState.value.stationAGrid
|
||||
_uiState.update { it.copy(stationAGrid = value) }
|
||||
if (value.trim().uppercase() == old.trim().uppercase()) return
|
||||
val pos = gridToLatLon(value.trim().uppercase())
|
||||
if (pos != null) {
|
||||
_uiState.update { it.copy(
|
||||
stationALat = "%.4f".format(pos.latitude),
|
||||
stationALon = "%.4f".format(pos.longitude)
|
||||
)}
|
||||
}
|
||||
}
|
||||
|
||||
fun onStationBLat(value: String) {
|
||||
_uiState.update { it.copy(stationBLat = value) }
|
||||
val lat = value.toDoubleOrNull()
|
||||
val lon = _uiState.value.stationBLon.toDoubleOrNull()
|
||||
if (lat != null && lon != null) {
|
||||
_uiState.update { it.copy(stationBGrid = latLonToGrid(lat, lon)) }
|
||||
}
|
||||
}
|
||||
|
||||
fun onStationBLon(value: String) {
|
||||
_uiState.update { it.copy(stationBLon = value) }
|
||||
val lat = _uiState.value.stationBLat.toDoubleOrNull()
|
||||
val lon = value.toDoubleOrNull()
|
||||
if (lat != null && lon != null) {
|
||||
_uiState.update { it.copy(stationBGrid = latLonToGrid(lat, lon)) }
|
||||
}
|
||||
}
|
||||
|
||||
fun onStationBGrid(value: String) {
|
||||
val old = _uiState.value.stationBGrid
|
||||
_uiState.update { it.copy(stationBGrid = value) }
|
||||
if (value.trim().uppercase() == old.trim().uppercase()) return
|
||||
val pos = gridToLatLon(value.trim().uppercase())
|
||||
if (pos != null) {
|
||||
_uiState.update { it.copy(
|
||||
stationBLat = "%.4f".format(pos.latitude),
|
||||
stationBLon = "%.4f".format(pos.longitude)
|
||||
)}
|
||||
}
|
||||
}
|
||||
fun onStationAMinElev(value: Double) = _uiState.update { it.copy(stationAMinElev = value) }
|
||||
fun onStationBMinElev(value: Double) = _uiState.update { it.copy(stationBMinElev = value) }
|
||||
fun onUseCurrentPosition() {
|
||||
val pos = settingsRepo.stationPosition.value
|
||||
_uiState.update { it.copy(
|
||||
stationALat = "%.4f".format(pos.latitude),
|
||||
stationALon = "%.4f".format(pos.longitude),
|
||||
stationAGrid = latLonToGrid(pos.latitude, pos.longitude)
|
||||
) }
|
||||
}
|
||||
fun onHoursAhead(value: Int) = _uiState.update { it.copy(hoursAhead = value) }
|
||||
fun onSelectPass(index: Int) = _uiState.update { it.copy(selectedPassIndex = index) }
|
||||
|
||||
fun queryMutualPasses() {
|
||||
val state = _uiState.value
|
||||
|
||||
// Resolve positions from lat/lon or grid
|
||||
var posA = resolvePosition(state.stationALat, state.stationALon, state.stationAGrid)
|
||||
var posB = resolvePosition(state.stationBLat, state.stationBLon, state.stationBGrid)
|
||||
|
||||
if (posA == null || posB == null) {
|
||||
_uiState.update { it.copy(errorMessage = "请输入有效的位置坐标或网格(4/6/8位)") }
|
||||
return
|
||||
}
|
||||
|
||||
val satellites = satelliteRepo.satellites.value
|
||||
if (satellites.isEmpty()) {
|
||||
_uiState.update { it.copy(errorMessage = "没有卫星数据,请先在卫星列表中选择卫星") }
|
||||
return
|
||||
}
|
||||
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isCalculating = true,
|
||||
hasSearched = true,
|
||||
errorMessage = null,
|
||||
mutualPasses = emptyList(),
|
||||
selectedPassIndex = -1
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val time = System.currentTimeMillis()
|
||||
val minElevA = state.stationAMinElev
|
||||
val minElevB = state.stationBMinElev
|
||||
val hours = state.hoursAhead
|
||||
|
||||
val results = withContext(Dispatchers.Default) {
|
||||
findMutualPasses(satellites, posA, posB, minElevA, minElevB, time, hours)
|
||||
}
|
||||
|
||||
val errorMsg = if (results.isEmpty()) {
|
||||
// Debug: check if the main pass list is the culprit
|
||||
val passCount = satelliteRepo.passes.value.size
|
||||
val satCount = satellites.size
|
||||
when {
|
||||
passCount == 0 -> "过境列表为空,请先在卫星列表中选择卫星"
|
||||
satCount == 0 -> "没有已选中的卫星"
|
||||
else -> "未找到共同过境(${passCount}个过境,${satCount}颗卫星)"
|
||||
}
|
||||
} else null
|
||||
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
mutualPasses = results.sortedBy { mp -> mp.startTime },
|
||||
isCalculating = false,
|
||||
errorMessage = errorMsg
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve a position from lat/lon text or grid square text. */
|
||||
private fun resolvePosition(latText: String, lonText: String, gridText: String): GeoPos? {
|
||||
val trimmed = gridText.trim().uppercase()
|
||||
if (trimmed.length in listOf(4, 6, 8) && trimmed.all { it.isLetterOrDigit() }) {
|
||||
return gridToLatLon(trimmed)
|
||||
}
|
||||
val lat = latText.toDoubleOrNull()
|
||||
val lon = lonText.toDoubleOrNull()
|
||||
return if (lat != null && lon != null) GeoPos(lat, lon) else null
|
||||
}
|
||||
|
||||
/** Convert lat/lon to Maidenhead grid (6-char). */
|
||||
private fun latLonToGrid(lat: Double, lon: Double): String {
|
||||
var adjLon = (lon + 180.0) % 360.0
|
||||
var adjLat = (lat + 90.0) % 180.0
|
||||
|
||||
val fieldLon = (adjLon / 20.0).toInt()
|
||||
val fieldLat = (adjLat / 10.0).toInt()
|
||||
adjLon -= fieldLon * 20.0
|
||||
adjLat -= fieldLat * 10.0
|
||||
|
||||
val squareLon = (adjLon / 2.0).toInt()
|
||||
val squareLat = (adjLat / 1.0).toInt()
|
||||
adjLon -= squareLon * 2.0
|
||||
adjLat -= squareLat * 1.0
|
||||
|
||||
val subLon = (adjLon * 60.0 / 5.0).toInt()
|
||||
val subLat = (adjLat * 60.0 / 2.5).toInt()
|
||||
|
||||
return buildString {
|
||||
append('A' + fieldLon)
|
||||
append('A' + fieldLat)
|
||||
append('0' + squareLon)
|
||||
append('0' + squareLat)
|
||||
append('A' + subLon)
|
||||
append('A' + subLat)
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert Maidenhead grid (4, 6, or 8 chars) to lat/lon center of the square. */
|
||||
private fun gridToLatLon(grid: String): GeoPos? {
|
||||
val g = grid.uppercase()
|
||||
if (g.length < 4) return null
|
||||
val lonField = (g[0] - 'A').toDouble() * 20.0
|
||||
val latField = (g[1] - 'A').toDouble() * 10.0
|
||||
if (lonField < 0 || lonField > 340 || latField < 0 || latField > 170) return null
|
||||
|
||||
val lonSquare = (g[2] - '0').toDouble() * 2.0
|
||||
val latSquare = (g[3] - '0').toDouble() * 1.0
|
||||
if (lonSquare < 0 || lonSquare > 18 || latSquare < 0 || latSquare > 9) return null
|
||||
|
||||
var lon = lonField + lonSquare
|
||||
var lat = latField + latSquare
|
||||
|
||||
if (g.length >= 6) {
|
||||
val lonSub = (g[4] - 'A').toDouble() * 5.0 / 60.0
|
||||
val latSub = (g[5] - 'A').toDouble() * 2.5 / 60.0
|
||||
if (lonSub < 0 || lonSub > 115.0 / 60.0 || latSub < 0 || latSub > 57.5 / 60.0) return null
|
||||
lon += lonSub
|
||||
lat += latSub
|
||||
|
||||
if (g.length >= 8) {
|
||||
val lonExt = (g[6] - '0').toDouble() * 30.0 / 3600.0
|
||||
val latExt = (g[7] - '0').toDouble() * 15.0 / 3600.0
|
||||
if (lonExt < 0 || lonExt > 270.0 / 3600.0 || latExt < 0 || latExt > 135.0 / 3600.0) return null
|
||||
lon += lonExt + 15.0 / 3600.0
|
||||
lat += latExt + 7.5 / 3600.0
|
||||
} else {
|
||||
lon += 2.5 / 60.0
|
||||
lat += 1.25 / 60.0
|
||||
}
|
||||
} else {
|
||||
lon += 1.0
|
||||
lat += 0.5
|
||||
}
|
||||
|
||||
lon = lon - 180.0
|
||||
lat = lat - 90.0
|
||||
return GeoPos(lat, lon)
|
||||
}
|
||||
|
||||
private fun findMutualPasses(
|
||||
satellites: List<OrbitalObject>,
|
||||
posA: GeoPos, posB: GeoPos,
|
||||
minElevADeg: Double, minElevBDeg: Double,
|
||||
time: Long, hours: Int
|
||||
): List<MutualPass> {
|
||||
val endTime = time + hours * 60L * 60L * 1000L
|
||||
val sampleInterval = 5_000L
|
||||
|
||||
// Use the main page's pass list for AOS/LOS times, then sample the curves
|
||||
// using the actual positions. The passes list is already computed by getLeoPass
|
||||
// and its AOS/LOS times match the Passes page exactly.
|
||||
val existingPasses = satelliteRepo.passes.value
|
||||
val results = findMutualPassesFromList(existingPasses, satellites, posA, posB,
|
||||
minElevADeg, minElevBDeg, time, endTime, sampleInterval)
|
||||
if (results.isNotEmpty()) return results
|
||||
|
||||
// Fallback: try the independent search.
|
||||
return findMutualPassesFallback(satellites, posA, posB,
|
||||
minElevADeg, minElevBDeg, time, endTime, sampleInterval)
|
||||
}
|
||||
|
||||
/** Reuse the main page's pass list. Uses AOS/LOS times directly (no refineEdge) */
|
||||
private fun findMutualPassesFromList(
|
||||
existingPasses: List<OrbitalPass>,
|
||||
satellites: List<OrbitalObject>,
|
||||
posA: GeoPos, posB: GeoPos,
|
||||
minElevADeg: Double, minElevBDeg: Double,
|
||||
time: Long, endTime: Long, sampleInterval: Long
|
||||
): List<MutualPass> {
|
||||
val results = mutableListOf<MutualPass>()
|
||||
for (pass in existingPasses) {
|
||||
if (pass.losTime <= time || pass.aosTime >= endTime) continue
|
||||
if (pass.isDeepSpace) continue
|
||||
if (pass.orbitalObject.data.meanmo < 1e-8) continue
|
||||
|
||||
val sat = pass.orbitalObject
|
||||
// Refine the AOS/LOS to the actual posA/posB (0° horizon).
|
||||
// This ensures the elevation curve starts from ~0°.
|
||||
val refinedAos = refineEdge(sat, posA, posB, pass.aosTime, 1_000L, goingUp = true)
|
||||
val refinedLos = refineEdge(sat, posA, posB, pass.losTime, 1_000L, goingUp = false)
|
||||
if (refinedLos <= refinedAos) continue
|
||||
|
||||
val samples = mutableListOf<Pair<Long, Pair<Double, Double>>>()
|
||||
val tracks = mutableListOf<TrackSample>()
|
||||
var maxElevA = 0.0
|
||||
var maxElevB = 0.0
|
||||
|
||||
var tSample = refinedAos
|
||||
while (tSample <= refinedLos) {
|
||||
// Use getElevation (same function used by getLeoPass) for elevation,
|
||||
// and getFullPosition only for azimuth.
|
||||
val elevA = sat.getElevation(posA, tSample) * 180.0 / PI
|
||||
val elevB = sat.getElevation(posB, tSample) * 180.0 / PI
|
||||
val fullA = sat.getFullPosition(posA, tSample)
|
||||
val fullB = sat.getFullPosition(posB, tSample)
|
||||
if (elevA > maxElevA) maxElevA = elevA
|
||||
if (elevB > maxElevB) maxElevB = elevB
|
||||
samples.add(tSample to (elevA to elevB))
|
||||
tracks.add(
|
||||
TrackSample(
|
||||
time = tSample,
|
||||
azimuthA = fullA.azimuth * 180.0 / PI,
|
||||
elevationA = elevA,
|
||||
azimuthB = fullB.azimuth * 180.0 / PI,
|
||||
elevationB = elevB
|
||||
)
|
||||
)
|
||||
tSample += sampleInterval
|
||||
}
|
||||
|
||||
// Search uses the 0° horizon as the pass boundary, but the user sliders are
|
||||
// a final visibility filter. Both stations must exceed their own threshold;
|
||||
// otherwise the result would expand to an empty filtered chart.
|
||||
if (maxElevA > minElevADeg && maxElevB > minElevBDeg) {
|
||||
results.add(
|
||||
MutualPass(
|
||||
catNum = pass.catNum,
|
||||
name = pass.orbitalObject.data.name,
|
||||
startTime = refinedAos,
|
||||
endTime = refinedLos,
|
||||
maxElevationA = (maxElevA * 10).roundToInt() / 10.0,
|
||||
maxElevationB = (maxElevB * 10).roundToInt() / 10.0,
|
||||
elevationSamples = samples,
|
||||
trackSamples = tracks
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
/** Fallback: search passes independently (same logic as original findMutualPasses). */
|
||||
private fun findMutualPassesFallback(
|
||||
satellites: List<OrbitalObject>,
|
||||
posA: GeoPos, posB: GeoPos,
|
||||
minElevADeg: Double, minElevBDeg: Double,
|
||||
time: Long, endTime: Long, sampleInterval: Long
|
||||
): List<MutualPass> {
|
||||
val results = mutableListOf<MutualPass>()
|
||||
for (sat in satellites) {
|
||||
if (sat.data.meanmo < 1e-8) continue
|
||||
var searchStart = time
|
||||
while (true) {
|
||||
val t = findNextMutualPass(sat, posA, posB, searchStart, endTime)
|
||||
if (t == null) break
|
||||
val (aos, los) = t
|
||||
|
||||
val refinedAos = refineEdge(sat, posA, posB, aos, 1_000L, true)
|
||||
val refinedLos = refineEdge(sat, posA, posB, los, 1_000L, false)
|
||||
if (refinedLos <= refinedAos) continue
|
||||
|
||||
val mutualPass = sampleMutualPass(sat, posA, posB, refinedAos, refinedLos,
|
||||
minElevADeg, minElevBDeg, sampleInterval)
|
||||
if (mutualPass != null) results.add(mutualPass)
|
||||
searchStart = refinedLos + 120_000L
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
/** Sample elevation/azimuth data for one mutual pass window. */
|
||||
private fun sampleMutualPass(
|
||||
sat: OrbitalObject, posA: GeoPos, posB: GeoPos,
|
||||
refinedAos: Long, refinedLos: Long,
|
||||
minElevADeg: Double, minElevBDeg: Double,
|
||||
sampleInterval: Long
|
||||
): MutualPass? {
|
||||
val samples = mutableListOf<Pair<Long, Pair<Double, Double>>>()
|
||||
val tracks = mutableListOf<TrackSample>()
|
||||
var maxElevA = 0.0
|
||||
var maxElevB = 0.0
|
||||
|
||||
var tSample = refinedAos
|
||||
while (tSample <= refinedLos) {
|
||||
// Use getElevation for elevation, matching the main pass predictor; use
|
||||
// getFullPosition only for azimuth needed by the radar plot.
|
||||
val elevA = elevationDeg(sat, posA, tSample)
|
||||
val elevB = elevationDeg(sat, posB, tSample)
|
||||
val fullA = sat.getFullPosition(posA, tSample)
|
||||
val fullB = sat.getFullPosition(posB, tSample)
|
||||
if (elevA > maxElevA) maxElevA = elevA
|
||||
if (elevB > maxElevB) maxElevB = elevB
|
||||
samples.add(tSample to (elevA to elevB))
|
||||
tracks.add(
|
||||
TrackSample(
|
||||
time = tSample,
|
||||
azimuthA = fullA.azimuth * 180.0 / PI,
|
||||
elevationA = elevA,
|
||||
azimuthB = fullB.azimuth * 180.0 / PI,
|
||||
elevationB = elevB
|
||||
)
|
||||
)
|
||||
tSample += sampleInterval
|
||||
}
|
||||
|
||||
if (maxElevA > minElevADeg && maxElevB > minElevBDeg) {
|
||||
return MutualPass(
|
||||
catNum = sat.data.catnum,
|
||||
name = sat.data.name,
|
||||
startTime = refinedAos,
|
||||
endTime = refinedLos,
|
||||
maxElevationA = (maxElevA * 10).roundToInt() / 10.0,
|
||||
maxElevationB = (maxElevB * 10).roundToInt() / 10.0,
|
||||
elevationSamples = samples,
|
||||
trackSamples = tracks
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** Refine the AOS (goingUp=true) or LOS (goingUp=false) to ~1s precision at the 0° horizon. */
|
||||
private fun refineEdge(
|
||||
sat: OrbitalObject, posA: GeoPos, posB: GeoPos,
|
||||
approxTime: Long, step: Long, goingUp: Boolean
|
||||
): Long {
|
||||
if (goingUp) {
|
||||
// AOS: walk backward from approxTime to find the last sample where either is below
|
||||
// the horizon, then AOS is the next step after that.
|
||||
var t = approxTime
|
||||
while (t > approxTime - 70_000L) {
|
||||
val eA = elevationDeg(sat, posA, t)
|
||||
val eB = elevationDeg(sat, posB, t)
|
||||
if (eA > 0.0 && eB > 0.0) {
|
||||
t -= step
|
||||
} else {
|
||||
return t + step
|
||||
}
|
||||
}
|
||||
return approxTime - 70_000L + step
|
||||
} else {
|
||||
// LOS: walk forward from approxTime to find the first sample where either drops
|
||||
// below the horizon, then LOS is the step before that.
|
||||
var t = approxTime
|
||||
while (t < approxTime + 70_000L) {
|
||||
val eA = elevationDeg(sat, posA, t)
|
||||
val eB = elevationDeg(sat, posB, t)
|
||||
if (eA > 0.0 && eB > 0.0) {
|
||||
t += step
|
||||
} else {
|
||||
return t - step
|
||||
}
|
||||
}
|
||||
return approxTime + 70_000L - step
|
||||
}
|
||||
}
|
||||
|
||||
private fun elevationDeg(sat: OrbitalObject, pos: GeoPos, time: Long): Double {
|
||||
return sat.getElevation(pos, time) * 180.0 / PI
|
||||
}
|
||||
|
||||
private fun findNextMutualPass(
|
||||
sat: OrbitalObject,
|
||||
posA: GeoPos, posB: GeoPos,
|
||||
startTime: Long, endTime: Long
|
||||
): Pair<Long, Long>? {
|
||||
var t = startTime
|
||||
val step = 60_000L
|
||||
|
||||
// Skip an in-progress mutual window at the search start (same as getLeoPass):
|
||||
// walk forward until either station drops below the horizon, then keep searching.
|
||||
if (elevationDeg(sat, posA, t) > 0.0 && elevationDeg(sat, posB, t) > 0.0) {
|
||||
while (t < endTime) {
|
||||
if (elevationDeg(sat, posA, t) <= 0.0 || elevationDeg(sat, posB, t) <= 0.0) break
|
||||
t += step
|
||||
}
|
||||
}
|
||||
|
||||
while (t < endTime) {
|
||||
val elevA = elevationDeg(sat, posA, t)
|
||||
val elevB = elevationDeg(sat, posB, t)
|
||||
|
||||
if (elevA > 0.0 && elevB > 0.0) {
|
||||
var aos = t
|
||||
var rew = t
|
||||
while (rew > startTime - 600_000L) {
|
||||
val eA = elevationDeg(sat, posA, rew)
|
||||
val eB = elevationDeg(sat, posB, rew)
|
||||
if (eA <= 0.0 || eB <= 0.0) {
|
||||
aos = rew + step
|
||||
break
|
||||
}
|
||||
rew -= step
|
||||
}
|
||||
|
||||
var los = t
|
||||
var fwd = t
|
||||
while (fwd < endTime + 600_000L) {
|
||||
val eA = elevationDeg(sat, posA, fwd)
|
||||
val eB = elevationDeg(sat, posB, fwd)
|
||||
if (eA <= 0.0 || eB <= 0.0) {
|
||||
los = fwd
|
||||
break
|
||||
}
|
||||
fwd += step
|
||||
}
|
||||
|
||||
if (los > aos) return Pair(aos, los)
|
||||
}
|
||||
t += step
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun clearError() = _uiState.update { it.copy(errorMessage = null) }
|
||||
|
||||
companion object {
|
||||
fun factory(container: IMainContainer) = object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||
MutualViewModel(
|
||||
satelliteRepo = container.satelliteRepo,
|
||||
settingsRepo = container.settingsRepo
|
||||
) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,13 +316,12 @@ private fun PassItem(
|
||||
isVerticalLayout: Boolean = true,
|
||||
isUtc: Boolean = false
|
||||
) {
|
||||
val passSatId = stringResource(id = R.string.pass_satId, pass.catNum)
|
||||
val horizontalPadding = if (isVerticalLayout) 6.dp else 10.dp
|
||||
val timeZone = remember(isUtc) {
|
||||
if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault()
|
||||
}
|
||||
val sdfTime = remember(isUtc) {
|
||||
SimpleDateFormat("HH:mm:ss", Locale.ENGLISH).also { it.timeZone = timeZone }
|
||||
SimpleDateFormat("HH:mm:ss", Locale.getDefault()).also { it.timeZone = timeZone }
|
||||
}
|
||||
val aosTimeStr = remember(pass.aosTime, isUtc) { sdfTime.format(Date(pass.aosTime)) }
|
||||
val losTimeStr = remember(pass.losTime, isUtc) { sdfTime.format(Date(pass.losTime)) }
|
||||
@@ -342,10 +341,6 @@ private fun PassItem(
|
||||
.padding(horizontal = horizontalPadding, vertical = 4.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "$passSatId - ",
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = pass.name,
|
||||
modifier = Modifier
|
||||
@@ -355,7 +350,7 @@ private fun PassItem(
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
val elevColor = elevationColor(pass.maxElevation)
|
||||
Icon(
|
||||
|
||||
+30
-6
@@ -38,6 +38,7 @@ 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
|
||||
@@ -152,16 +153,29 @@ class PassesViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
// Computes sunrise/sunset strings for each unique calendar day in the pass list, plus today for DeepSpace
|
||||
/** Returns a locale-appropriate date format for pass grouping headers. */
|
||||
private fun dateFormat(tz: TimeZone): SimpleDateFormat {
|
||||
val pattern = if (Locale.getDefault().language == "zh") {
|
||||
"yyyy'年'M'月'd'日' EEEE"
|
||||
} else {
|
||||
"EEE, dd MMM yyyy"
|
||||
}
|
||||
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.
|
||||
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()
|
||||
val sdfDate = SimpleDateFormat("EEE, dd MMM yyyy", Locale.ENGLISH).also { it.timeZone = tz }
|
||||
val sdfTime = SimpleDateFormat("HH:mm", Locale.ENGLISH).also { it.timeZone = tz }
|
||||
val sdfDate = dateFormat(tz)
|
||||
val sdfTime = SimpleDateFormat("HH:mm", Locale.getDefault()).also { it.timeZone = tz }
|
||||
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, System.currentTimeMillis())
|
||||
val riseSet = CelestialComputer.findSunRiseSet(stationPos, startOfDayMillis(System.currentTimeMillis(), tz))
|
||||
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
|
||||
@@ -170,7 +184,7 @@ class PassesViewModel(
|
||||
if (pass.isDeepSpace) continue
|
||||
val label = sdfDate.format(Date(pass.aosTime))
|
||||
if (label in result) continue
|
||||
val riseSet = CelestialComputer.findSunRiseSet(stationPos, pass.aosTime)
|
||||
val riseSet = CelestialComputer.findSunRiseSet(stationPos, startOfDayMillis(pass.aosTime, tz))
|
||||
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
|
||||
@@ -178,9 +192,19 @@ 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 = SimpleDateFormat("EEE, dd MMM yyyy", Locale.ENGLISH).also { it.timeZone = tz }
|
||||
val sdfDate = dateFormat(tz)
|
||||
val ordered = LinkedHashMap<String, List<OrbitalPass>>()
|
||||
val deepSpace = passes.filter { it.isDeepSpace }
|
||||
if (deepSpace.isNotEmpty()) ordered["DeepSpace (period >225min)"] = deepSpace
|
||||
|
||||
@@ -5,3 +5,9 @@ plugins {
|
||||
android {
|
||||
namespace = "com.rtbishop.look4sat.feature.radar"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":feature:mutual"))
|
||||
implementation(project(":feature:cw"))
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
||||
}
|
||||
@@ -45,6 +45,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -59,6 +60,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
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
|
||||
@@ -71,9 +74,12 @@ import com.rtbishop.look4sat.core.presentation.getDefaultPass
|
||||
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
|
||||
import com.rtbishop.look4sat.core.presentation.layoutPadding
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.math.PI
|
||||
|
||||
private enum class RadarPage(val title: String) {
|
||||
Transceivers("Transceivers"),
|
||||
Calculator("Calculator"),
|
||||
Sstv("SSTV")
|
||||
}
|
||||
|
||||
@@ -83,17 +89,39 @@ fun RadarDestination(navigateUp: () -> Unit) {
|
||||
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
||||
val viewModel: RadarViewModel = viewModel(factory = RadarViewModel.factory(container))
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val mutualData by container.mutualPassData.collectAsStateWithLifecycle()
|
||||
val navigateUpAndClearMutual = {
|
||||
if (container.mutualPassData.value.endTime > 0L) {
|
||||
container.setMutualPassData(MutualPassData())
|
||||
}
|
||||
navigateUp()
|
||||
}
|
||||
LaunchedEffect(mutualData.endTime) {
|
||||
if (mutualData.endTime <= 0L) return@LaunchedEffect
|
||||
while (true) {
|
||||
val remainingMs = mutualData.endTime - System.currentTimeMillis()
|
||||
if (remainingMs <= 0L) {
|
||||
navigateUpAndClearMutual()
|
||||
return@LaunchedEffect
|
||||
}
|
||||
delay(remainingMs.coerceAtMost(1000L))
|
||||
}
|
||||
}
|
||||
// Sync actual permission state on every recomposition so it survives screen re-entry
|
||||
val hasPermission = ContextCompat.checkSelfPermission(
|
||||
context, Manifest.permission.RECORD_AUDIO
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
LaunchedEffect(hasPermission) {
|
||||
viewModel.onAction(RadarAction.SstvPermissionResult(hasPermission))
|
||||
viewModel.onAction(RadarAction.CwPermissionResult(hasPermission))
|
||||
}
|
||||
val permissionLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { granted -> viewModel.onAction(RadarAction.SstvPermissionResult(granted)) }
|
||||
RadarScreen(uiState, viewModel::onAction, navigateUp, requestMicPermission = {
|
||||
) { granted ->
|
||||
viewModel.onAction(RadarAction.SstvPermissionResult(granted))
|
||||
viewModel.onAction(RadarAction.CwPermissionResult(granted))
|
||||
}
|
||||
RadarScreen(uiState, viewModel::onAction, navigateUpAndClearMutual, mutualData, requestMicPermission = {
|
||||
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
||||
})
|
||||
}
|
||||
@@ -103,12 +131,37 @@ private fun RadarScreen(
|
||||
uiState: RadarState,
|
||||
onAction: (RadarAction) -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
mutualData: MutualPassData,
|
||||
requestMicPermission: () -> Unit
|
||||
) {
|
||||
val upcomingPass = uiState.currentPass ?: getDefaultPass()
|
||||
val addToCalendar: () -> Unit = {
|
||||
uiState.currentPass?.let { onAction(RadarAction.AddToCalendar(it.name, it.aosTime, it.losTime)) }
|
||||
}
|
||||
// Station-B overlay: full track line (only where B's elevation > 0) + live position dot
|
||||
// at the current moment, same display mode as the local station.
|
||||
val trackB = remember(mutualData.trackSamples) {
|
||||
mutualData.trackSamples
|
||||
.filter { it.elevationB > 0.0 }
|
||||
.map {
|
||||
OrbitalPos(
|
||||
azimuth = it.azimuthB * PI / 180.0,
|
||||
elevation = it.elevationB * PI / 180.0,
|
||||
time = it.time
|
||||
)
|
||||
}
|
||||
}
|
||||
val timeNow = System.currentTimeMillis()
|
||||
val trackBPosition = mutualData.trackSamples
|
||||
.filter { it.time <= timeNow }
|
||||
.lastOrNull()
|
||||
?.let {
|
||||
OrbitalPos(
|
||||
azimuth = it.azimuthB * PI / 180.0,
|
||||
elevation = it.elevationB * PI / 180.0,
|
||||
time = it.time
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.layoutPadding()
|
||||
@@ -132,11 +185,11 @@ private fun RadarScreen(
|
||||
}
|
||||
}
|
||||
if (isVertical) {
|
||||
RadarCard(uiState, Modifier.weight(1f))
|
||||
RadarCard(uiState, trackB, trackBPosition, Modifier.weight(1f))
|
||||
PagerCard(uiState, onAction, requestMicPermission, Modifier.weight(1f))
|
||||
} else {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
RadarCard(uiState, Modifier.weight(1f))
|
||||
RadarCard(uiState, trackB, trackBPosition, Modifier.weight(1f))
|
||||
PagerCard(uiState, onAction, requestMicPermission, Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
@@ -150,16 +203,31 @@ private fun PagerCard(
|
||||
requestMicPermission: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val pages = RadarPage.entries
|
||||
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 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()) {
|
||||
PrimaryTabRow(selectedTabIndex = pagerState.currentPage) {
|
||||
val selectedTabIndex = pagerState.currentPage.coerceIn(0, pages.lastIndex)
|
||||
PrimaryTabRow(selectedTabIndex = selectedTabIndex) {
|
||||
pages.forEachIndexed { index, page ->
|
||||
Tab(
|
||||
selected = pagerState.currentPage == index,
|
||||
selected = selectedTabIndex == index,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } },
|
||||
text = { Text(text = page.title, maxLines = 1, overflow = TextOverflow.Ellipsis) }
|
||||
)
|
||||
@@ -176,6 +244,14 @@ private fun PagerCard(
|
||||
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
|
||||
)
|
||||
RadarPage.Sstv -> SstvPage(
|
||||
sstv = uiState.sstv,
|
||||
dopplerFrequency = uiState.transceivers.selectedFrequency?.let { formatFrequency(it) },
|
||||
@@ -189,7 +265,12 @@ private fun PagerCard(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RadarCard(uiState: RadarState, modifier: Modifier = Modifier) {
|
||||
private fun RadarCard(
|
||||
uiState: RadarState,
|
||||
trackB: List<OrbitalPos> = emptyList(),
|
||||
trackBPosition: OrbitalPos? = null,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val satellitePos = uiState.orbitalPos
|
||||
val shouldAnimateBorder = satellitePos?.aboveHorizon == true && satellitePos.eclipsed
|
||||
// Always call these composables unconditionally — conditional composable calls violate
|
||||
@@ -222,6 +303,8 @@ private fun RadarCard(uiState: RadarState, modifier: Modifier = Modifier) {
|
||||
RadarViewCompose(
|
||||
item = position,
|
||||
items = uiState.satTrack,
|
||||
trackB = trackB.takeIf { it.isNotEmpty() },
|
||||
trackBPosition = trackBPosition,
|
||||
azimElev = uiState.orientationValues,
|
||||
shouldShowSweep = uiState.shouldShowSweep,
|
||||
shouldUseCompass = uiState.shouldUseCompass,
|
||||
|
||||
@@ -62,7 +62,14 @@ data class RadarState(
|
||||
val moonPosition: CelestialComputer.MoonPosition? = null,
|
||||
val transceivers: TransceiverSubState = TransceiverSubState(),
|
||||
val radioControl: RadioControlSubState = RadioControlSubState(),
|
||||
val sstv: SstvSubState = SstvSubState()
|
||||
val sstv: SstvSubState = SstvSubState(),
|
||||
val cw: CwSubState = CwSubState(),
|
||||
val mutualSamples: List<Pair<Long, Pair<Double, Double>>> = emptyList(),
|
||||
val mutualStartTime: Long = 0L,
|
||||
val mutualEndTime: Long = 0L,
|
||||
val mutualMaxElev: Double = 10.0,
|
||||
val mutualLabelA: String = "你",
|
||||
val mutualLabelB: String = "友台"
|
||||
)
|
||||
|
||||
enum class SstvStatus { Idle, Recording }
|
||||
@@ -77,6 +84,19 @@ data class SstvSubState(
|
||||
val diagnosticsMetrics: SstvQualityMetrics? = null
|
||||
)
|
||||
|
||||
// --- CW Decoder ---
|
||||
|
||||
enum class CwStatus { Idle, Listening }
|
||||
|
||||
data class CwSubState(
|
||||
val status: CwStatus = CwStatus.Idle,
|
||||
val hasPermission: Boolean = false,
|
||||
val decodedText: String = "",
|
||||
val cwToneFreq: Float = 700f,
|
||||
val isExpanded: Boolean = false,
|
||||
val signalStrength: Float = 0f
|
||||
)
|
||||
|
||||
sealed interface RadarAction {
|
||||
data class AddToCalendar(val name: String, val aosTime: Long, val losTime: Long) : RadarAction
|
||||
data class SelectTransmitter(val uuid: String) : RadarAction
|
||||
@@ -96,4 +116,12 @@ sealed interface RadarAction {
|
||||
data object SstvReset : RadarAction
|
||||
data class SstvSelectMode(val modeName: String) : RadarAction
|
||||
data class SstvPermissionResult(val granted: Boolean) : RadarAction
|
||||
|
||||
// CW actions
|
||||
data object CwStartListening : RadarAction
|
||||
data object CwStopListening : RadarAction
|
||||
data object CwReset : RadarAction
|
||||
data class CwSetToneFreq(val freq: Float) : RadarAction
|
||||
data class CwToggleExpanded(val expanded: Boolean) : RadarAction
|
||||
data class CwPermissionResult(val granted: Boolean) : RadarAction
|
||||
}
|
||||
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user