Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 83 additions & 13 deletions android/app/src/main/java/com/js8call/example/ui/MonitorFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.PopupMenu
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.materialswitch.MaterialSwitch
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.js8call.example.R
import com.js8call.example.model.EngineState
import com.js8call.example.model.MonitorStatus
Expand All @@ -40,6 +44,7 @@ class MonitorFragment : Fragment() {
private lateinit var frequencyButton: MaterialButton
private lateinit var powerSwitch: MaterialSwitch
private lateinit var telemetryText: TextView
private lateinit var driftText: TextView

// Frequency management
private var frequencyEntries = listOf<String>()
Expand Down Expand Up @@ -78,6 +83,7 @@ class MonitorFragment : Fragment() {
frequencyButton = view.findViewById(R.id.frequency_button)
powerSwitch = view.findViewById(R.id.power_switch)
telemetryText = view.findViewById(R.id.telemetry_text)
driftText = view.findViewById(R.id.drift_text)

// Navigation keeps this fragment instance but recreates its views at
// their layout defaults, so the repaint memo has to reset with them or
Expand Down Expand Up @@ -113,6 +119,7 @@ class MonitorFragment : Fragment() {
frequencyButton.setOnClickListener { showFrequencyDialog() }
view.findViewById<MaterialButton>(R.id.monitor_overflow)
.setOnClickListener { showOverflowMenu(it) }
driftText.setOnClickListener { showTimeDriftDialog() }
}

override fun onResume() {
Expand Down Expand Up @@ -214,21 +221,17 @@ class MonitorFragment : Fragment() {
}

private fun renderTelemetry(status: MonitorStatus) {
val drift = if (status.timeDriftMs != 0L) {
String.format("%+d ms", status.timeDriftMs)
} else {
"0 ms"
}
val offsetAndDrift = getString(
R.string.monitor_telemetry,
status.txOffsetHz.toInt(),
drift
)
val offset = getString(R.string.monitor_telemetry, status.txOffsetHz.toInt())
// A stopped engine reads no power, and a leading placeholder just adds noise
telemetryText.text = if (status.powerDb != 0f) {
getString(R.string.monitor_telemetry_power, status.powerDb, offsetAndDrift)
getString(R.string.monitor_telemetry_power, status.powerDb, offset)
} else {
offsetAndDrift
offset
}
driftText.text = if (status.timeDriftMs != 0L) {
String.format("%+d ms", status.timeDriftMs)
} else {
"0 ms"
}
}

Expand All @@ -245,6 +248,10 @@ class MonitorFragment : Fragment() {
armTimeSync()
true
}
R.id.action_time_drift_adjust -> {
showTimeDriftDialog()
true
}
R.id.action_time_drift_reset -> {
resetTimeDrift()
true
Expand All @@ -264,13 +271,73 @@ class MonitorFragment : Fragment() {
}

private fun resetTimeDrift() {
applyTimeDrift(0L)
}

private fun applyTimeDrift(driftMs: Long) {
val intent = Intent(requireContext(), JS8EngineService::class.java).apply {
action = JS8EngineService.ACTION_SET_TIME_DRIFT
putExtra(JS8EngineService.EXTRA_TIME_DRIFT_MS, 0L)
putExtra(JS8EngineService.EXTRA_TIME_DRIFT_MS, driftMs)
}
requireContext().startService(intent)
}

private fun showTimeDriftDialog() {
val view = layoutInflater.inflate(R.layout.dialog_time_drift, null)
val input = view.findViewById<TextInputEditText>(R.id.drift_input)
val inputLayout = view.findViewById<TextInputLayout>(R.id.drift_input_layout)

// Not the strip's copy, which reads 0 after a relaunch
val currentMs = PreferenceManager.getDefaultSharedPreferences(requireContext())
.getLong(JS8EngineService.PREF_TIME_DRIFT_MS, 0L)
input.setText(currentMs.toString())
input.setSelection(input.text?.length ?: 0)

fun nudge(deltaMs: Long) {
val current = input.text?.toString()?.toLongOrNull() ?: 0L
input.setText((current + deltaMs).coerceIn(-DRIFT_LIMIT_MS, DRIFT_LIMIT_MS).toString())
input.setSelection(input.text?.length ?: 0)
inputLayout.error = null
}
view.findViewById<MaterialButton>(R.id.nudge_minus_second).setOnClickListener { nudge(-1000L) }
view.findViewById<MaterialButton>(R.id.nudge_minus_fine).setOnClickListener { nudge(-100L) }
view.findViewById<MaterialButton>(R.id.nudge_plus_fine).setOnClickListener { nudge(100L) }
view.findViewById<MaterialButton>(R.id.nudge_plus_second).setOnClickListener { nudge(1000L) }

val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.monitor_time_drift_title)
.setView(view)
.setPositiveButton(R.string.monitor_time_drift_set, null)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(R.string.monitor_time_drift_reset_action, null)
.create()

dialog.setOnShowListener {
// Set here, not in the builder, so a bad value keeps the dialog open
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val value = input.text?.toString()?.toLongOrNull()
if (value == null || value < -DRIFT_LIMIT_MS || value > DRIFT_LIMIT_MS) {
inputLayout.error = getString(
R.string.monitor_time_drift_range, -DRIFT_LIMIT_MS.toInt(), DRIFT_LIMIT_MS.toInt()
)
return@setOnClickListener
}
applyTimeDrift(value)
Snackbar.make(
requireView(),
getString(R.string.monitor_time_drift_applied, value.toInt()),
Snackbar.LENGTH_SHORT
).show()
dialog.dismiss()
}
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
input.setText("0")
inputLayout.error = null
}
}
dialog.show()
}

private fun startMonitoring() {
// Starting an engine that is already up tears down its audio capture
val state = viewModel.status.value?.state
Expand Down Expand Up @@ -498,5 +565,8 @@ class MonitorFragment : Fragment() {

companion object {
private const val REQUEST_AUDIO_PERMISSION = 1

// The RX ring aligns to the UTC minute, so a larger offset wraps
private const val DRIFT_LIMIT_MS = 30_000L
}
}
101 changes: 101 additions & 0 deletions android/app/src/main/res/layout/dialog_time_drift.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="wrap_content"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:paddingTop="8dp">

<com.google.android.material.textfield.TextInputLayout
android:id="@+id/drift_input_layout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/monitor_time_drift_hint"
app:suffixText="@string/monitor_time_drift_suffix">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/drift_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberSigned"
android:maxLength="7"
android:selectAllOnFocus="true" />

</com.google.android.material.textfield.TextInputLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal">

<com.google.android.material.button.MaterialButton
android:id="@+id/nudge_minus_second"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:insetLeft="0dp"
android:insetRight="0dp"
android:minWidth="0dp"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@string/monitor_time_drift_minus_second" />

<com.google.android.material.button.MaterialButton
android:id="@+id/nudge_minus_fine"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:insetLeft="0dp"
android:insetRight="0dp"
android:minWidth="0dp"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@string/monitor_time_drift_minus_fine" />

<com.google.android.material.button.MaterialButton
android:id="@+id/nudge_plus_fine"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:insetLeft="0dp"
android:insetRight="0dp"
android:minWidth="0dp"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@string/monitor_time_drift_plus_fine" />

<com.google.android.material.button.MaterialButton
android:id="@+id/nudge_plus_second"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:insetLeft="0dp"
android:insetRight="0dp"
android:minWidth="0dp"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@string/monitor_time_drift_plus_second" />

</LinearLayout>

<TextView
android:id="@+id/drift_help"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/monitor_time_drift_help"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?android:attr/textColorSecondary" />

</LinearLayout>
29 changes: 25 additions & 4 deletions android/app/src/main/res/layout/monitor_status_card.xml
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,35 @@

<TextView
android:id="@+id/telemetry_text"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnSurfaceVariant"
tools:text="-18.0 dB · TX 1500 Hz · +12 ms" />
tools:text="-18.0 dB · TX 1500 Hz" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/monitor_telemetry_separator"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnSurfaceVariant" />

<TextView
android:id="@+id/drift_text"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnSurfaceVariant"
tools:text="+12 ms" />

<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />

<com.google.android.material.button.MaterialButton
android:id="@+id/monitor_overflow"
Expand Down
4 changes: 4 additions & 0 deletions android/app/src/main/res/menu/monitor_overflow.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
android:id="@+id/action_time_sync"
android:title="@string/monitor_menu_time_sync" />

<item
android:id="@+id/action_time_drift_adjust"
android:title="@string/monitor_menu_time_drift_adjust" />

<item
android:id="@+id/action_time_drift_reset"
android:title="@string/monitor_menu_time_drift_reset" />
Expand Down
16 changes: 15 additions & 1 deletion android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,25 @@
<string name="monitor_state_error">Error</string>
<string name="monitor_toggle">Monitoring</string>
<string name="monitor_more">More options</string>
<string name="monitor_telemetry">TX %1$d Hz · %2$s</string>
<string name="monitor_telemetry">TX %1$d Hz</string>
<string name="monitor_telemetry_separator" translatable="false">" · "</string>
<string name="monitor_telemetry_power">%1$.1f dB · %2$s</string>
<string name="monitor_menu_audio_device">Audio device</string>
<string name="monitor_menu_time_sync">Sync clock to next decode</string>
<string name="monitor_menu_time_drift_adjust">Adjust time drift…</string>
<string name="monitor_menu_time_drift_reset">Reset time drift</string>
<string name="monitor_time_drift_title">Time drift</string>
<string name="monitor_time_drift_hint">Offset</string>
<string name="monitor_time_drift_suffix">ms</string>
<string name="monitor_time_drift_minus_second">−1s</string>
<string name="monitor_time_drift_minus_fine">−100</string>
<string name="monitor_time_drift_plus_fine">+100</string>
<string name="monitor_time_drift_plus_second">+1s</string>
<string name="monitor_time_drift_help">Shifts decode windows and transmit timing without changing the device clock. Negative when the clock runs fast.</string>
<string name="monitor_time_drift_set">Set</string>
<string name="monitor_time_drift_reset_action">Reset</string>
<string name="monitor_time_drift_applied">Time drift %+d ms</string>
<string name="monitor_time_drift_range">Enter %1$+d to %2$+d ms</string>
<string name="monitor_audio_device_switching">Switching to %s…</string>
<string name="monitor_audio_device_selected">Audio device: %s</string>
<string name="monitor_audio_device_lost">%s was removed. Monitoring stopped.</string>
Expand Down
Loading