d6b54d3247
Erweitert den Foreground-Service um den microphone-Type damit nicht nur TTS, sondern auch Wake-Word-Lauschen und aktive Aufnahmen weiterlaufen wenn die App im Hintergrund ist. Slot-System (backgroundAudio.ts): - 'tts' : ARIA spricht - 'rec' : Aufnahme laeuft - 'wake' : Wake-Word lauscht passiv (Ohr aktiv) Mehrere Slots koennen unabhaengig acquired/released werden, der Service laeuft solange mindestens einer aktiv ist. Notification-Text passt sich dynamisch an den hoechstprioren Slot an (tts > rec > wake). Wiring (ChatScreen): - onPlaybackStarted/Finished → 'tts' Slot - audioService.onStateChange (recording) → 'rec' Slot - wakeWordService.onStateChange (off→armed/conversing) → 'wake' Slot AndroidManifest: - foregroundServiceType="mediaPlayback|microphone" (Pflicht ab Android 14 fuer Background-Mic-Zugriff) - FOREGROUND_SERVICE_MICROPHONE Permission Doku: - issue.md Erledigt-Sektion in "Bugs / Fixes", "App Features" und "Infrastruktur" gesplittet - README: Background-Service-Beschreibung erweitert Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
109 lines
4.2 KiB
Kotlin
109 lines
4.2 KiB
Kotlin
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
|
|
const val EXTRA_REASON = "reason" // "tts" | "wake" | "rec" | ""
|
|
}
|
|
|
|
private var currentReason: String = ""
|
|
|
|
override fun onCreate() {
|
|
super.onCreate()
|
|
ensureNotificationChannel()
|
|
}
|
|
|
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
val reason = intent?.getStringExtra(EXTRA_REASON) ?: ""
|
|
currentReason = reason
|
|
Log.i(TAG, "Foreground-Service start/update (reason=$reason)")
|
|
try {
|
|
startForeground(NOTIFICATION_ID, buildNotification(reason))
|
|
} 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(reason: String): 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)
|
|
|
|
val (title, body) = when (reason) {
|
|
"tts" -> "ARIA spricht" to "Antwort wird abgespielt — antippen oeffnet die App"
|
|
"rec" -> "ARIA hoert zu" to "Sprachaufnahme laeuft — antippen oeffnet die App"
|
|
"wake" -> "ARIA bereit" to "Wake-Word lauscht passiv — antippen oeffnet die App"
|
|
else -> "ARIA aktiv" to "Hintergrund-Modus — antippen oeffnet die App"
|
|
}
|
|
|
|
return NotificationCompat.Builder(this, CHANNEL_ID)
|
|
.setContentTitle(title)
|
|
.setContentText(body)
|
|
.setSmallIcon(R.mipmap.ic_launcher)
|
|
.setContentIntent(pendingIntent)
|
|
.setOngoing(true)
|
|
.setShowWhen(false)
|
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
.build()
|
|
}
|
|
}
|