fix: remove the receive-only notice, and stop the CW record scrolling itself
Two things the operator asked for after running 4.6.1. The receive-only notice named a state this app does not have. APRS-IS lets an unverified station connect and then discards its packets, which is what "receive-only" means at the protocol level - but this app only reports its own position. There is no receiving side to it, and none intended, so telling the operator they are in receive-only mode described a mode that does not exist here. Without a passcode the packet does not arrive, and the unverified notice already says exactly that. The string is gone from all five locales, along with the AprsReport.receiveOnly field, which had no remaining consumer. AprsPasscode.classify stays: loginValue still uses it, and its tests hold the distinction between a deliberate -1 and a typo, which is a separate defect worth keeping fixed. The CW history pane no longer follows the decode. Its whole purpose is to be read back, and a record that scrolls itself is worse than paper - as the operator put it, if it scrolls away then why use a decoder instead of listening and writing it down, since paper does not erase itself. The single line above it is where new characters appear; that still scrolls, because that is its job. A down arrow in the toolbar jumps to the newest text when wanted. Not fixed here: logged times in the log page look wrong and inconsistent. I proposed a timezone explanation and wrote a probe, and the probe disproved it - on a real JVM both the session header and the row times are stable and both resolve to local time. That reverted attempt is not in this commit. The cause is still unknown.
This commit is contained in:
1 parent
e315c87f05
commit
8445c17033
13 files changed
+37
-45
No files matched your search
@@ -123,13 +123,13 @@ class AprsForegroundService : Service() {
|
|||||||
// An unverified login needs its own message: the write succeeded, so a bare
|
// An unverified login needs its own message: the write succeeded, so a bare
|
||||||
// failure notice would send the operator looking at their network when the
|
// failure notice would send the operator looking at their network when the
|
||||||
// problem is the passcode - and APRS-IS is dropping every packet meanwhile.
|
// problem is the passcode - and APRS-IS is dropping every packet meanwhile.
|
||||||
|
// No receive-only notice. APRS-IS lets an unverified station connect and then
|
||||||
|
// discards its packets, but this app only reports its own position - there is no
|
||||||
|
// receiving side to it - so telling the operator they are "in receive-only mode"
|
||||||
|
// named a state that does not exist here. Without a passcode the packet does not
|
||||||
|
// arrive, and that is what the failure notice says.
|
||||||
val msg = when {
|
val msg = when {
|
||||||
report.ok -> getString(R.string.aprs_toast_ok)
|
report.ok -> getString(R.string.aprs_toast_ok)
|
||||||
// Receive-only first: it logs in with -1 exactly as a wrong passcode does and
|
|
||||||
// the server answers "unverified" to both, so without this branch the one safe
|
|
||||||
// way to test a setup reported itself as a configuration error.
|
|
||||||
!report.verified && report.receiveOnly ->
|
|
||||||
getString(R.string.aprs_toast_receive_only)
|
|
||||||
!report.verified -> getString(R.string.aprs_toast_unverified)
|
!report.verified -> getString(R.string.aprs_toast_unverified)
|
||||||
else -> getString(R.string.aprs_toast_fail, report.detail)
|
else -> getString(R.string.aprs_toast_fail, report.detail)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ data class AprsReport(
|
|||||||
* test a setup without putting anything on the network - was reported as a wrong passcode
|
* test a setup without putting anything on the network - was reported as a wrong passcode
|
||||||
* and sent the operator to fix something they had set on purpose.
|
* and sent the operator to fix something they had set on purpose.
|
||||||
*/
|
*/
|
||||||
val receiveOnly: Boolean = false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Report scheduler (periodic + manual trigger); connection management lives in the foreground service */
|
/** Report scheduler (periodic + manual trigger); connection management lives in the foreground service */
|
||||||
@@ -127,13 +127,6 @@ class AprsReporter(
|
|||||||
}
|
}
|
||||||
val packetLine = (beacon as AprsBeacon.Result.Line).text
|
val packetLine = (beacon as AprsBeacon.Result.Line).text
|
||||||
|
|
||||||
// Receive-only is a deliberate choice and has to be told apart from a typo, because
|
|
||||||
// both log in with -1 and the server answers "unverified" to each. Classifying rather
|
|
||||||
// than asking canTransmit: that collapses ReceiveOnly and Mismatch into one boolean,
|
|
||||||
// so a mistyped passcode would be told it is in receive-only mode - the same
|
|
||||||
// mis-diagnosis as before, pointing the other way.
|
|
||||||
val wantsReceiveOnly =
|
|
||||||
AprsPasscode.classify(cfg.callsign, cfg.passcode) is AprsPasscode.Entry.ReceiveOnly
|
|
||||||
val c = client ?: AprsIsClient(
|
val c = client ?: AprsIsClient(
|
||||||
host = cfg.server,
|
host = cfg.server,
|
||||||
port = cfg.port,
|
port = cfg.port,
|
||||||
@@ -166,14 +159,13 @@ class AprsReporter(
|
|||||||
// report, say that instead.
|
// report, say that instead.
|
||||||
val reported = when {
|
val reported = when {
|
||||||
ok -> detail
|
ok -> detail
|
||||||
refused && wantsReceiveOnly -> "receive-only, not forwarded"
|
|
||||||
refused -> "login not verified"
|
refused -> "login not verified"
|
||||||
else -> detail
|
else -> detail
|
||||||
}
|
}
|
||||||
onReport(
|
onReport(
|
||||||
AprsReport(
|
AprsReport(
|
||||||
System.currentTimeMillis(), packetLine, ok, reported,
|
System.currentTimeMillis(), packetLine, ok, reported,
|
||||||
verified = !refused, receiveOnly = wantsReceiveOnly
|
verified = !refused
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if (ok) onState(AprsState.Connected) else onState(AprsState.Error)
|
if (ok) onState(AprsState.Connected) else onState(AprsState.Error)
|
||||||
|
|||||||
@@ -39,7 +39,6 @@
|
|||||||
<string name="aprs_toast_ok">APRS: laporan terkirim</string>
|
<string name="aprs_toast_ok">APRS: laporan terkirim</string>
|
||||||
<string name="aprs_toast_fail">APRS: laporan gagal - %1$s</string>
|
<string name="aprs_toast_fail">APRS: laporan gagal - %1$s</string>
|
||||||
<string name="aprs_toast_unverified">APRS-IS tidak memverifikasi passcode Anda. Laporan Anda tidak diteruskan - periksa tanda panggil dan passcode di Pengaturan.</string>
|
<string name="aprs_toast_unverified">APRS-IS tidak memverifikasi passcode Anda. Laporan Anda tidak diteruskan - periksa tanda panggil dan passcode di Pengaturan.</string>
|
||||||
<string name="aprs_toast_receive_only">APRS-IS menerima koneksi dalam mode terima-saja, laporan Anda tidak diteruskan. Masukkan passcode untuk mengirim.</string>
|
|
||||||
<string name="aprs_toast_not_configured">APRS belum dikonfigurasi - isi panggilan dulu</string>
|
<string name="aprs_toast_not_configured">APRS belum dikonfigurasi - isi panggilan dulu</string>
|
||||||
<string name="prefs_aprs_last_report">Laporan terakhir: %1$s %2$s</string>
|
<string name="prefs_aprs_last_report">Laporan terakhir: %1$s %2$s</string>
|
||||||
<string name="prefs_aprs_last_ok">OK</string>
|
<string name="prefs_aprs_last_ok">OK</string>
|
||||||
|
|||||||
@@ -39,7 +39,6 @@
|
|||||||
<string name="aprs_toast_ok">APRS: laporan terkirim</string>
|
<string name="aprs_toast_ok">APRS: laporan terkirim</string>
|
||||||
<string name="aprs_toast_fail">APRS: laporan gagal - %1$s</string>
|
<string name="aprs_toast_fail">APRS: laporan gagal - %1$s</string>
|
||||||
<string name="aprs_toast_unverified">APRS-IS tidak memverifikasi passcode Anda. Laporan Anda tidak diteruskan - periksa tanda panggil dan passcode di Pengaturan.</string>
|
<string name="aprs_toast_unverified">APRS-IS tidak memverifikasi passcode Anda. Laporan Anda tidak diteruskan - periksa tanda panggil dan passcode di Pengaturan.</string>
|
||||||
<string name="aprs_toast_receive_only">APRS-IS menerima koneksi dalam mode terima-saja, laporan Anda tidak diteruskan. Masukkan passcode untuk mengirim.</string>
|
|
||||||
<string name="aprs_toast_not_configured">APRS belum dikonfigurasi - isi panggilan dulu</string>
|
<string name="aprs_toast_not_configured">APRS belum dikonfigurasi - isi panggilan dulu</string>
|
||||||
<string name="prefs_aprs_last_report">Laporan terakhir: %1$s %2$s</string>
|
<string name="prefs_aprs_last_report">Laporan terakhir: %1$s %2$s</string>
|
||||||
<string name="prefs_aprs_last_ok">OK</string>
|
<string name="prefs_aprs_last_ok">OK</string>
|
||||||
|
|||||||
@@ -40,7 +40,6 @@
|
|||||||
<string name="aprs_toast_ok">APRS: rapor gönderildi</string>
|
<string name="aprs_toast_ok">APRS: rapor gönderildi</string>
|
||||||
<string name="aprs_toast_fail">APRS: rapor başarısız - %1$s</string>
|
<string name="aprs_toast_fail">APRS: rapor başarısız - %1$s</string>
|
||||||
<string name="aprs_toast_unverified">APRS-IS passcode bilginizi doğrulamadı. Raporlarınız iletilmiyor - Ayarlar bölümünde çağrı işareti ve passcode bilgisini kontrol edin.</string>
|
<string name="aprs_toast_unverified">APRS-IS passcode bilginizi doğrulamadı. Raporlarınız iletilmiyor - Ayarlar bölümünde çağrı işareti ve passcode bilgisini kontrol edin.</string>
|
||||||
<string name="aprs_toast_receive_only">APRS-IS baglantiyi yalnizca alma modunda kabul etti, raporlariniz iletilmiyor. Gondermek icin passcode girin.</string>
|
|
||||||
<string name="aprs_toast_not_configured">APRS yapılandırılmadı - önce çağrı girin</string>
|
<string name="aprs_toast_not_configured">APRS yapılandırılmadı - önce çağrı girin</string>
|
||||||
<string name="prefs_aprs_last_report">Son rapor: %1$s %2$s</string>
|
<string name="prefs_aprs_last_report">Son rapor: %1$s %2$s</string>
|
||||||
<string name="prefs_aprs_last_ok">OK</string>
|
<string name="prefs_aprs_last_ok">OK</string>
|
||||||
|
|||||||
@@ -58,7 +58,6 @@
|
|||||||
<string name="aprs_toast_ok">APRS: 上报成功</string>
|
<string name="aprs_toast_ok">APRS: 上报成功</string>
|
||||||
<string name="aprs_toast_fail">APRS: 上报失败 - %1$s</string>
|
<string name="aprs_toast_fail">APRS: 上报失败 - %1$s</string>
|
||||||
<string name="aprs_toast_unverified">APRS-IS 未验证你的 passcode, 上报不会被转发 - 请在设置里检查呼号与 passcode。</string>
|
<string name="aprs_toast_unverified">APRS-IS 未验证你的 passcode, 上报不会被转发 - 请在设置里检查呼号与 passcode。</string>
|
||||||
<string name="aprs_toast_receive_only">APRS-IS 以只收模式接受了连接, 上报不会被转发。填入 passcode 才能发射。</string>
|
|
||||||
<string name="aprs_toast_not_configured">APRS 未配置,请先填呼号</string>
|
<string name="aprs_toast_not_configured">APRS 未配置,请先填呼号</string>
|
||||||
<string name="prefs_aprs_last_report">上次上报: %1$s %2$s</string>
|
<string name="prefs_aprs_last_report">上次上报: %1$s %2$s</string>
|
||||||
<string name="prefs_aprs_last_ok">成功</string>
|
<string name="prefs_aprs_last_ok">成功</string>
|
||||||
|
|||||||
@@ -59,7 +59,6 @@
|
|||||||
<string name="aprs_toast_ok">APRS: report sent OK</string>
|
<string name="aprs_toast_ok">APRS: report sent OK</string>
|
||||||
<string name="aprs_toast_fail">APRS: report failed - %1$s</string>
|
<string name="aprs_toast_fail">APRS: report failed - %1$s</string>
|
||||||
<string name="aprs_toast_unverified">APRS-IS did not verify your passcode. Your reports are not being passed on - check the callsign and passcode in Settings.</string>
|
<string name="aprs_toast_unverified">APRS-IS did not verify your passcode. Your reports are not being passed on - check the callsign and passcode in Settings.</string>
|
||||||
<string name="aprs_toast_receive_only">APRS-IS accepted the connection in receive-only mode, so your reports are not forwarded. Enter your passcode to transmit.</string>
|
|
||||||
<string name="aprs_toast_not_configured">APRS not configured - set callsign first</string>
|
<string name="aprs_toast_not_configured">APRS not configured - set callsign first</string>
|
||||||
<string name="prefs_aprs_last_report">Last report: %1$s %2$s</string>
|
<string name="prefs_aprs_last_report">Last report: %1$s %2$s</string>
|
||||||
<string name="prefs_aprs_last_ok">OK</string>
|
<string name="prefs_aprs_last_ok">OK</string>
|
||||||
|
|||||||
@@ -51,8 +51,10 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -75,7 +77,6 @@ import kotlin.math.roundToInt
|
|||||||
* Not zero: an animated scroll settles a pixel or two short of the maximum, and an exact
|
* Not zero: an animated scroll settles a pixel or two short of the maximum, and an exact
|
||||||
* comparison would drop out of follow-mode the moment it did.
|
* comparison would drop out of follow-mode the moment it did.
|
||||||
*/
|
*/
|
||||||
private const val AUTOSCROLL_SLACK_PX = 4
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full-page CW decoder backed by DeepCW.
|
* Full-page CW decoder backed by DeepCW.
|
||||||
@@ -103,6 +104,10 @@ fun CwDecodeScreen() {
|
|||||||
}
|
}
|
||||||
var permanentlyDenied by remember { mutableStateOf(false) }
|
var permanentlyDenied by remember { mutableStateOf(false) }
|
||||||
var isListening by remember { mutableStateOf(false) }
|
var isListening by remember { mutableStateOf(false) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
// Hoisted so the toolbar button can reach it: the record below does not follow
|
||||||
|
// the decode, so jumping to the newest text has to be an explicit action.
|
||||||
|
val transcriptScroll = rememberScrollState()
|
||||||
|
|
||||||
val decodedText by decoder.decodedText.collectAsState()
|
val decodedText by decoder.decodedText.collectAsState()
|
||||||
val historyText by decoder.historyText.collectAsState()
|
val historyText by decoder.historyText.collectAsState()
|
||||||
@@ -157,6 +162,17 @@ fun CwDecodeScreen() {
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(start = 12.dp)
|
.padding(start = 12.dp)
|
||||||
)
|
)
|
||||||
|
// Jump to the newest text. The record below deliberately does not follow the decode,
|
||||||
|
// so this is how you get back to the bottom after reading earlier traffic.
|
||||||
|
IconButton(onClick = { scope.launch { transcriptScroll.animateScrollTo(transcriptScroll.maxValue) } }) {
|
||||||
|
Icon(
|
||||||
|
// The shared arrow points right; rotated to point down. Adding a second
|
||||||
|
// drawable for the same shape is how icon sets start to drift.
|
||||||
|
painter = painterResource(CoreR.drawable.ic_arrow),
|
||||||
|
contentDescription = stringResource(R.string.cw_scroll_newest),
|
||||||
|
modifier = Modifier.rotate(90f)
|
||||||
|
)
|
||||||
|
}
|
||||||
IconButton(onClick = { isListening = !isListening }) {
|
IconButton(onClick = { isListening = !isListening }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
@@ -232,34 +248,18 @@ fun CwDecodeScreen() {
|
|||||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f))
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f))
|
||||||
) {
|
) {
|
||||||
val transcript = (historyText + decodedText).ifEmpty { "…" }
|
val transcript = (historyText + decodedText).ifEmpty { "…" }
|
||||||
val scroll = rememberScrollState()
|
// This pane does not follow the decode. The single line above it is where new
|
||||||
// Follow the newest text, but stop as soon as the operator scrolls away, so
|
// characters appear; this is the record, and a record that scrolls itself is worse
|
||||||
// reading back over earlier traffic is not undone by the next decode.
|
// than paper - you cannot read back over what just arrived because it keeps moving.
|
||||||
//
|
//
|
||||||
// A boolean rather than comparing position against maxValue: maxValue is
|
// It used to auto-scroll to the bottom on every decode, dropping out of follow mode
|
||||||
// written during layout, after the composition that would read it, so such a
|
// only once the operator had already fought it by scrolling away. Now it stays where
|
||||||
// comparison tests the previous frame's height and drifts short of the true
|
// it was put, and the button below jumps to the newest text on request.
|
||||||
// bottom until it latches out of follow-mode altogether.
|
|
||||||
var following by remember { mutableStateOf(true) }
|
|
||||||
LaunchedEffect(scroll) {
|
|
||||||
snapshotFlow { scroll.isScrollInProgress to scroll.value }
|
|
||||||
.collect { (scrolling, value) ->
|
|
||||||
if (scrolling) following = value >= scroll.maxValue - AUTOSCROLL_SLACK_PX
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(transcript, following) {
|
|
||||||
if (!following) return@LaunchedEffect
|
|
||||||
// Twice: the first pass lands at the height known when it started, the
|
|
||||||
// second covers growth that arrived while it was animating.
|
|
||||||
repeat(2) {
|
|
||||||
if (scroll.value < scroll.maxValue) scroll.animateScrollTo(scroll.maxValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(
|
Text(
|
||||||
text = transcript,
|
text = transcript,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(scroll)
|
.verticalScroll(transcriptScroll)
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<string name="cw_pause">Jeda pendekodean</string>
|
<string name="cw_pause">Jeda pendekodean</string>
|
||||||
<string name="cw_resume">Lanjutkan pendekodean</string>
|
<string name="cw_resume">Lanjutkan pendekodean</string>
|
||||||
<string name="cw_clear">Hapus teks hasil dekode</string>
|
<string name="cw_clear">Hapus teks hasil dekode</string>
|
||||||
|
<string name="cw_scroll_newest">Lompat ke teks terbaru</string>
|
||||||
<string name="cw_mic_permission">Izin mikrofon diperlukan untuk pendekodean CW</string>
|
<string name="cw_mic_permission">Izin mikrofon diperlukan untuk pendekodean CW</string>
|
||||||
<string name="cw_grant_permission">Berikan izin</string>
|
<string name="cw_grant_permission">Berikan izin</string>
|
||||||
<string name="cw_open_settings">Buka pengaturan aplikasi</string>
|
<string name="cw_open_settings">Buka pengaturan aplikasi</string>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<string name="cw_pause">Jeda pendekodean</string>
|
<string name="cw_pause">Jeda pendekodean</string>
|
||||||
<string name="cw_resume">Lanjutkan pendekodean</string>
|
<string name="cw_resume">Lanjutkan pendekodean</string>
|
||||||
<string name="cw_clear">Hapus teks hasil dekode</string>
|
<string name="cw_clear">Hapus teks hasil dekode</string>
|
||||||
|
<string name="cw_scroll_newest">Lompat ke teks terbaru</string>
|
||||||
<string name="cw_mic_permission">Izin mikrofon diperlukan untuk pendekodean CW</string>
|
<string name="cw_mic_permission">Izin mikrofon diperlukan untuk pendekodean CW</string>
|
||||||
<string name="cw_grant_permission">Berikan izin</string>
|
<string name="cw_grant_permission">Berikan izin</string>
|
||||||
<string name="cw_open_settings">Buka pengaturan aplikasi</string>
|
<string name="cw_open_settings">Buka pengaturan aplikasi</string>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<string name="cw_pause">Çözmeyi duraklat</string>
|
<string name="cw_pause">Çözmeyi duraklat</string>
|
||||||
<string name="cw_resume">Çözmeyi sürdür</string>
|
<string name="cw_resume">Çözmeyi sürdür</string>
|
||||||
<string name="cw_clear">Çözülen metni temizle</string>
|
<string name="cw_clear">Çözülen metni temizle</string>
|
||||||
|
<string name="cw_scroll_newest">En yeni metne git</string>
|
||||||
<string name="cw_mic_permission">CW çözümü için mikrofon izni gerekli</string>
|
<string name="cw_mic_permission">CW çözümü için mikrofon izni gerekli</string>
|
||||||
<string name="cw_grant_permission">İzin ver</string>
|
<string name="cw_grant_permission">İzin ver</string>
|
||||||
<string name="cw_open_settings">Uygulama ayarlarını aç</string>
|
<string name="cw_open_settings">Uygulama ayarlarını aç</string>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<string name="cw_pause">暂停解码</string>
|
<string name="cw_pause">暂停解码</string>
|
||||||
<string name="cw_resume">继续解码</string>
|
<string name="cw_resume">继续解码</string>
|
||||||
<string name="cw_clear">清空解码文本</string>
|
<string name="cw_clear">清空解码文本</string>
|
||||||
|
<string name="cw_scroll_newest">跳到最新</string>
|
||||||
<string name="cw_mic_permission">CW 解码需要麦克风权限</string>
|
<string name="cw_mic_permission">CW 解码需要麦克风权限</string>
|
||||||
<string name="cw_grant_permission">授予权限</string>
|
<string name="cw_grant_permission">授予权限</string>
|
||||||
<string name="cw_open_settings">打开应用设置</string>
|
<string name="cw_open_settings">打开应用设置</string>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<string name="cw_pause">Pause decoding</string>
|
<string name="cw_pause">Pause decoding</string>
|
||||||
<string name="cw_resume">Resume decoding</string>
|
<string name="cw_resume">Resume decoding</string>
|
||||||
<string name="cw_clear">Clear decoded text</string>
|
<string name="cw_clear">Clear decoded text</string>
|
||||||
|
<string name="cw_scroll_newest">Jump to the newest text</string>
|
||||||
<string name="cw_mic_permission">Microphone permission is required for CW decoding</string>
|
<string name="cw_mic_permission">Microphone permission is required for CW decoding</string>
|
||||||
<string name="cw_grant_permission">Grant permission</string>
|
<string name="cw_grant_permission">Grant permission</string>
|
||||||
<string name="cw_open_settings">Open app settings</string>
|
<string name="cw_open_settings">Open app settings</string>
|
||||||
|
|||||||
Reference in new issue
Block a user