diff --git a/android/README.md b/android/README.md index 8e7a8935..507862e5 100644 --- a/android/README.md +++ b/android/README.md @@ -64,6 +64,32 @@ To run one class: JAVA_HOME=/opt/homebrew/opt/openjdk ANDROID_HOME=~/Library/Android/sdk ./gradlew :js8core-lib:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.js8call.core.JS8EngineLoopbackTest ``` +## Inject decodes + +On a station with a rig, turn off Enable Autoreply and Enable Relay in Settings first. Both act on injected decodes like real ones, and a reply queued with monitoring off goes out at the next start. + +Debug builds take synthetic decodes over adb, no audio needed. With the app in front, and your callsign from Settings in place of `N0CALL`: + +```bash +adb shell am broadcast -n com.js8call.example/.debug.DebugDecodeReceiver --es text "'K1ABC: N0CALL SNR?'" +``` + +Optional extras: + +- `--ei snr -12` (default -10) +- `--ef freq 1450` audio offset in Hz (default 1500) +- `--ei mode 2` submode: 0 Normal, 1 Fast, 2 Turbo, 4 Slow (default 0) +- `--ei type 1` frame flags, added together: 1 first, 2 last, 4 data (default 3, a single frame) + +A multi-frame message is one broadcast per frame, back to back, at the same `freq`. The frames after a `MSG` header are data frames: 4 in the middle, 6 last. + +```bash +adb shell am broadcast -n com.js8call.example/.debug.DebugDecodeReceiver --es text "'K1ABC: N0CALL MSG HELLO'" --ei type 1 +adb shell am broadcast -n com.js8call.example/.debug.DebugDecodeReceiver --es text "' WORLD'" --ei type 6 +``` + +Output: the Decodes tab, and Messages for a complete `MSG` to your callsign. Log: `adb logcat -s JS8EngineService DebugDecodeReceiver` + ## Build Release APK (signed) Create a keystore once: diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..3aa509ab --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/android/app/src/debug/java/com/js8call/example/debug/DebugDecodeReceiver.kt b/android/app/src/debug/java/com/js8call/example/debug/DebugDecodeReceiver.kt new file mode 100644 index 00000000..a510edf2 --- /dev/null +++ b/android/app/src/debug/java/com/js8call/example/debug/DebugDecodeReceiver.kt @@ -0,0 +1,27 @@ +package com.js8call.example.debug + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import com.js8call.example.service.JS8EngineService + +/** Forwards adb decode injections to [JS8EngineService]. */ +class DebugDecodeReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val forward = Intent(context, JS8EngineService::class.java).apply { + action = JS8EngineService.ACTION_DEBUG_INJECT_DECODE + intent.extras?.let { putExtras(it) } + } + try { + context.startService(forward) + } catch (e: IllegalStateException) { + // Refused from the background unless the engine is running + Log.w(TAG, "Injected decode dropped: bring the app to the front") + } + } + + companion object { + private const val TAG = "DebugDecodeReceiver" + } +} diff --git a/android/app/src/main/java/com/js8call/example/service/JS8EngineService.kt b/android/app/src/main/java/com/js8call/example/service/JS8EngineService.kt index 836cfc3c..bfa48bd6 100644 --- a/android/app/src/main/java/com/js8call/example/service/JS8EngineService.kt +++ b/android/app/src/main/java/com/js8call/example/service/JS8EngineService.kt @@ -355,6 +355,9 @@ class JS8EngineService : Service() { driftMmaN = if (driftMs == 0L) 0 else 1 applyTimeDrift(driftMs) } + ACTION_DEBUG_INJECT_DECODE -> { + if (BuildConfig.DEBUG) injectDecode(intent) + } } return START_STICKY } @@ -552,11 +555,7 @@ class JS8EngineService : Service() { // Broadcast on main thread mainHandler.post { maybeApplyTimeSync(driftMs) - updateHeardCallsign(text) - broadcastDecode(utc, snr, dt, freq, text, type, quality, mode, driftMs) - handleRelayFrame(text, snr, mode, freq, type) - maybeHandleIncomingMessage(text, snr, freq, type, mode) - maybeHandleAutoReply(text, snr, mode) + handleDecode(utc, snr, dt, freq, text, type, quality, mode, driftMs) maybeReportToPskReporter(utc, snr, freq, text) } } @@ -1686,6 +1685,35 @@ class JS8EngineService : Service() { LocalBroadcastManager.getInstance(this).sendBroadcast(intent) } + // Shared with injected decodes + private fun handleDecode( + utc: Int, snr: Int, dt: Float, freq: Float, + text: String, type: Int, quality: Float, mode: Int, driftMs: Int + ) { + updateHeardCallsign(text) + broadcastDecode(utc, snr, dt, freq, text, type, quality, mode, driftMs) + handleRelayFrame(text, snr, mode, freq, type) + maybeHandleIncomingMessage(text, snr, freq, type, mode) + maybeHandleAutoReply(text, snr, mode) + } + + private fun injectDecode(intent: Intent) { + val text = intent.getStringExtra(EXTRA_TEXT) + if (text.isNullOrBlank()) { + Log.w(TAG, "Injected decode has no text") + return + } + val snr = intent.getIntExtra(EXTRA_SNR, -10) + val freq = intent.getFloatExtra(EXTRA_FREQ, 1500f) + val type = intent.getIntExtra(EXTRA_TYPE, 3) + val mode = intent.getIntExtra(EXTRA_MODE, 0) + val now = Calendar.getInstance(TimeZone.getTimeZone("UTC")) + val utc = now.get(Calendar.HOUR_OF_DAY) * 10000 + + now.get(Calendar.MINUTE) * 100 + now.get(Calendar.SECOND) + Log.i(TAG, "Injected decode: '$text' snr=$snr freq=$freq type=$type mode=$mode") + handleDecode(utc, snr, 0f, freq, text, type, 1f, mode, 0) + } + private fun broadcastDecode( utc: Int, snr: Int, dt: Float, freq: Float, text: String, type: Int, quality: Float, mode: Int, driftMs: Int @@ -4067,6 +4095,7 @@ class JS8EngineService : Service() { const val ACTION_SET_TIME_DRIFT = "com.js8call.example.ACTION_SET_TIME_DRIFT" const val ACTION_TIME_DRIFT = "com.js8call.example.ACTION_TIME_DRIFT" const val ACTION_RIG_STATUS = "com.js8call.example.ACTION_RIG_STATUS" + const val ACTION_DEBUG_INJECT_DECODE = "com.js8call.example.ACTION_DEBUG_INJECT_DECODE" // Engine states const val STATE_STOPPED = "stopped"