feat(audio): Foreground-Service haelt TTS am Leben bei minimierter App

ARIAs Antwort wird jetzt auch dann fertig vorgelesen wenn der User die
App im Hintergrund schickt. Vorher hat Android den Prozess kurz nach
dem Minimieren eingefroren — TTS verstummte mitten im Satz.

Native:
- AriaPlaybackService.kt: Service mit foregroundServiceType=mediaPlayback,
  zeigt persistente Notification "ARIA spricht — antippen oeffnet die App"
  (channel low-priority, ongoing, tap → MainActivity)
- BackgroundAudioModule.kt: RN-Bridge mit start()/stop()
- AndroidManifest: FOREGROUND_SERVICE + FOREGROUND_SERVICE_MEDIA_PLAYBACK
  + POST_NOTIFICATIONS Permissions, Service deklariert

JS:
- backgroundAudio.ts: idempotenter Wrapper (active-Flag verhindert
  doppelte start/stop calls)
- ChatScreen onPlaybackStarted → startBackgroundAudio
- ChatScreen onPlaybackFinished → stopBackgroundAudio
- audio.ts stopPlayback ruft auch stopBackgroundAudio damit die
  Notification bei Cancel/Barge-In/Anruf nicht haengen bleibt

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 23:37:46 +02:00
parent f682aad4ff
commit ead28cf09a
10 changed files with 237 additions and 1 deletions
@@ -0,0 +1,96 @@
package com.ariacockpit
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
/**
* Foreground-Service der den App-Prozess waehrend TTS-Wiedergabe am Leben
* haelt — Android killt sonst den Prozess sobald die App im Hintergrund ist
* und ARIA verstummt mitten im Satz.
*
* Notification ist persistent (ongoing) waehrend der Service laeuft.
* Tap auf die Notification bringt MainActivity zurueck nach vorne.
*
* foregroundServiceType="mediaPlayback" ist Pflicht ab Android 14, sonst
* wirft startForeground() eine SecurityException.
*/
class AriaPlaybackService : Service() {
companion object {
private const val TAG = "AriaPlaybackService"
private const val CHANNEL_ID = "aria_playback"
private const val NOTIFICATION_ID = 1042
}
override fun onCreate() {
super.onCreate()
ensureNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(TAG, "Foreground-Service gestartet")
try {
startForeground(NOTIFICATION_ID, buildNotification())
} catch (e: Exception) {
Log.e(TAG, "startForeground fehlgeschlagen", e)
stopSelf()
}
// START_NOT_STICKY: wenn Android den Service killt, NICHT automatisch
// wieder starten — die App entscheidet wann der Service noetig ist.
return START_NOT_STICKY
}
override fun onDestroy() {
Log.i(TAG, "Foreground-Service gestoppt")
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? = null
private fun ensureNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val nm = getSystemService(NotificationManager::class.java) ?: return
if (nm.getNotificationChannel(CHANNEL_ID) == null) {
val channel = NotificationChannel(
CHANNEL_ID,
"ARIA Audio-Wiedergabe",
NotificationManager.IMPORTANCE_LOW,
).apply {
description = "Notification waehrend ARIA spricht (haelt die App im Hintergrund am Leben)"
setShowBadge(false)
}
nm.createNotificationChannel(channel)
}
}
}
private fun buildNotification(): Notification {
val launchIntent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
else
PendingIntent.FLAG_UPDATE_CURRENT
val pendingIntent = PendingIntent.getActivity(this, 0, launchIntent, pendingFlags)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("ARIA spricht")
.setContentText("Antwort wird abgespielt — antippen oeffnet die App")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setShowWhen(false)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.build()
}
}