Push-Benachrichtigungen auf Android
Übersicht
Für Android-VoIP-Push-Benachrichtigungen müssen zwei Systemintegrationen zusammenwirken:
- Firebase Cloud Messaging (FCM) — übermittelt Datenmeldungen mit hoher Priorität, um die App zu aktivieren, selbst wenn sie beendet wurde
- Jetpack Core-Telecom (
CallsManager) — meldet Ihren Anruf beim Android-Telekom-Stack zur nativen Anrufbearbeitung an: Systemaufruf-Benutzeroberfläche, Audio-Routing und Priorität bei der Ausführung im Vordergrund
Zuständigkeiten der Komponenten
| Komponente | Rolle |
|---|---|
FirebaseMessagingService |
Empfängt FCM-Nachrichten und leitet diese an Ihren Call-Manager weiter |
VoiceClient |
Vonage SDK – Sitzungsverwaltung, Anrufsignalisierung, Medien |
CallsManager |
Jetpack Core-Telecom – registriert den Anruf über das Betriebssystem mittels addCall |
CallControlScope |
Der Griff addCall Rückmeldungen – Steuerung (Antworten / Halten / Trennen / Audio weiterleiten) und den OS-Aufruf beobachten |
Gestartet Service |
Ist Eigentümer von CallStyle Benachrichtigung, übernimmt die Registrierung und hält die Verbindung aufrecht, wenn die App in den Hintergrund wechselt oder weggewischt wird |
In dieser Anleitung werden CallsManager (der androidx.core.telecom (Jetpack-Bibliothek), Googles empfohlene API für VoIP-Anrufe und was die Beispiel-App Verwendungszwecke. Das alte android.telecom.ConnectionService funktioniert immer noch — siehe Altes System: ConnectionService warum CallsManager wird bevorzugt.
Wesentlicher Unterschied zu iOS
Im Gegensatz zu iOS PushKit verfügt Android FCM über keine feste Frist zum Anzeigen der Anruf-Benutzeroberfläche. Sie können asynchrone Vorgänge (Wiederherstellung der Sitzung, Aktualisierung des Tokens) sicher ausführen, bevor der eingehende Anruf angezeigt wird. Dadurch werden die Race-Conditions vermieden, die bei iOS-Implementierungen häufig auftreten.
Projekteinrichtung
Gradle-Abhängigkeiten
dependencies {
// Firebase
implementation(platform("com.google.firebase:firebase-bom:33.0.0"))
implementation("com.google.firebase:firebase-messaging")
// Jetpack Core-Telecom — CallsManager
implementation("androidx.core:core-telecom:1.0.1")
// Vonage Client SDK
implementation("com.vonage:client-sdk-voice:latest.release")
}
plugins {
id("com.google.gms.google-services") version "4.4.2" apply false
}
plugins {
id("com.google.gms.google-services")
}
Google-Services.json
Aus der Firebase-Konsole herunterladen und in folgenden Ordner verschieben: app/google-services.json. Füge es hinzu zu .gitignore — Es enthält Ihre Anmeldedaten für das Firebase-Projekt.
Manifest-Einträge
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<application ...>
<!-- FCM Service -->
<service
android:name=".services.PushNotificationService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<!-- Started (NOT foreground) service: owns the CallStyle notification,
hosts the CallsManager registration, and keeps the call alive when
backgrounded/swiped. Core-Telecom provides the foreground service
itself, so you do NOT declare one of your own. -->
<service
android:name=".telecom.CallService"
android:exported="false" />
<!-- Delivers the CallStyle notification's Decline / Hang up actions. -->
<receiver
android:name=".telecom.CallActionReceiver"
android:exported="false" />
<!-- Incoming call activity (shows over lock screen) -->
<activity
android:name=".ui.CallActivity"
android:showOnLockScreen="true"
android:turnScreenOn="true"
android:showWhenLocked="true"
android:exported="false" />
</application>
</manifest>
Laufzeitberechtigungen
Fordern Sie diese zur Laufzeit an, bevor Sie einen Aufruf versuchen:
private val requiredPermissions = arrayOf(
Manifest.permission.RECORD_AUDIO,
Manifest.permission.READ_PHONE_STATE,
)
// For API 33+
private val api33Permissions = arrayOf(
Manifest.permission.POST_NOTIFICATIONS,
)
MANAGE_OWN_CALLS und CALL_PHONE sind normale Berechtigungen – sie werden bei der Installation automatisch erteilt und erfordern keine Dialoge zur Laufzeit.
Warnung: RECORD_AUDIO muss vor jedem Anrufversuch gewährt werden. Wird dies verweigert, stellt das SDK zwar die Verbindung her, der Gesprächspartner hört jedoch nichts.
SDK-Initialisierung
Initialisieren VoiceClient einmal in deinem Application.onCreate() oder ein Singleton. Erstellen Sie es niemals neu – dadurch werden alle Listener gelöscht und der Sitzungsstatus geht verloren.
class CoreContext(context: Context) {
// Immutable snapshot of the in-progress call — the single source of truth
// for the UI and the call notification (replaces a Connection subclass).
val activeCall = MutableStateFlow<ActiveCall?>(null)
var pushToken: String? = null
var lastRegisteredPushToken: String? = null
var deviceId: String? = null
var authToken: String? = null
var refreshToken: String? = null
}
class App : Application() {
lateinit var voiceClientManager: VoiceClientManager
private set
lateinit var coreContext: CoreContext
private set
override fun onCreate() {
super.onCreate()
instance = this
coreContext = CoreContext(this)
val config = VGClientInitConfig(LoggingLevel.Info)
val client = VoiceClient(this, config)
voiceClientManager = VoiceClientManager(client, this)
}
companion object {
lateinit var instance: App
private set
}
}
FCM-Integration
FirebaseMessagingService
Halten Sie den Service schlank – delegieren Sie die gesamte Logik an Ihre Manager-Klasse.
class PushNotificationService : FirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
// Delegate immediately — keep the service class thin
App.instance.voiceClientManager.processVoipPush(remoteMessage)
}
override fun onNewToken(token: String) {
super.onNewToken(token)
// Store and re-register if session is active
App.instance.coreContext.pushToken = token
App.instance.voiceClientManager.onPushTokenRefreshed(token)
}
companion object {
fun requestToken(callback: (String) -> Unit) {
FirebaseMessaging.getInstance().token
.addOnSuccessListener { callback(it) }
.addOnFailureListener { println("FCM token request failed: $it") }
}
}
}
Umgang mit Token
FCM kann Token jederzeit austauschen. onNewToken ist die nur Zuverlässige Benachrichtigung. Wenn sie ausgelöst wird:
- Speichere das neue Token lokal
- Wenn bereits eine Sitzung aktiv ist, rufen Sie Folgendes auf:
registerDevicePushTokensofort
Ein abgelaufenes Token bedeutet, dass Vonage das Gerät nicht erreichen kann – alle zukünftigen Push-Benachrichtigungen können nicht zugestellt werden, ohne dass eine entsprechende Meldung angezeigt wird.
Registrierung des Tokens bei Vonage
client.registerDevicePushToken(token) { error, deviceId ->
if (error != null) {
println("Failed to register push token: $error")
return@registerDevicePushToken
}
println("Push token registered successfully, deviceId: $deviceId")
}
client.registerDevicePushToken(token, (error, deviceId) -> {
if (error != null) {
System.out.println("Failed to register push token: " + error);
return null;
}
System.out.println("Push token registered successfully, deviceId: " + deviceId);
return null;
});
Registrieren Sie das FCM-Token bei client.registerDevicePushToken(token) nachdem eine Sitzung erstellt wurde. Der Server generiert eine eindeutige deviceId für jede Registrierung; vermeiden Sie daher den Aufruf dieser Funktion, wenn sich das Token nicht geändert hat – andernfalls sammeln sich doppelte Registrierungen an, was zu mehreren Push-Benachrichtigungen pro Aufruf führt.
private fun registerDevicePushTokenIfNeeded() {
val registerCallback: (String) -> Unit = { token ->
// Skip registration if the token is the same as the one we last registered
if (token == coreContext.lastRegisteredPushToken && coreContext.deviceId != null) {
println("Push token unchanged — skipping re-registration")
return@registerCallback
}
client.registerDevicePushToken(token) { err, deviceId ->
err?.let {
println("Failed to register push token: $err")
} ?: deviceId?.let {
coreContext.deviceId = deviceId
coreContext.lastRegisteredPushToken = token
println("Push token registered, deviceId: $deviceId")
}
}
}
coreContext.pushToken?.let { registerCallback(it) }
?: PushNotificationService.requestToken { registerCallback(it) }
}
fun unregisterDeviceToken(completion: () -> Unit) {
val deviceId = coreContext.deviceId ?: run { completion(); return }
client.unregisterDevicePushToken(deviceId) { error ->
error?.let { println("Unregister error: $it") }
coreContext.deviceId = null
completion()
}
}
Nachrichten mit hoher Priorität und Doze-Modus Das Backend von Vonage versendet FCM-Nachrichten um hohe Priorität. Nachrichten mit hoher Priorität umgehen den Android-Doze-Modus und wecken die App sofort auf. Sie müssen dies in Ihrer App nicht konfigurieren – es handelt sich um eine serverseitige Einstellung in der Vonage-Plattform.
Sitzungsmanagement
Eine Vonage-Sitzung (client.createSession(jwt)) muss aktiv sein, bevor das SDK Anrufeinladungen verarbeiten oder Anrufaktionen ausführen kann. Wenn eine Push-Benachrichtigung eintrifft und die App beendet wurde oder die Sitzung abgelaufen ist, muss zunächst eine Wiederherstellung erfolgen.
Im Gegensatz zu iOS – ist das Verkettung auf Android sicher
Android FCM kennt keine Frist im Stil von PushKit für die Anzeige der System-Benutzeroberfläche. Sie können getrost auf die Wiederherstellung der Sitzung warten, bevor Sie den Aufruf ausführen. client.processPushCallInvite. Dadurch wird die Race Condition vermieden, bei der der Benutzer antwortet, bevor eine Sitzung bereit ist.
Empfohlenes Vorgehen: Stellen Sie zunächst die Sitzung wieder her. Sobald bestätigt ist, dass sie aktiv ist, rufen Sie processPushCallInvite. Die Sitzung ist garantiert vorhanden, wenn answer() / reject() werden aufgerufen.
Zu vermeiden: Die sofortige Verarbeitung des Pushs und die gleichzeitige Wiederherstellung der Sitzung. Der Benutzer könnte antworten, bevor die Sitzung bereit ist, was dazu führt, dass client.answer() stillschweigend zu scheitern.
Erstellung von Sitzungen
do {
let sessionId = try await client.createSession(jwt)
print("Session created: \(sessionId)")
} catch {
print("Failed to create session: \(error.localizedDescription)")
}
[client createSession:jwt callback:^(NSError * _Nullable error, NSString * _Nullable sessionId) {
if (error != nil || sessionId == nil) {
NSLog(@"Failed to create session: %@", error);
return;
}
NSLog(@"Session created: %@", sessionId);
}];
fun login(
token: String,
isUserInitiated: Boolean = true,
onError: ((Exception) -> Unit)? = null,
onSuccess: ((String) -> Unit)? = null
) {
if (isUserInitiated) {
// On user-initiated login: clean up any stale device registration first,
// then create session and register the push token (always needed after unregister)
unregisterExistingDeviceIfNeeded {
createSession(token, registerPush = true, onError, onSuccess)
}
} else {
// Session restore: skip cleanup (same user, just reconnecting).
// Only re-register push token if the FCM token has changed since last registration.
createSession(token, registerPush = false, onError, onSuccess)
}
}
private fun createSession(
token: String,
registerPush: Boolean = true,
onError: ((Exception) -> Unit)? = null,
onSuccess: ((String) -> Unit)? = null
) {
client.createSession(token) { error, sessionId ->
sessionId?.let { sid ->
coreContext.authToken = token // persist for future restoration
if (registerPush) {
registerDevicePushTokenIfNeeded()
}
onSuccess?.invoke(sid)
_sessionId.value = sid
} ?: error?.let { onError?.invoke(it) }
}
}
Prioritätskette für die Wiederherstellung von Sitzungen
| Priorität | Zustand | Aktion | Typische Latenz |
|---|---|---|---|
| 1. | Es besteht eine aktive Sitzung (sessionId != null) |
Sofort zurückkehren | 0 ms |
| 2. | Gespeichertes JWT verfügbar (authToken) |
client.createSession(storedJWT) |
100–400 ms |
| 3. | JWT abgelaufen, Refresh-Token verfügbar | Endpunkt aktualisieren -> neues JWT -> createSession |
300–1000 ms |
| 4. | Keine Anmeldedaten | Rückkehr null — Der Push kann nicht verarbeitet werden |
0 ms |
private fun restoreSessionIfNeeded(completion: (String?) -> Unit) {
_sessionId.value?.let { completion(it); return } // already active
coreContext.authToken?.let { token ->
restoreSessionWithToken(token, completion)
} ?: coreContext.refreshToken?.let { refresh ->
restoreSessionWithRefreshToken(refresh, completion)
} ?: completion(null) // no credentials
}
Das JWT bei der Anmeldung speichern. Wenn createSession Da bei jedem eingehenden Push ein Live-Backend-Aufruf erforderlich ist, um ein JWT abzurufen, fügen Sie einen zusätzlichen Netzwerk-Hop hinzu, bevor die Sitzung bereit ist. Bei langsamen Verbindungen erhöht dies die Latenz bei der Wiederherstellung und beeinträchtigt die Zuverlässigkeit der Antwort auf den Aufruf.
Verhindern der Token-Anhäufung bei erneuter Anmeldung
Bei einer neuen, vom Benutzer initiierten Anmeldung muss das zuvor gespeicherte Gerät immer erst abgemeldet werden, bevor eine neue Sitzung erstellt wird. Wird dieser Schritt übersprungen, sammelt sich bei Vonage eine Vielzahl von Geräteregistrierungen an, was zu doppelten Push-Benachrichtigungen für denselben Anruf führt.
private fun unregisterExistingDeviceIfNeeded(completion: () -> Unit) {
val existingDeviceId = coreContext.deviceId ?: run { completion(); return }
val authToken = coreContext.authToken ?: run {
coreContext.deviceId = null; completion(); return
}
// Create a temporary session just to perform the unregistration
client.createSession(authToken) { _, sessionId ->
sessionId ?: run { coreContext.deviceId = null; completion(); return@createSession }
unregisterDeviceToken {
client.deleteSession { completion() }
}
}
}
Verarbeitung von Push-Nutzdaten
Der gesamte Weg von der FCM-Übermittlung bis zur Benutzeroberfläche für eingehende Anrufe im System:
Umsetzung
fun processVoipPush(remoteMessage: RemoteMessage) {
// Step 1: ensure a session exists before touching the push payload
restoreSessionIfNeeded { sessionId ->
sessionId ?: run {
println("Session restoration failed — cannot process push")
return@restoreSessionIfNeeded
}
// Step 2: now safe to process — session is guaranteed active
processIncomingPush(remoteMessage)
}
}
fun processIncomingPush(remoteMessage: RemoteMessage) {
val dataString = remoteMessage.data.toString()
val type = VoiceClient.getPushNotificationType(dataString)
if (type == PushType.INCOMING_CALL) {
// Triggers setCallInviteListener callback
client.processPushCallInvite(dataString)
}
}
Bearbeitung der Anrufeinladung
Wenn das SDK ausgelöst wird setCallInviteListener, speichern Sie den Aufruf als App-Zustand und starten Sie die CallService — wodurch der Anruf bei Telecom registriert wird (siehe Telekommunikationsintegration (warum dies nämlich vom Dienst übernommen werden muss).
// In setClientListeners()
client.setCallInviteListener { callId, from, type ->
// One call at a time — ignore a second invite
coreContext.activeCall.value?.let { return@setCallInviteListener }
coreContext.setActiveCall(
ActiveCall(callId = callId, displayName = from, isIncoming = true, state = CallState.RINGING)
)
CallService.register(context) // starts the service, which runs callsManager.addCall
}
Telekommunikationsintegration mit CallsManager
Die Registrierung des Anrufs beim Betriebssystem erfolgt über Core-Telecom's CallsManager. Sie registrieren Ihre App einmal und rufen dann addCall pro Anruf: Es übermittelt Telecom eine Beschreibung des Anrufs (CallAttributesCompat) und wird für die gesamte Laufzeit des Aufrufs ausgesetzt, wodurch Sie ein CallControlScope um es zu bedienen (Annehmen / Halten / Auflegen / Audio weiterleiten) und zu überwachen (Stummschaltungsstatus, Audio-Endpunkte).
Die komprimierte Form (siehe CallManager.kt (für den vollständigen Wrapper):
private val callsManager = CallsManager(context).apply {
registerAppWithTelecom(CallsManager.CAPABILITY_BASELINE)
}
suspend fun registerCall(displayName: String, isIncoming: Boolean) {
val attributes = CallAttributesCompat(
displayName = displayName,
address = Uri.fromParts("sip", displayName, null),
direction = if (isIncoming) CallAttributesCompat.DIRECTION_INCOMING
else CallAttributesCompat.DIRECTION_OUTGOING,
)
callsManager.addCall(
attributes,
onAnswer = { onTelecomAnswer() }, // answered from system UI / watch / Auto
onDisconnect = { onTelecomDisconnect() }, // ended from system UI / watch / Auto
onSetActive = {},
onSetInactive = {},
) {
// CallControlScope — drive + observe the OS call for its whole lifetime
launch { answer(CallAttributesCompat.CALL_TYPE_AUDIO_CALL) } // or setActive() / disconnect(...)
launch { currentCallEndpoint.collect { /* active audio route */ } }
launch { availableEndpoints.collect { /* route options */ } }
launch { isMuted.collect { muted -> onMuteChanged(muted) } }
}
}
Die Registrierung in einem bereits gestarteten Dienst hosten
Rufen Sie an. addCall von einer in Betrieb genommen Gültigkeitsbereich, kein App-Singleton-Gültigkeitsbereich. Der gestartete Dienst verleiht dem Prozess die Berechtigung für Telecom, behalten der Aufruf; ein Aufruf, der von einem im Kaltstart-Modus gestarteten Hintergrundprozess ohne Service-Anker hinzugefügt wurde, wird hinzugefügt und anschließend sofort gelöscht (CallException.ERROR_CALL_IS_NOT_BEING_TRACKED), woraufhin jeder Kontrollvorgang fehlschlägt. Aus diesem Grund wird der Einladungshandler gestartet CallService, und der Dienst führt die Registrierung durch. Siehe CallService.kt.
Die Ursache für die Unterbrechung festklemmen
Core-Telecoms disconnect akzeptiert nur LOCAL, REMOTE, MISSEDund REJECTED — sonstige DisconnectCause bringt die Sitzung zum Absturz und reißt sie auseinander. Karte an der Grenze:
private val VALID_CAUSES = setOf(
DisconnectCause.LOCAL, DisconnectCause.REMOTE,
DisconnectCause.MISSED, DisconnectCause.REJECTED,
)
fun disconnect(cause: Int) {
val safe = if (cause in VALID_CAUSES) cause else DisconnectCause.LOCAL
onControl { disconnect(DisconnectCause(safe)) }
}
Eine einzige Quelle der Wahrheit: ActiveCall
Anstatt den App-Status an einen Connection Unterklasse, eine unveränderliche ActiveCall das von der Benutzeroberfläche und den Benachrichtigungen beachtet wird; sowohl das SDK als auch die Telecom-Callbacks aktualisieren es. Das CallManager Der Wrapper ist eine „Fire-and-Forget“-Integration in das Betriebssystem und schreibt den Aufrufstatus niemals zurück. Siehe ActiveCall.kt.
Ausführung im Vordergrund und die „CallStyle“-Benachrichtigung
Das tust du nicht Erstellen Sie einen Mikrofon-Vordergrunddienst. Während ein Anruf bei CallsManager und a CallStyle Sobald die Benachrichtigung veröffentlicht wird, behandelt Core-Telecom den Anruf als einen phoneCall|microphone Ein Vordergrunddienst für Sie – dieser sorgt dafür, dass Live-Mikrofon-Audio und die Ausführung im Vordergrund auch dann Priorität haben, wenn der Dienst beendet oder in den Hintergrund verschoben wurde. Ein manuell gestarteter Vordergrunddienst ist überflüssig und kann zu Konflikten führen.
Die CallStyle Die Benachrichtigung ist daher obligatorisch und keine reine Formsache. Zwei Dinge, die beachtet werden müssen (siehe CallNotifier.kt):
- Vollbild-Intent für eingehende Anrufe Aktiviert die „Wakes Your Call“-Funktion über die Tastensperre, wenn auf einem gesperrten Gerät eine Benachrichtigung eingeht.
- Die Antwort muss eine Aktivität sein.
PendingIntent, keine Übertragung, die dann aufruftstartActivity— Letzteres ist ein Benachrichtigungs-Trampolin, das seit Android 12 verboten ist. „Ablehnen“ / „Auflegen“ sind Broadcasts an einBroadcastReceiver.
Pinnen Sie den Timer der laufenden Benachrichtigung an die tatsächliche Verbindungszeit und veröffentlichen Sie die Benachrichtigung erst dann erneut, wenn sich ihre Eingaben ändern, damit das Umschalten zwischen Stummschaltung und Halten den Timer für die Gesprächsdauer nicht zurücksetzt.
SDK-Listener
Alle Listener einmalig direkt nach der Initialisierung einrichten VoiceClient, bevor eine Sitzung erstellt wird. Ordnen Sie jedes SDK-Ereignis dem ActiveCall Festlegen und jedes Endgeräteereignis über einen einzigen Aufräumpfad leiten, sodass die Telekommunikationsverbindung stets getrennt und die Benachrichtigung stets abgebrochen wird. (Vollständiger Satz in VoiceClientManager.kt.)
Aktiv / Wiederverbindung
// Outbound leg answered, or media (re)connected -> active
client.setOnLegStatusUpdate { callId, _, status ->
if (currentIfMatches(callId) != null && status == LegStatus.answered) markActive()
}
client.setOnCallMediaReconnectingListener { callId ->
if (currentIfMatches(callId) != null) updateActiveCall { copy(state = CallState.RECONNECTING) }
}
client.setOnCallMediaReconnectionListener { callId ->
if (currentIfMatches(callId) != null) markActive()
}
markActive() Sätze ActiveCall.state = ACTIVE (die Verbindungszeit für den Benachrichtigungs-Chronometer festlegt) und ruft telecom.setActive().
Endereignisse
Ordne den SDK-Grund einem DisconnectCause, dann führe eine aus cleanUp Pfad (der telecom.disconnect(cause), zeigt kurz den getrennten Zustand an und löscht ihn anschließend ActiveCall).
client.setOnCallHangupListener { callId, _, reason ->
if (currentIfMatches(callId) != null) {
val cause = when (reason) {
HangupReason.remoteReject -> DisconnectCause.REJECTED
HangupReason.remoteHangup -> DisconnectCause.REMOTE
HangupReason.localHangup -> DisconnectCause.LOCAL
HangupReason.mediaTimeout -> DisconnectCause.BUSY
HangupReason.remoteNoAnswerTimeout -> DisconnectCause.CANCELED
}
cleanUp(cause)
}
}
client.setCallInviteCancelListener { callId, reason ->
if (currentIfMatches(callId) != null) {
val cause = when (reason) {
VoiceInviteCancelReason.AnsweredElsewhere -> DisconnectCause.ANSWERED_ELSEWHERE
VoiceInviteCancelReason.RejectedElsewhere -> DisconnectCause.REJECTED
VoiceInviteCancelReason.RemoteCancel -> DisconnectCause.CANCELED
VoiceInviteCancelReason.RemoteTimeout -> DisconnectCause.MISSED
}
cleanUp(cause)
}
}
client.setOnCallMediaDisconnectListener { callId, _ ->
if (currentIfMatches(callId) != null) cleanUp(DisconnectCause.ERROR)
}
Weil cleanUp Läufe telecom.disconnectdie DisconnectCause Die oben genannten Codes sind auf die vier von Core-Telecom akzeptierten Werte begrenzt (siehe Die Ursache für die Unterbrechung festklemmen).
Ausgehende Anrufe
Einfacher als mit ConnectionService — nein placeCall / onCreateOutgoingConnection Tanz und kein OEM-Fallback-Timer. Starten Sie den SDK-Aufruf, zeichnen Sie ihn als aktiven Aufruf auf und starten Sie den Dienst, um ihn bei Telecom als DIRECTION_OUTGOING:
fun startOutboundCall(callContext: Map<String, String>? = null) {
client.serverCall(callContext) { err, callId ->
callId ?: run { err?.let { println("Outbound call error: $it") }; return@serverCall }
coreContext.setActiveCall(
ActiveCall(callId = callId, displayName = callee, isIncoming = false, state = CallState.DIALING)
)
CallService.register(context) // registers the outgoing call with Telecom
}
}
Fehlerbehandlung
Das SDK wird ausgelöst setSessionErrorListener wenn die WebSocket-Verbindung während einer aktiven Sitzung unterbrochen wird. Versuche automatisch, die Verbindung wiederherzustellen, und wähle dabei anhand des Fehlergrunds die passende Strategie aus.
client.setSessionErrorListener { reason ->
when (reason) {
SessionErrorReason.TokenExpired ->
// Stored JWT is invalid — skip it, go straight to refresh token
attemptSessionRestoration(skipAuthToken = true)
SessionErrorReason.TransportClosed,
SessionErrorReason.PingTimeout ->
// Transient network issue — try stored JWT first
attemptSessionRestoration(skipAuthToken = false)
}
}
private fun attemptSessionRestoration(skipAuthToken: Boolean = false) {
val handleFailure = {
clearSession()
// Prompt user to log in again
}
val fallbackToRefresh = {
coreContext.refreshToken?.let { token ->
restoreSessionWithRefreshToken(token) { it ?: handleFailure() }
} ?: handleFailure()
}
if (!skipAuthToken) {
coreContext.authToken?.let { token ->
restoreSessionWithToken(token) { it ?: fallbackToRefresh() }
} ?: fallbackToRefresh()
} else {
fallbackToRefresh()
}
}
Häufige Fehlerszenarien
| Fehler | Ursache | Erholung |
|---|---|---|
TokenExpired |
JWT-TTL überschritten | Verwenden Sie das Refresh-Token, um ein neues JWT zu erhalten |
TransportClosed |
Netzwerkunterbrechung | Erneut versuchen mit gespeichertem JWT |
PingTimeout |
Server nicht erreichbar | Erneuter Versuch mit exponentiellem Backoff |
registerDevicePushToken scheitert |
Sitzung nicht aktiv | Sicherstellen createSession vor der Registrierung abgeschlossen |
processPushCallInvite still |
Keine Sitzung | Stellen Sie immer zuerst die Sitzung wieder her |
answer() scheitert |
Die Sitzung ist zwischen dem Push und der Antwort abgelaufen | Sitzung erneut wiederherstellen, dann antworten |
Altes System: ConnectionService
Vor Jetpack Core-Telecom wurden selbstverwaltete VoIP-Anrufe genutzt android.telecom.ConnectionService direkt: Sie haben sich bei einem PhoneAccount, mit Unterklassen ConnectionService, zurückgegeben Connection Objekte aus onCreateIncomingConnection / onCreateOutgoingConnection, und hast dein eigenes microphone Vordergrunddienst. Er funktioniert nach wie vor und ist nicht veraltet, sodass bestehende Integrationen nicht dringend migriert werden müssen.
Für neu Integrationen, CallsManager wird bevorzugt:
CallsManager (Core-Telecom) |
ConnectionService (veraltet) |
|
|---|---|---|
| Android 14 FGS-Typ + Berechtigung | Wir kümmern uns darum | Sie melden das FGS selbst an und nehmen den Betrieb auf |
| Dienst im Vordergrund | Zur Verfügung gestellt von der Bibliothek | Du schreibst und verwaltest es |
| Plattformübergreifend (Wear / Auto / Bluetooth) | Eingebaut | Handbuch |
| API-Form | Eins addCall + a CallControlScope |
PhoneAccount + Connection Unterklasse + System-Callbacks |
| Eigenheiten bei ausgehenden Anrufen bei OEMs | Wird von der Bibliothek übernommen | Manuelle Fallback-Timer (Samsung/Xiaomi) |
| Min-API | 26 | 23 |
Der Punkt zu Android 14 ist der entscheidende Faktor: Ab API 34 prüft das Betriebssystem den Typ des Vordergrunddienstes und die entsprechende Berechtigung (z. B. RECORD_AUDIO) zum Zeitpunkt des Starts des Dienstes. CallsManager erfüllt diese Anforderung für dich; eine handgerollte ConnectionService Die Integration muss absolut korrekt funktionieren, sonst schlagen Anrufe auf modernen Geräten fehl.
Siehe die ConnectionService-Referenz wenn Sie eine bestehende Integration pflegen.
Fallstricke und Anti-Muster
1. Verarbeitung von Push-Nachrichten vor der Wiederherstellung der Sitzung
Nicht tun:
Rufen Sie an. processPushCallInvite unmittelbar nach Erhalt der Push-Benachrichtigung, ohne zu überprüfen, ob eine Sitzung besteht. Der Anruf-Invite-Listener wird möglicherweise nicht ausgelöst, was dazu führt, dass der Anruf verpasst wird und keine Benutzeroberfläche angezeigt wird.
Tun:
Wiederherstellung der Kettensitzung vor der Verarbeitung: restoreSessionIfNeeded { processPushCallInvite(data) }.
2. Bei jedem Push ein JWT vom Backend abrufen
Nicht tun:
Vorher muss ein Live-Backend-Aufruf erfolgen createSession bei jedem Push. Dies verdoppelt die Latenz bei Cold-Start-Pushes.
Tun:
Das JWT beim Anmelden speichern, damit die Wiederherstellung nur einen Schritt erfordert createSession anrufen.
3. Das Push-Token wird nach der Erstellung der Sitzung nicht registriert
Nicht tun:
Registrieren Sie das Push-Token einmalig beim Start der App und danach nie wieder. Das Token muss nach jeder vom Benutzer initiierten Anmeldung bei Vonage registriert werden (da die vorherige Geräteregistrierung beim Abmelden gelöscht wurde).
Tun:
Rufen Sie an. registerDevicePushToken innerhalb der createSession Erfolgs-Callback für vom Benutzer initiierte Anmeldungen. Bei der Wiederherstellung von Sitzungen sollte die Registrierung übersprungen werden, es sei denn, das FCM-Token hat sich geändert (siehe Fallstrick Nr. 5).
4. Die Abmeldung führt nicht zur Aufhebung der Registrierung
Nicht tun:
Überspringen unregisterDevicePushToken beim Abmelden. Die Anrufe des bisherigen Nutzers gehen weiterhin auf diesem Gerät ein.
Tun:
Rufen Sie immer an unregisterDevicePushToken vorher deleteSession beim Abmelden.
5. Registrierung doppelter Gerätetoken
Nicht tun:
Rufen Sie an. registerDevicePushToken auf jedem createSession unabhängig davon, ob sich das Token geändert hat. Der Vonage-Server gibt ein neues deviceId bei jedem Registrierungsaufruf – wiederholte Aufrufe mit demselben Token führen zu doppelten Registrierungen, was mehrere Push-Benachrichtigungen pro Aufruf zur Folge hat.
Tun:
Verfolgen Sie das zuletzt erfolgreich registrierte Token (z. B. coreContext.lastRegisteredPushToken). Rufen Sie nur registerDevicePushToken Wann:
- Dies ist eine neue, vom Benutzer initiierte Anmeldung (das vorherige Gerät wurde abgemeldet).
- Der FCM-Token hat sich geändert (
onNewTokenentlassen) coreContext.deviceIdist null (es liegt keine vorherige Registrierung vor)
Die Registrierung bei der Wiederherstellung der Sitzung überspringen, wenn das Token mit dem zuvor registrierten übereinstimmt.
6. Abgelaufenes FCM-Token wurde nicht neu registriert
Nicht tun:
Ignorieren onNewToken. FCM wechselt die Tokens ohne Vorwarnung – alle zukünftigen Push-Benachrichtigungen können dann nicht mehr zugestellt werden.
Tun:
Unter onNewToken, speichern Sie das neue Token und rufen Sie registerDevicePushToken wenn eine Sitzung aktiv ist.
7. Registrierung des Anrufs außerhalb eines gestarteten Dienstes
Nicht tun:
Rufen Sie an. callsManager.addCall aus einem App-Singleton oder einem einmaligen Coroutine-Gültigkeitsbereich. Bei einem Kaltstart im Hintergrund fügt Telecom die Verbindung hinzu und löst sie anschließend sofort wieder auf (ERROR_CALL_IS_NOT_BEING_TRACKED).
Tun:
Starte ein Service und laufen addCall aus seinem Geltungsbereich heraus, sodass Telecom im Rahmen dieses Verfahrens berechtigt ist, den Anruf beizubehalten.
8. Keine CallStyle-Benachrichtigung versenden
Nicht tun:
Den Anruf protokollieren, aber das CallStyle Benachrichtigung. Core-Telecom gewährt nur solange eine Vordergrund-/Mikrofonpriorität, wie eine solche Benachrichtigung angezeigt wird – ohne diese Benachrichtigung hat der Anruf im Hintergrund keinen zuverlässigen Zugriff auf das Mikrofon.
Tun:
Einen Beitrag veröffentlichen CallStyle Benachrichtigung für den gesamten Anruf mit Vollbild-Intent bei eingehenden Anrufen.
9. Einen eigenen Mikrofon-Foreground-Dienst hinzufügen
Nicht tun:
Ein Handbuch definieren und starten foregroundServiceType="microphone" Dienst. Bei Core-Telecom ist dieser redundant und kann zu Konflikten führen.
Tun:
Überlassen Sie Core-Telecom die Bereitstellung des Vordergrunddienstes; Ihr eigener Dienst ist ein gewöhnlicher gestartet Dienst für die Benachrichtigung und die Verankerung von Prozessen.
10. Übergabe einer nicht unterstützten „DisconnectCause“
Nicht tun:
Beliebig weiterleiten DisconnectCause Codes an Core-Telecom disconnect — alles, was draußen ist LOCAL/REMOTE/MISSED/REJECTED bringt die Sitzung zum Absturz und beendet sie.
Tun:
Die Ursache an der Grenze abgrenzen, bevor der Aufruf erfolgt disconnect.
11. VoiceClient bei jedem Push neu erstellen
Nicht tun:
Instanziieren VoiceClient in Ihrem FirebaseMessagingService oder als „Per-Push“-Handler. Dadurch werden alle Listener gelöscht und der Sitzungsstatus geht verloren.
Tun:
Initialisieren VoiceClient einmal in Application.onCreate() oder als Singleton. Verwenden Sie es während des gesamten Lebenszyklus der App wieder.
Checkliste für die Produktion
Bevor Sie Ihre App veröffentlichen, Verify bitte jeden Punkt:
- [ ]
google-services.jsonvorhanden inapp/und aus der Versionskontrolle ausgeschlossen - [ ]
androidx.core:core-telecomAbhängigkeit hinzugefügt - [ ] In der Datei „AndroidManifest.xml“ deklarierte erforderliche Berechtigungen:
RECORD_AUDIO,MANAGE_OWN_CALLS,FOREGROUND_SERVICE,FOREGROUND_SERVICE_PHONE_CALL,FOREGROUND_SERVICE_MICROPHONE,POST_NOTIFICATIONS,USE_FULL_SCREEN_INTENT - [ ]
RECORD_AUDIOLaufzeitberechtigung wird vor jedem Aufrufversuch angefordert und erteilt - [ ]
PushNotificationService,CallServiceundCallActionReceiverim Manifest deklariert (kein manueller Mikrofon-Foreground-Dienst) - [ ]
VoiceClientwird einmal initialisiert (in Applications.onCreate oder als Singleton), wobei alle Listener vor der ersten Sitzung festgelegt werden - [ ] Warten auf die Wiederherstellung der Sitzung
processPushCallInvite— Die Verkettung ist unter Android sicher - [ ] JWT (und Refresh-Token) werden bei der Anmeldung lokal gespeichert – die Wiederherstellung sollte keinen Live-Backend-Aufruf erfordern
- [ ]
registerDevicePushTokenwurde nach der benutzerseitig initiierten Anmeldung aufgerufencreateSessiongelingt; wird bei der Wiederherstellung der Sitzung übersprungen, es sei denn, das FCM-Token hat sich seit der letzten Registrierung geändert - [ ]
unregisterDevicePushTokenwird bei expliziter Abmeldung des Benutzers aufgerufen - [ ] Die FCM-Token-Rotation wird abgewickelt in
onNewToken— speichern und erneut registrieren, falls die Sitzung aktiv ist - [ ]
addCallinnerhalb eines gestarteten Prozesses gehostetService(kein App-Singleton) - [ ]
CallStyleBenachrichtigung für den gesamten Anruf, mit Vollbild-Modus bei eingehenden Anrufen - [ ]
DisconnectCausefestgeklemmt anLOCAL/REMOTE/MISSED/REJECTEDvorherdisconnect - [ ] Jeder Aufleg-, Abbruch- oder Medien-Trennvorgang bricht die Benachrichtigung ab und beendet die Telekommunikationsverbindung.
- [ ] Die Fehlerbehandlung für Sitzungen stellt die Sitzung automatisch wieder her;
TokenExpiredüberspringt gespeicherte JWTs - [ ] Getestet auf einem physischen Gerät, wobei die App vor dem eingehenden Anruf vollständig beendet wurde
- [ ] Auf mindestens einem Gerät von Samsung oder einem anderen Hersteller getestet (nicht nur auf Pixel-Geräten oder im Emulator)