Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/react-native-callingx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ Call events:
- `didDisplayIncomingCall`
- `didToggleHoldCallAction`
- `didPerformSetMutedCallAction`
- `didChangeAudioRoute`
- `didReceiveStartCallAction`
- `didActivateAudioSession`
- `didDeactivateAudioSession`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
<action android:name="io.getstream.CALL_INACTIVE" />
<action android:name="io.getstream.CALL_ACTIVE" />
<action android:name="io.getstream.CALL_MUTED" />
<action android:name="io.getstream.CALL_ENDPOINT_CHANGED" />
<action android:name="io.getstream.CALL_AUDIO_ENDPOINTS_CHANGED" />
<action android:name="io.getstream.CALL_END" />
<action android:name="io.getstream.CALL_REGISTRATION_FAILED" />
</intent-filter>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package io.getstream.rn.callingx

import io.getstream.rn.callingx.utils.AudioEndpointUtils
import java.util.concurrent.ConcurrentHashMap

/**
* Shared, process-wide store for Telecom audio-endpoint state.
*
* [CallService] owns the Telecom repository and writes endpoint snapshots here whenever they
* change; [CallingxModuleImpl] reads them to answer synchronous JS queries
* without cross-component coupling.
*
* Also holds the sticky default-endpoint preference, which is applied at call registration time by
* resolving it against the pre-call endpoints and passing it as
* `CallAttributesCompat.preferredStartingCallEndpoint`.
*/
object AudioEndpointStore {

/** Last serialized endpoints snapshot per callId (see [AudioEndpointUtils.snapshotJson]). */
private val snapshotByCallId = ConcurrentHashMap<String, String>()

/** Sticky default preference: "speaker" or "earpiece". Null means "let Telecom decide". */
@Volatile
private var defaultEndpointPref: String? = null

fun setSnapshot(callId: String, snapshotJson: String) {
snapshotByCallId[callId] = snapshotJson
}

fun getSnapshot(callId: String): String =
snapshotByCallId[callId] ?: AudioEndpointUtils.EMPTY_SNAPSHOT_JSON

fun setDefaultEndpointPref(endpointType: String?) {
defaultEndpointPref = endpointType
}

fun getDefaultEndpointPref(): String? = defaultEndpointPref

fun clear(callId: String) {
snapshotByCallId.remove(callId)
}

fun clearAll() {
snapshotByCallId.clear()
defaultEndpointPref = null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ object CallRegistrationStore {
return trackedCallIds.isNotEmpty()
}

fun getTrackedCallIds(): List<String> {
return trackedCallIds.toList()
}

/**
* Queues an action for a call that is not yet registered.
* Pending actions are drained and executed once registration completes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import io.getstream.rn.callingx.notifications.NotificationChannelsManager
import io.getstream.rn.callingx.notifications.NotificationsConfig
import io.getstream.rn.callingx.repo.CallRepository
import io.getstream.rn.callingx.repo.CallRepositoryFactory
import io.getstream.rn.callingx.utils.AudioEndpointUtils
import io.getstream.rn.callingx.utils.SettingsStore
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -347,6 +348,8 @@ class CallService : Service(), CallRepository.Listener {
}
}
is Call.None, is Call.Unregistered -> {
CallRegistrationStore.removeTrackedCall(callId)
AudioEndpointStore.clear(callId)
repromoteForegroundIfNeeded(callId)
if (!callRepository.hasRingingCall()) notificationManager.stopRingtone()

Expand Down Expand Up @@ -418,10 +421,18 @@ class CallService : Service(), CallRepository.Listener {
}
}

override fun onCallEndpointChanged(callId: String, endpoint: String) {
sendBroadcastEvent(CallingxModuleImpl.CALL_ENDPOINT_CHANGED_ACTION) {
override fun onCallAudioEndpointsChanged(callId: String) {
val call = callRepository.getCall(callId) ?: return
val snapshotJson =
AudioEndpointUtils.snapshotJson(
call.currentCallEndpoint,
call.availableCallEndpoints,
)
AudioEndpointStore.setSnapshot(callId, snapshotJson)

sendBroadcastEvent(CallingxModuleImpl.CALL_AUDIO_ENDPOINTS_CHANGED_ACTION) {
putExtra(CallingxModuleImpl.EXTRA_CALL_ID, callId)
putExtra(CallingxModuleImpl.EXTRA_AUDIO_ENDPOINT, endpoint)
putExtra(CallingxModuleImpl.EXTRA_AUDIO_ENDPOINTS_SNAPSHOT, snapshotJson)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package io.getstream.rn.callingx

import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.ParcelUuid
import android.telecom.DisconnectCause
import android.util.Log
import androidx.core.content.ContextCompat
Expand Down Expand Up @@ -33,7 +35,7 @@ class CallingxModuleImpl(
const val EXTRA_MUTED = "is_muted"
const val EXTRA_ON_HOLD = "hold"
const val EXTRA_DISCONNECT_CAUSE = "disconnect_cause"
const val EXTRA_AUDIO_ENDPOINT = "audio_endpoint"
const val EXTRA_AUDIO_ENDPOINTS_SNAPSHOT = "audio_endpoints_snapshot"
const val EXTRA_SOURCE = "source"
const val EXTRA_ACTION = "action_name"

Expand All @@ -44,7 +46,7 @@ class CallingxModuleImpl(
const val CALL_INACTIVE_ACTION = "io.getstream.CALL_INACTIVE"
const val CALL_ACTIVE_ACTION = "io.getstream.CALL_ACTIVE"
const val CALL_MUTED_ACTION = "io.getstream.CALL_MUTED"
const val CALL_ENDPOINT_CHANGED_ACTION = "io.getstream.CALL_ENDPOINT_CHANGED"
const val CALL_AUDIO_ENDPOINTS_CHANGED_ACTION = "io.getstream.CALL_AUDIO_ENDPOINTS_CHANGED"
const val CALL_END_ACTION = "io.getstream.CALL_END"
const val CALL_REGISTRATION_FAILED_ACTION = "io.getstream.CALL_REGISTRATION_FAILED"
const val CALL_OPTIMISTIC_ACCEPT_ACTION = "io.getstream.ACCEPT_CALL_OPTIMISTIC"
Expand Down Expand Up @@ -72,6 +74,7 @@ class CallingxModuleImpl(

LifecycleListener.unregister()
CallRegistrationStore.clearAll()
AudioEndpointStore.clearAll()
CallEventBus.unsubscribe(this)

isModuleInitialized = false
Expand Down Expand Up @@ -192,12 +195,7 @@ class CallingxModuleImpl(

fun answerIncomingCall(callId: String, promise: Promise) {
debugLog(TAG, "[module] answerIncomingCall: Answering call: $callId")
// TODO: get the call type from the call attributes
val isAudioCall = true // TODO: get the call type from the call attributes
// registeredCall.callAttributes.callType ==
// CallAttributesCompat.CALL_TYPE_AUDIO_CALL
// currentCall?.processAction(TelecomCallAction.Answer(isAudioCall))
executeServiceAction(callId, CallAction.Answer(isAudioCall), promise)
executeServiceAction(callId, CallAction.Answer, promise)
}

fun startCall(
Expand Down Expand Up @@ -291,6 +289,35 @@ class CallingxModuleImpl(
return CallRegistrationStore.hasRegisteredCall()
}

/** Android backs Telecom audio routing only when the Jetpack Telecom repository is used (API 26+). */
fun isTelecomBacked(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O

fun getAvailableAudioEndpoints(callId: String, promise: Promise) {
promise.resolve(AudioEndpointStore.getSnapshot(callId))
}

fun requestAudioEndpointChange(callId: String, endpointId: String, promise: Promise) {
debugLog(TAG, "[module] requestAudioEndpointChange: $callId -> $endpointId")
val parcelUuid =
try {
ParcelUuid.fromString(endpointId)
} catch (e: IllegalArgumentException) {
promise.reject("INVALID_ENDPOINT_ID", "Invalid endpoint id: $endpointId", e)
return
}
executeServiceAction(callId, CallAction.SwitchAudioEndpoint(parcelUuid), promise)
}

fun setDefaultAudioDeviceEndpointType(endpointType: String?) {
debugLog(TAG, "[module] setDefaultAudioDeviceEndpointType: $endpointType")
AudioEndpointStore.setDefaultEndpointPref(endpointType)
SettingsStore.setDefaultDeviceEndpointType(reactApplicationContext, endpointType)
}

fun getRegisteredCallIds(): WritableArray {
return Arguments.fromList(CallRegistrationStore.getTrackedCallIds())
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

fun setMutedCall(callId: String, isMuted: Boolean, promise: Promise) {
debugLog(TAG, "[module] setMutedCall: Setting muted call: $callId, $isMuted")
val action = CallAction.ToggleMute(isMuted)
Expand Down Expand Up @@ -488,11 +515,16 @@ class CallingxModuleImpl(
}
sendJSEvent("didPerformSetMutedCallAction", params)
}
CALL_ENDPOINT_CHANGED_ACTION -> {
if (extras.containsKey(EXTRA_AUDIO_ENDPOINT)) {
params.putString("output", extras.getString(EXTRA_AUDIO_ENDPOINT))
CALL_AUDIO_ENDPOINTS_CHANGED_ACTION -> {
// The snapshot (a JSON string with endpoints + currentEndpoint) is parsed on the
// JS side. Carried as a single string field to survive event param flattening.
if (extras.containsKey(EXTRA_AUDIO_ENDPOINTS_SNAPSHOT)) {
params.putString(
"snapshot",
extras.getString(EXTRA_AUDIO_ENDPOINTS_SNAPSHOT),
)
}
sendJSEvent("didChangeAudioRoute", params)
sendJSEvent("didChangeAudioEndpoints", params)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import kotlinx.parcelize.Parcelize
*/
sealed interface CallAction : Parcelable {
@Parcelize
data class Answer(val isAudioCall: Boolean) : CallAction
object Answer : CallAction

@Parcelize
data class Disconnect(val cause: DisconnectCause) : CallAction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.os.Bundle
import android.telecom.DisconnectCause
import android.util.Log
import androidx.core.telecom.CallAttributesCompat
import androidx.core.telecom.CallEndpointCompat
import io.getstream.rn.callingx.model.Call
import io.getstream.rn.callingx.model.CallAction
import io.getstream.rn.callingx.CallService
Expand Down Expand Up @@ -36,7 +37,11 @@ abstract class CallRepository(protected val context: Context) {
fun onIsCallActive(callId: String)
fun onCallRegistered(callId: String, incoming: Boolean)
fun onMuteCallChanged(callId: String, isMuted: Boolean)
fun onCallEndpointChanged(callId: String, endpoint: String)

/**
* Fired when the current endpoint or the set of available endpoints changes.
*/
fun onCallAudioEndpointsChanged(callId: String) {}
Comment thread
santhoshvai marked this conversation as resolved.
}

protected val _calls: MutableStateFlow<Map<String, Call.Registered>> = MutableStateFlow(emptyMap())
Expand Down Expand Up @@ -143,7 +148,8 @@ abstract class CallRepository(protected val context: Context) {
displayName: String,
address: Uri,
isIncoming: Boolean,
isVideo: Boolean
isVideo: Boolean,
preferredStartingCallEndpoint: CallEndpointCompat? = null,
): CallAttributesCompat {
return CallAttributesCompat(
displayName = displayName,
Expand All @@ -164,6 +170,7 @@ abstract class CallRepository(protected val context: Context) {
CallAttributesCompat.SUPPORTS_SET_INACTIVE or
CallAttributesCompat.SUPPORTS_STREAM or
CallAttributesCompat.SUPPORTS_TRANSFER,
preferredStartingCallEndpoint = preferredStartingCallEndpoint,
)
}

Expand Down
Loading