Initial release of GPS2Audio
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user