fix: dB-Range -85, Mute haert auch laufende TTS, VoIP-Anrufe + Bild-Bubble
Bug 1 — dB-Range erweitert: VAD_SILENCE_DB_MIN von -55 auf -85 dB. Damit hat Stefan einen weiten Regler-Spielraum wenn die adaptive Auto-Erkennung in seiner Umgebung nicht zuverlaessig greift. Bug 5 — Mute-Button stoppt laufende TTS nicht: audioService bekommt jetzt einen internen _muted-Flag. handlePcmChunk setzt silent automatisch wenn _muted true ist, playAudio kehrt frueh zurueck. Verhindert Race zwischen User-Klick auf Mute und einem TTS-Chunk der im selben JS-Tick ankommt (vorher: Ref-Update via useEffect erst nach dem Re-Render → Chunks "rutschten durch"). Plus ttsCanPlayRef wird im toggleMute-Handler synchron aktualisiert. Bug 4 — VoIP/Messenger-Anrufe erkennen: AudioFocusModule emittiert jetzt "AudioFocusChanged" Events mit type "loss"/"loss_transient"/"gain". WhatsApp/Signal/Discord/etc. requestn AudioFocus_GAIN_TRANSIENT_EXCLUSIVE wenn ein Anruf reinkommt — wir fangen das in phoneCall.ts ab und rufen halt + pauseForCall genau wie beim klassischen Anruf. Plus getMode() Polling-Fallback (alle 3s) weil GAIN nicht zuverlaessig kommt wenn wir den Focus selbst released haben — sobald AudioMode wieder NORMAL ist, resumeFromCall. Bug 6 — Bilder als "Strich": attachmentImage hatte width: '100%' in einer Bubble mit maxWidth: '80%' ohne explizite Parent-Breite → RN rendert auf 0px Breite. Neue ChatImage- Komponente nutzt Image.getSize um die echte aspectRatio zu messen + setzt sie dynamisch. Bubble passt sich dem Bild an. Bugs 2 (lange Texte mid-cutoff) + 3 (Spotify resumed) — brauchen ADB-Logs. ADB-WLAN ueber 192.168.177.22:5555 schlaegt fehl (refused) — bei Android 11+ braucht's Wireless-Debugging-Pairing-Code. Stefan kann den nennen sobald er soweit ist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,26 +5,71 @@ import android.media.AudioAttributes
|
||||
import android.media.AudioFocusRequest
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.Promise
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||
import com.facebook.react.bridge.ReactMethod
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule
|
||||
|
||||
/**
|
||||
* Steuert Audio-Focus fuer Ducking/Muten anderer Apps.
|
||||
* Steuert Audio-Focus fuer Ducking/Muten anderer Apps + emittiert Loss-Events
|
||||
* an JS damit ARIA bei VoIP-Anrufen (WhatsApp/Signal/Discord/...) aufhoert
|
||||
* zu sprechen — diese Anrufe gehen nicht ueber TelephonyManager, sondern
|
||||
* requestn AudioFocus_GAIN_TRANSIENT_EXCLUSIVE was wir hier mitbekommen.
|
||||
*
|
||||
* - requestDuck() → andere Apps werden leiser (ARIA spricht TTS)
|
||||
* - requestExclusive() → andere Apps werden pausiert (Mikrofon-Aufnahme)
|
||||
* - release() → Focus abgeben, andere Apps duerfen wieder
|
||||
*
|
||||
* Events:
|
||||
* - "AudioFocusChanged" mit type:
|
||||
* "loss" — endgueltiger Verlust (Anruf, andere App permanent)
|
||||
* "loss_transient" — vorruebergehender Verlust (kurze Unterbrechung)
|
||||
* "gain" — Fokus zurueck
|
||||
*/
|
||||
class AudioFocusModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
||||
override fun getName() = "AudioFocus"
|
||||
|
||||
companion object { private const val TAG = "AudioFocus" }
|
||||
|
||||
private var currentRequest: AudioFocusRequest? = null
|
||||
|
||||
private fun audioManager(): AudioManager? =
|
||||
reactApplicationContext.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
||||
|
||||
private fun emitFocusChange(type: String) {
|
||||
try {
|
||||
val params = Arguments.createMap().apply { putString("type", type) }
|
||||
reactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
||||
.emit("AudioFocusChanged", params)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "emit failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private val focusListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
||||
when (focusChange) {
|
||||
AudioManager.AUDIOFOCUS_LOSS -> {
|
||||
Log.i(TAG, "AUDIOFOCUS_LOSS (z.B. Anruf, anderer Player permanent)")
|
||||
emitFocusChange("loss")
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||
Log.i(TAG, "AUDIOFOCUS_LOSS_TRANSIENT (kurze Unterbrechung)")
|
||||
emitFocusChange("loss_transient")
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||
// Notification-Sound o.ae. — wir ignorieren das, ARIA macht weiter
|
||||
Log.d(TAG, "AUDIOFOCUS_LOSS_CAN_DUCK ignoriert")
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||
Log.i(TAG, "AUDIOFOCUS_GAIN")
|
||||
emitFocusChange("gain")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestFocus(durationHint: Int, usage: Int, promise: Promise) {
|
||||
val am = audioManager()
|
||||
if (am == null) {
|
||||
@@ -41,13 +86,13 @@ class AudioFocusModule(reactContext: ReactApplicationContext) : ReactContextBase
|
||||
.build()
|
||||
val req = AudioFocusRequest.Builder(durationHint)
|
||||
.setAudioAttributes(attrs)
|
||||
.setOnAudioFocusChangeListener { /* kein Callback noetig */ }
|
||||
.setOnAudioFocusChangeListener(focusListener)
|
||||
.build()
|
||||
currentRequest = req
|
||||
am.requestAudioFocus(req)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, durationHint)
|
||||
am.requestAudioFocus(focusListener, AudioManager.STREAM_MUSIC, durationHint)
|
||||
}
|
||||
|
||||
promise.resolve(result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
|
||||
@@ -92,8 +137,24 @@ class AudioFocusModule(reactContext: ReactApplicationContext) : ReactContextBase
|
||||
currentRequest?.let { am.abandonAudioFocusRequest(it) }
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
am.abandonAudioFocus(null)
|
||||
am.abandonAudioFocus(focusListener)
|
||||
}
|
||||
currentRequest = null
|
||||
}
|
||||
|
||||
/** Aktueller Audio-Mode: NORMAL=0, IN_CALL=2, IN_COMMUNICATION=3, CALL_SCREENING=4.
|
||||
* IN_COMMUNICATION ist der typische VoIP-Anruf-Mode (WhatsApp, Signal, etc.) —
|
||||
* kann gepollt werden um zu erkennen wann der Anruf vorbei ist (zurueck NORMAL). */
|
||||
@ReactMethod
|
||||
fun getMode(promise: Promise) {
|
||||
val am = audioManager()
|
||||
if (am == null) {
|
||||
promise.resolve(0)
|
||||
return
|
||||
}
|
||||
promise.resolve(am.mode)
|
||||
}
|
||||
|
||||
@ReactMethod fun addListener(eventName: String) {}
|
||||
@ReactMethod fun removeListeners(count: Int) {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user