Initial release of GPS2Audio

This commit is contained in:
Marcel Mayer
2026-05-05 20:54:05 +02:00
commit b2aa1c167c
48 changed files with 11570 additions and 0 deletions
@@ -0,0 +1,220 @@
package de.waypointaudio.service
import android.content.Context
import android.content.Intent
import android.util.Log
import de.waypointaudio.data.AudioRoutingSettings
import de.waypointaudio.data.AudioRoutingStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
/**
* App-weiter Singleton-Manager für Live / PTT.
*
* Steuert den [LivePttService] (startet/stoppt den Vordergrundservice) und
* verwaltet den Status (pttActive, Gerätewahl, Fehler).
*
* Audio-Priorität:
* - Wenn PTT aktiv: Begleitmusik wird pausiert (über [BackgroundMusicManager.pauseMusic]).
* - Wegpunkt-Tracks: Der [WaypointLocationService] prüft [isActive] und verschiebt Wiedergabe.
* - Beim PTT-Ende: Begleitmusik wird wiederhergestellt (über [BackgroundMusicManager.afterWaypointAudio]).
*
* Bekannte Einschränkungen:
* - Android-Audiorouting via setPreferredDevice() wird nicht von allen Geräten unterstützt.
* - Bluetooth SCO: Erfordert ggf. AudioManager.startBluetoothSco() für volles Routing.
* Dieser MVP setzt setPreferredDevice() und loggt Fehler.
* - Echo/Feedback: Bei Nutzung des eingebauten Lautsprechers ohne Headset zu erwarten.
*/
class LivePttManager private constructor(private val appContext: Context) {
companion object {
private const val TAG = "LivePttManager"
@Volatile
private var instance: LivePttManager? = null
fun getInstance(context: Context): LivePttManager {
return instance ?: synchronized(this) {
instance ?: LivePttManager(context.applicationContext).also { instance = it }
}
}
}
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val store = AudioRoutingStore(appContext)
// ─── Öffentliche States ───────────────────────────────────────────────────
/** true wenn PTT gerade aktiv ist. */
private val _pttActive = MutableStateFlow(false)
val pttActive: StateFlow<Boolean> = _pttActive.asStateFlow()
/** Aktuell gespeicherte Routing-Einstellungen. */
private val _routingSettings = MutableStateFlow(AudioRoutingSettings())
val routingSettings: StateFlow<AudioRoutingSettings> = _routingSettings.asStateFlow()
/** Verfügbare Eingabegeräte (aktualisiert beim Öffnen der Einstellungen). */
private val _inputDevices = MutableStateFlow<List<AudioDeviceItem>>(emptyList())
val inputDevices: StateFlow<List<AudioDeviceItem>> = _inputDevices.asStateFlow()
/** Verfügbare Ausgabegeräte (aktualisiert beim Öffnen der Einstellungen). */
private val _outputDevices = MutableStateFlow<List<AudioDeviceItem>>(emptyList())
val outputDevices: StateFlow<List<AudioDeviceItem>> = _outputDevices.asStateFlow()
/**
* Status-/Fehlermeldung für die UI.
* null = kein Fehler / kein Hinweis
*/
private val _statusMessage = MutableStateFlow<String?>(null)
val statusMessage: StateFlow<String?> = _statusMessage.asStateFlow()
// ─── Initialisierung ──────────────────────────────────────────────────────
init {
// Gespeicherte Einstellungen laden
scope.launch {
store.settings.collect { settings ->
_routingSettings.value = settings
}
}
}
// ─── PTT starten / stoppen ────────────────────────────────────────────────
/**
* Startet PTT:
* 1. Atmo pausieren
* 2. Foreground Service starten
* 3. Status setzen
*/
fun startPtt() {
if (_pttActive.value) {
Log.d(TAG, "PTT bereits aktiv kein Neustart")
return
}
// Begleitmusik pausieren
pauseAtmo()
val settings = _routingSettings.value
val intent = Intent(appContext, LivePttService::class.java).apply {
action = LivePttService.ACTION_START_PTT
settings.selectedInputDeviceId?.let {
putExtra(LivePttService.EXTRA_INPUT_DEVICE_ID, it)
}
settings.selectedOutputDeviceId?.let {
putExtra(LivePttService.EXTRA_OUTPUT_DEVICE_ID, it)
}
}
runCatching {
appContext.startForegroundService(intent)
_pttActive.value = true
_statusMessage.value = null
Log.i(TAG, "PTT gestartet")
}.onFailure { e ->
Log.e(TAG, "PTT konnte nicht gestartet werden", e)
_statusMessage.value = "PTT-Fehler: ${e.localizedMessage ?: e.javaClass.simpleName}"
restoreAtmo()
}
}
/**
* Stoppt PTT:
* 1. Foreground Service stoppen
* 2. Atmo wiederherstellen
* 3. Status setzen
*/
fun stopPtt() {
if (!_pttActive.value) return
val intent = Intent(appContext, LivePttService::class.java).apply {
action = LivePttService.ACTION_STOP_PTT
}
runCatching {
appContext.startService(intent)
}.onFailure { e ->
Log.w(TAG, "Fehler beim Stoppen des PTT-Service", e)
}
_pttActive.value = false
_statusMessage.value = null
restoreAtmo()
Log.i(TAG, "PTT gestoppt")
}
/** Schaltet PTT ein/aus. */
fun togglePtt() {
if (_pttActive.value) stopPtt() else startPtt()
}
// ─── Geräteverwaltung ────────────────────────────────────────────────────
/** Lädt die verfügbaren Audiogeräte neu. */
fun refreshDevices() {
_inputDevices.value = AudioDeviceManager.getInputDevices(appContext)
_outputDevices.value = AudioDeviceManager.getOutputDevices(appContext)
Log.d(TAG, "Geräte neu geladen: ${_inputDevices.value.size} Eingabe, ${_outputDevices.value.size} Ausgabe")
}
/** Speichert neue Routing-Einstellungen persistent. */
fun saveRoutingSettings(settings: AudioRoutingSettings) {
_routingSettings.value = settings
scope.launch {
runCatching {
store.save(settings)
Log.i(TAG, "Routing-Einstellungen gespeichert: Input=${settings.selectedInputDeviceId}, Output=${settings.selectedOutputDeviceId}")
}.onFailure { e ->
Log.e(TAG, "Fehler beim Speichern der Routing-Einstellungen", e)
}
}
}
/** Löscht die Statusmeldung. */
fun clearStatus() {
_statusMessage.value = null
}
// ─── Atmo-Integration ────────────────────────────────────────────────────
private fun pauseAtmo() {
runCatching {
val mgr = BackgroundMusicManager.getInstance(appContext)
if (mgr.player.isPlaying) {
mgr.pauseMusic()
Log.d(TAG, "Atmo pausiert für PTT")
}
}.onFailure { e ->
Log.w(TAG, "Konnte Atmo nicht pausieren: ${e.message}")
}
}
private fun restoreAtmo() {
runCatching {
val mgr = BackgroundMusicManager.getInstance(appContext)
val settings = mgr.currentSettings
if (settings.enabled) {
mgr.afterWaypointAudio()
Log.d(TAG, "Atmo nach PTT wiederhergestellt")
}
}.onFailure { e ->
Log.w(TAG, "Konnte Atmo nicht wiederherstellen: ${e.message}")
}
}
// ─── Lifecycle ────────────────────────────────────────────────────────────
/** Ressourcen freigeben. Danach nicht mehr nutzbar. */
fun release() {
if (_pttActive.value) stopPtt()
scope.cancel()
instance = null
}
}