expo-callkit-telecom 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/android/src/main/java/expo/modules/callkittelecom/ExpoCallKitTelecomModule.kt +254 -282
- package/android/src/main/java/expo/modules/callkittelecom/IncomingCallActivity.kt +27 -34
- package/android/src/main/java/expo/modules/callkittelecom/events/CallEventEmitter.kt +21 -32
- package/android/src/main/java/expo/modules/callkittelecom/managers/CallAudioManager.kt +53 -74
- package/android/src/main/java/expo/modules/callkittelecom/managers/CallManager.kt +95 -156
- package/android/src/main/java/expo/modules/callkittelecom/managers/CallNotificationManager.kt +71 -84
- package/android/src/main/java/expo/modules/callkittelecom/managers/CaptureSessionManager.kt +1 -3
- package/android/src/main/java/expo/modules/callkittelecom/managers/DialtonePlayer.kt +6 -4
- package/android/src/main/java/expo/modules/callkittelecom/managers/FulfillRequestManager.kt +21 -21
- package/android/src/main/java/expo/modules/callkittelecom/managers/VoIPPushManager.kt +3 -5
- package/android/src/main/java/expo/modules/callkittelecom/models/CallModels.kt +28 -36
- package/android/src/main/java/expo/modules/callkittelecom/services/CallNotificationReceiver.kt +7 -8
- package/android/src/main/java/expo/modules/callkittelecom/services/ExpoCallKitTelecomMessagingService.kt +9 -16
- package/android/src/main/java/expo/modules/callkittelecom/store/CallStore.kt +32 -68
- package/android/src/main/java/expo/modules/callkittelecom/utils/CallKitTelecomLog.kt +6 -17
- package/android/src/main/java/expo/modules/callkittelecom/utils/PermissionUtils.kt +7 -7
- package/ios/Managers/CaptureSessionManager.swift +1 -1
- package/package.json +1 -1
|
@@ -16,26 +16,25 @@ import expo.modules.callkittelecom.managers.CallManager
|
|
|
16
16
|
import expo.modules.callkittelecom.models.CallSessionStatus
|
|
17
17
|
import expo.modules.callkittelecom.store.CallStore
|
|
18
18
|
import expo.modules.callkittelecom.utils.CallKitTelecomLog
|
|
19
|
+
import java.net.HttpURLConnection
|
|
20
|
+
import java.net.URL
|
|
21
|
+
import java.util.UUID
|
|
19
22
|
import kotlinx.coroutines.CoroutineScope
|
|
20
23
|
import kotlinx.coroutines.Dispatchers
|
|
21
24
|
import kotlinx.coroutines.SupervisorJob
|
|
22
25
|
import kotlinx.coroutines.cancel
|
|
23
26
|
import kotlinx.coroutines.launch
|
|
24
27
|
import kotlinx.coroutines.withContext
|
|
25
|
-
import java.net.HttpURLConnection
|
|
26
|
-
import java.net.URL
|
|
27
|
-
import java.util.UUID
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Native full-screen incoming call Activity displayed over the lock screen.
|
|
31
31
|
*
|
|
32
|
-
* Shows caller information with answer/decline buttons. Automatically
|
|
33
|
-
*
|
|
34
|
-
* timed out, or ended elsewhere).
|
|
32
|
+
* Shows caller information with answer/decline buttons. Automatically dismisses when the call
|
|
33
|
+
* leaves the RINGING state (answered, declined, timed out, or ended elsewhere).
|
|
35
34
|
*
|
|
36
35
|
* Answer flow: answers the call directly, dismisses the keyguard via
|
|
37
|
-
* [KeyguardManager.requestDismissKeyguard], then launches the main Activity
|
|
38
|
-
*
|
|
36
|
+
* [KeyguardManager.requestDismissKeyguard], then launches the main Activity so the user sees the
|
|
37
|
+
* in-call UI after unlocking.
|
|
39
38
|
*/
|
|
40
39
|
class IncomingCallActivity : Activity() {
|
|
41
40
|
companion object {
|
|
@@ -81,7 +80,7 @@ class IncomingCallActivity : Activity() {
|
|
|
81
80
|
window.addFlags(
|
|
82
81
|
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
|
|
83
82
|
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
|
84
|
-
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
|
83
|
+
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
|
85
84
|
)
|
|
86
85
|
}
|
|
87
86
|
|
|
@@ -99,9 +98,8 @@ class IncomingCallActivity : Activity() {
|
|
|
99
98
|
private fun bindAppBranding() {
|
|
100
99
|
try {
|
|
101
100
|
val appInfo = packageManager.getApplicationInfo(packageName, 0)
|
|
102
|
-
findViewById<ImageView>(R.id.expo_callkit_telecom_app_icon)
|
|
103
|
-
packageManager.getApplicationIcon(appInfo)
|
|
104
|
-
)
|
|
101
|
+
findViewById<ImageView>(R.id.expo_callkit_telecom_app_icon)
|
|
102
|
+
.setImageDrawable(packageManager.getApplicationIcon(appInfo))
|
|
105
103
|
findViewById<TextView>(R.id.expo_callkit_telecom_app_name).text =
|
|
106
104
|
packageManager.getApplicationLabel(appInfo)
|
|
107
105
|
} catch (_: Exception) {
|
|
@@ -109,10 +107,7 @@ class IncomingCallActivity : Activity() {
|
|
|
109
107
|
}
|
|
110
108
|
}
|
|
111
109
|
|
|
112
|
-
private fun bindCallerInfo(
|
|
113
|
-
displayName: String?,
|
|
114
|
-
hasVideo: Boolean,
|
|
115
|
-
) {
|
|
110
|
+
private fun bindCallerInfo(displayName: String?, hasVideo: Boolean) {
|
|
116
111
|
val name = displayName ?: "Unknown"
|
|
117
112
|
|
|
118
113
|
findViewById<TextView>(R.id.expo_callkit_telecom_avatar_text).text =
|
|
@@ -125,9 +120,9 @@ class IncomingCallActivity : Activity() {
|
|
|
125
120
|
}
|
|
126
121
|
|
|
127
122
|
/**
|
|
128
|
-
* Loads the caller's avatar from [avatarUrl] on a background thread.
|
|
129
|
-
*
|
|
130
|
-
*
|
|
123
|
+
* Loads the caller's avatar from [avatarUrl] on a background thread. On success, displays a
|
|
124
|
+
* circular-cropped image and hides the initial letter. On failure, silently keeps the initial
|
|
125
|
+
* letter fallback.
|
|
131
126
|
*/
|
|
132
127
|
private fun loadAvatar(avatarUrl: String?) {
|
|
133
128
|
if (avatarUrl.isNullOrBlank()) return
|
|
@@ -165,10 +160,7 @@ class IncomingCallActivity : Activity() {
|
|
|
165
160
|
}
|
|
166
161
|
}
|
|
167
162
|
|
|
168
|
-
private fun bindButtons(
|
|
169
|
-
id: UUID,
|
|
170
|
-
hasVideo: Boolean,
|
|
171
|
-
) {
|
|
163
|
+
private fun bindButtons(id: UUID, hasVideo: Boolean) {
|
|
172
164
|
val answerButton = findViewById<ImageButton>(R.id.expo_callkit_telecom_answer_button)
|
|
173
165
|
if (hasVideo) {
|
|
174
166
|
answerButton.setImageResource(R.drawable.expo_callkit_telecom_ic_videocam)
|
|
@@ -181,12 +173,12 @@ class IncomingCallActivity : Activity() {
|
|
|
181
173
|
}
|
|
182
174
|
|
|
183
175
|
/**
|
|
184
|
-
* Answers the call directly, then dismisses the keyguard and launches
|
|
185
|
-
*
|
|
176
|
+
* Answers the call directly, then dismisses the keyguard and launches the main Activity so the
|
|
177
|
+
* user transitions into the in-call UI.
|
|
186
178
|
*
|
|
187
|
-
* The call is answered immediately (media connection starts) regardless
|
|
188
|
-
*
|
|
189
|
-
*
|
|
179
|
+
* The call is answered immediately (media connection starts) regardless of whether the keyguard
|
|
180
|
+
* dismissal succeeds. This matches the behavior of iOS CallKit where audio connects before the
|
|
181
|
+
* device is unlocked.
|
|
190
182
|
*/
|
|
191
183
|
private fun onAnswerTapped(id: UUID) {
|
|
192
184
|
if (isAnswering) return
|
|
@@ -237,12 +229,11 @@ class IncomingCallActivity : Activity() {
|
|
|
237
229
|
}
|
|
238
230
|
|
|
239
231
|
/**
|
|
240
|
-
* Observes session updates to auto-dismiss when the call is no longer ringing
|
|
241
|
-
*
|
|
232
|
+
* Observes session updates to auto-dismiss when the call is no longer ringing (answered
|
|
233
|
+
* elsewhere, timed out, or ended by the remote side).
|
|
242
234
|
*
|
|
243
|
-
* When [isAnswering] is true (user tapped answer locally), only auto-dismiss
|
|
244
|
-
*
|
|
245
|
-
* the keyguard dismissal flow.
|
|
235
|
+
* When [isAnswering] is true (user tapped answer locally), only auto-dismiss for ENDED status —
|
|
236
|
+
* the CONNECTING transition is expected and handled by the keyguard dismissal flow.
|
|
246
237
|
*/
|
|
247
238
|
private fun observeSessionChanges(id: UUID) {
|
|
248
239
|
scope.launch {
|
|
@@ -250,7 +241,9 @@ class IncomingCallActivity : Activity() {
|
|
|
250
241
|
if (session.status == CallSessionStatus.ENDED) {
|
|
251
242
|
finish()
|
|
252
243
|
} else if (!isAnswering && session.status != CallSessionStatus.RINGING) {
|
|
253
|
-
CallKitTelecomLog.d(TAG) {
|
|
244
|
+
CallKitTelecomLog.d(TAG) {
|
|
245
|
+
"Call no longer ringing (${session.status.value}), finishing"
|
|
246
|
+
}
|
|
254
247
|
finish()
|
|
255
248
|
}
|
|
256
249
|
}
|
|
@@ -4,10 +4,7 @@ import expo.modules.callkittelecom.utils.CallKitTelecomLog
|
|
|
4
4
|
import java.time.Instant
|
|
5
5
|
import java.time.format.DateTimeFormatter
|
|
6
6
|
|
|
7
|
-
private data class QueuedEvent(
|
|
8
|
-
val body: Map<String, Any?>,
|
|
9
|
-
val timestamp: Instant,
|
|
10
|
-
)
|
|
7
|
+
private data class QueuedEvent(val body: Map<String, Any?>, val timestamp: Instant)
|
|
11
8
|
|
|
12
9
|
/**
|
|
13
10
|
* Event bridge between Android native call state and the Expo JS module.
|
|
@@ -29,8 +26,7 @@ object CallEventEmitter {
|
|
|
29
26
|
|
|
30
27
|
var defaultQueueLimit: Int? = 0
|
|
31
28
|
|
|
32
|
-
@Volatile
|
|
33
|
-
private var sender: ((String, Map<String, Any?>) -> Unit)? = null
|
|
29
|
+
@Volatile private var sender: ((String, Map<String, Any?>) -> Unit)? = null
|
|
34
30
|
|
|
35
31
|
/** Sets or clears the active event sender provided by the Expo module. */
|
|
36
32
|
fun setSender(eventSender: ((String, Map<String, Any?>) -> Unit)?) {
|
|
@@ -42,24 +38,17 @@ object CallEventEmitter {
|
|
|
42
38
|
*
|
|
43
39
|
* `null` means unlimited queueing, `0` disables queueing.
|
|
44
40
|
*/
|
|
45
|
-
fun setQueueLimit(
|
|
46
|
-
eventName
|
|
47
|
-
limit: Int?,
|
|
48
|
-
) {
|
|
49
|
-
synchronized(lock) {
|
|
50
|
-
queueLimits[eventName] = limit
|
|
51
|
-
}
|
|
41
|
+
fun setQueueLimit(eventName: String, limit: Int?) {
|
|
42
|
+
synchronized(lock) { queueLimits[eventName] = limit }
|
|
52
43
|
}
|
|
53
44
|
|
|
54
45
|
/**
|
|
55
46
|
* Sends an event to JS if it is currently observed, or queues it otherwise.
|
|
56
47
|
*
|
|
57
|
-
* All delivered events are augmented with a `meta` object containing timestamp and flush
|
|
48
|
+
* All delivered events are augmented with a `meta` object containing timestamp and flush
|
|
49
|
+
* status.
|
|
58
50
|
*/
|
|
59
|
-
fun send(
|
|
60
|
-
eventName: String,
|
|
61
|
-
body: Map<String, Any?>,
|
|
62
|
-
) {
|
|
51
|
+
fun send(eventName: String, body: Map<String, Any?>) {
|
|
63
52
|
val timestamp = Instant.now()
|
|
64
53
|
val senderRef = sender
|
|
65
54
|
val isObserving = synchronized(lock) { observingEvents.contains(eventName) }
|
|
@@ -79,16 +68,16 @@ object CallEventEmitter {
|
|
|
79
68
|
queueCount = eventQueues[eventName]?.size ?: 0
|
|
80
69
|
observingEvents.add(eventName)
|
|
81
70
|
}
|
|
82
|
-
CallKitTelecomLog.d(TAG) {
|
|
71
|
+
CallKitTelecomLog.d(TAG) {
|
|
72
|
+
"Start observing - event: $eventName, queuedEvents: $queueCount"
|
|
73
|
+
}
|
|
83
74
|
flushQueue(eventName)
|
|
84
75
|
}
|
|
85
76
|
|
|
86
77
|
/** Marks an event as no longer observed by JS. */
|
|
87
78
|
fun stopObserving(eventName: String) {
|
|
88
79
|
CallKitTelecomLog.d(TAG) { "Stop observing - event: $eventName" }
|
|
89
|
-
synchronized(lock) {
|
|
90
|
-
observingEvents.remove(eventName)
|
|
91
|
-
}
|
|
80
|
+
synchronized(lock) { observingEvents.remove(eventName) }
|
|
92
81
|
}
|
|
93
82
|
|
|
94
83
|
/** Adds native event metadata used by TypeScript event types. */
|
|
@@ -107,11 +96,7 @@ object CallEventEmitter {
|
|
|
107
96
|
}
|
|
108
97
|
|
|
109
98
|
/** Queues an event and enforces per-event queue limits (drop oldest first). */
|
|
110
|
-
private fun queueEvent(
|
|
111
|
-
name: String,
|
|
112
|
-
body: Map<String, Any?>,
|
|
113
|
-
timestamp: Instant,
|
|
114
|
-
) {
|
|
99
|
+
private fun queueEvent(name: String, body: Map<String, Any?>, timestamp: Instant) {
|
|
115
100
|
synchronized(lock) {
|
|
116
101
|
val limit = queueLimits[name] ?: defaultQueueLimit
|
|
117
102
|
if (limit == 0) {
|
|
@@ -124,12 +109,14 @@ object CallEventEmitter {
|
|
|
124
109
|
|
|
125
110
|
if (limit != null && queue.size > limit) {
|
|
126
111
|
val dropCount = queue.size - limit
|
|
127
|
-
repeat(dropCount) {
|
|
128
|
-
|
|
112
|
+
repeat(dropCount) { queue.removeAt(0) }
|
|
113
|
+
CallKitTelecomLog.d(TAG) {
|
|
114
|
+
"Queueing event (dropped $dropCount old) - name: $name, queueSize: ${queue.size}"
|
|
129
115
|
}
|
|
130
|
-
CallKitTelecomLog.d(TAG) { "Queueing event (dropped $dropCount old) - name: $name, queueSize: ${queue.size}" }
|
|
131
116
|
} else {
|
|
132
|
-
CallKitTelecomLog.d(TAG) {
|
|
117
|
+
CallKitTelecomLog.d(TAG) {
|
|
118
|
+
"Queueing event (JS not listening) - name: $name, queueSize: ${queue.size}"
|
|
119
|
+
}
|
|
133
120
|
}
|
|
134
121
|
}
|
|
135
122
|
}
|
|
@@ -140,7 +127,9 @@ object CallEventEmitter {
|
|
|
140
127
|
val queue = synchronized(lock) { eventQueues.remove(eventName) } ?: return
|
|
141
128
|
if (queue.isEmpty()) return
|
|
142
129
|
|
|
143
|
-
CallKitTelecomLog.d(TAG) {
|
|
130
|
+
CallKitTelecomLog.d(TAG) {
|
|
131
|
+
"Flushing event queue - event: $eventName, count: ${queue.size}"
|
|
132
|
+
}
|
|
144
133
|
queue.forEach { event ->
|
|
145
134
|
senderRef(
|
|
146
135
|
eventName,
|
|
@@ -14,9 +14,8 @@ import expo.modules.callkittelecom.utils.PermissionUtils
|
|
|
14
14
|
/**
|
|
15
15
|
* Manages Android call audio state and routing for the shared calls API.
|
|
16
16
|
*
|
|
17
|
-
* Audio focus and mode are managed by Core-Telecom. This manager tracks
|
|
18
|
-
*
|
|
19
|
-
* via the active call scope.
|
|
17
|
+
* Audio focus and mode are managed by Core-Telecom. This manager tracks endpoint state, emits route
|
|
18
|
+
* changes to JS, and requests endpoint switches via the active call scope.
|
|
20
19
|
*/
|
|
21
20
|
object CallAudioManager {
|
|
22
21
|
private const val TAG = "ExpoCallKitTelecom.Audio"
|
|
@@ -55,14 +54,14 @@ object CallAudioManager {
|
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
|
|
58
|
-
CallKitTelecomLog.d(TAG) {
|
|
57
|
+
CallKitTelecomLog.d(TAG) {
|
|
58
|
+
"Audio devices removed - count: ${removedDevices.size}"
|
|
59
|
+
}
|
|
59
60
|
if (!isActive) emitRouteChanged()
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
routeCallback?.let { callback ->
|
|
64
|
-
audioManager.registerAudioDeviceCallback(callback, null)
|
|
65
|
-
}
|
|
64
|
+
routeCallback?.let { callback -> audioManager.registerAudioDeviceCallback(callback, null) }
|
|
66
65
|
|
|
67
66
|
isInitialized = true
|
|
68
67
|
CallKitTelecomLog.d(TAG) { "Initialized CallAudioManager" }
|
|
@@ -83,15 +82,14 @@ object CallAudioManager {
|
|
|
83
82
|
/** Returns current audio session state in the shared TypeScript shape. */
|
|
84
83
|
fun getAudioSessionState(): Map<String, Any?> {
|
|
85
84
|
val sampleRate =
|
|
86
|
-
audioManager
|
|
87
|
-
.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)
|
|
88
|
-
?.toDoubleOrNull()
|
|
85
|
+
audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)?.toDoubleOrNull()
|
|
89
86
|
val framesPerBuffer =
|
|
90
87
|
audioManager
|
|
91
88
|
.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)
|
|
92
89
|
?.toDoubleOrNull()
|
|
93
90
|
val ioBufferDuration =
|
|
94
|
-
if (sampleRate != null && framesPerBuffer != null) framesPerBuffer / sampleRate
|
|
91
|
+
if (sampleRate != null && framesPerBuffer != null) framesPerBuffer / sampleRate
|
|
92
|
+
else null
|
|
95
93
|
|
|
96
94
|
val currentRoute = currentRouteMap()
|
|
97
95
|
|
|
@@ -118,7 +116,9 @@ object CallAudioManager {
|
|
|
118
116
|
|
|
119
117
|
/** No-op on Android — Core-Telecom manages audio mode and focus. Kept for JS API compat. */
|
|
120
118
|
fun restoreAudioSession() {
|
|
121
|
-
CallKitTelecomLog.d(TAG) {
|
|
119
|
+
CallKitTelecomLog.d(TAG) {
|
|
120
|
+
"restoreAudioSession is a no-op on Android (Core-Telecom manages audio)"
|
|
121
|
+
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/**
|
|
@@ -133,18 +133,9 @@ object CallAudioManager {
|
|
|
133
133
|
|
|
134
134
|
isActive = true
|
|
135
135
|
|
|
136
|
-
val callInfos =
|
|
137
|
-
calls.map {
|
|
138
|
-
mapOf(
|
|
139
|
-
"id" to it.id.toString(),
|
|
140
|
-
"status" to it.status.value,
|
|
141
|
-
)
|
|
142
|
-
}
|
|
136
|
+
val callInfos = calls.map { mapOf("id" to it.id.toString(), "status" to it.status.value) }
|
|
143
137
|
|
|
144
|
-
CallEventEmitter.send(
|
|
145
|
-
CallEvents.AUDIO_SESSION_ACTIVATED,
|
|
146
|
-
mapOf("calls" to callInfos),
|
|
147
|
-
)
|
|
138
|
+
CallEventEmitter.send(CallEvents.AUDIO_SESSION_ACTIVATED, mapOf("calls" to callInfos))
|
|
148
139
|
}
|
|
149
140
|
|
|
150
141
|
/**
|
|
@@ -164,18 +155,9 @@ object CallAudioManager {
|
|
|
164
155
|
|
|
165
156
|
emitRouteChanged()
|
|
166
157
|
|
|
167
|
-
val callInfos =
|
|
168
|
-
calls.map {
|
|
169
|
-
mapOf(
|
|
170
|
-
"id" to it.id.toString(),
|
|
171
|
-
"status" to it.status.value,
|
|
172
|
-
)
|
|
173
|
-
}
|
|
158
|
+
val callInfos = calls.map { mapOf("id" to it.id.toString(), "status" to it.status.value) }
|
|
174
159
|
|
|
175
|
-
CallEventEmitter.send(
|
|
176
|
-
CallEvents.AUDIO_SESSION_DEACTIVATED,
|
|
177
|
-
mapOf("calls" to callInfos),
|
|
178
|
-
)
|
|
160
|
+
CallEventEmitter.send(CallEvents.AUDIO_SESSION_DEACTIVATED, mapOf("calls" to callInfos))
|
|
179
161
|
}
|
|
180
162
|
|
|
181
163
|
/** Requests endpoint change to speaker (`true`) or best non-speaker device (`false`). */
|
|
@@ -195,18 +177,22 @@ object CallAudioManager {
|
|
|
195
177
|
}
|
|
196
178
|
|
|
197
179
|
/**
|
|
198
|
-
* Selects the best non-speaker endpoint matching iOS priority:
|
|
199
|
-
*
|
|
180
|
+
* Selects the best non-speaker endpoint matching iOS priority: Bluetooth > Wired Headset >
|
|
181
|
+
* Earpiece (audio) / Speaker (video).
|
|
200
182
|
*/
|
|
201
183
|
private fun selectBestNonSpeakerEndpoint(): CallEndpointCompat? {
|
|
202
184
|
val available = currentAvailableEndpoints
|
|
203
185
|
|
|
204
186
|
available
|
|
205
187
|
.firstOrNull { it.type == CallEndpointCompat.TYPE_BLUETOOTH }
|
|
206
|
-
?.let {
|
|
188
|
+
?.let {
|
|
189
|
+
return it
|
|
190
|
+
}
|
|
207
191
|
available
|
|
208
192
|
.firstOrNull { it.type == CallEndpointCompat.TYPE_WIRED_HEADSET }
|
|
209
|
-
?.let {
|
|
193
|
+
?.let {
|
|
194
|
+
return it
|
|
195
|
+
}
|
|
210
196
|
|
|
211
197
|
return if (configuredForVideo) {
|
|
212
198
|
available.firstOrNull { it.type == CallEndpointCompat.TYPE_SPEAKER }
|
|
@@ -219,16 +205,13 @@ object CallAudioManager {
|
|
|
219
205
|
private fun emitRouteChanged() {
|
|
220
206
|
CallEventEmitter.send(
|
|
221
207
|
CallEvents.AUDIO_ROUTE_CHANGED,
|
|
222
|
-
mapOf(
|
|
223
|
-
"currentRoute" to currentRouteMap(),
|
|
224
|
-
"availableRoutes" to availableRoutesMap(),
|
|
225
|
-
),
|
|
208
|
+
mapOf("currentRoute" to currentRouteMap(), "availableRoutes" to availableRoutesMap()),
|
|
226
209
|
)
|
|
227
210
|
}
|
|
228
211
|
|
|
229
212
|
/**
|
|
230
|
-
* Converts Core-Telecom available endpoints into JS-facing AudioPort objects.
|
|
231
|
-
*
|
|
213
|
+
* Converts Core-Telecom available endpoints into JS-facing AudioPort objects. Each endpoint
|
|
214
|
+
* maps to a single output port.
|
|
232
215
|
*/
|
|
233
216
|
private fun availableRoutesMap(): List<Map<String, String>> =
|
|
234
217
|
currentAvailableEndpoints.mapNotNull { endpoint -> resolveOutputPort(endpoint) }
|
|
@@ -236,38 +219,40 @@ object CallAudioManager {
|
|
|
236
219
|
/**
|
|
237
220
|
* Builds the current route payload with the single active input/output device.
|
|
238
221
|
*
|
|
239
|
-
* During an active call, uses Core-Telecom's [CallEndpointCompat] to determine
|
|
240
|
-
*
|
|
241
|
-
*
|
|
222
|
+
* During an active call, uses Core-Telecom's [CallEndpointCompat] to determine the active
|
|
223
|
+
* device (matching iOS's `AVAudioSession.currentRoute` behavior). When no call is active, falls
|
|
224
|
+
* back to built-in defaults.
|
|
242
225
|
*/
|
|
243
226
|
private fun currentRouteMap(): Map<String, Any?> {
|
|
244
227
|
val endpoint = currentEndpoint
|
|
245
228
|
if (endpoint != null) {
|
|
246
229
|
val output = resolveOutputPort(endpoint)
|
|
247
230
|
val input = resolveInputPort(endpoint.type)
|
|
248
|
-
return mapOf(
|
|
249
|
-
"inputs" to listOfNotNull(input),
|
|
250
|
-
"outputs" to listOfNotNull(output),
|
|
251
|
-
)
|
|
231
|
+
return mapOf("inputs" to listOfNotNull(input), "outputs" to listOfNotNull(output))
|
|
252
232
|
}
|
|
253
233
|
|
|
254
|
-
val input =
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
)
|
|
234
|
+
val input =
|
|
235
|
+
findDevicePort(AudioManager.GET_DEVICES_INPUTS, AudioDeviceInfo.TYPE_BUILTIN_MIC)
|
|
236
|
+
val output =
|
|
237
|
+
findDevicePort(AudioManager.GET_DEVICES_OUTPUTS, AudioDeviceInfo.TYPE_BUILTIN_EARPIECE)
|
|
238
|
+
return mapOf("inputs" to listOfNotNull(input), "outputs" to listOfNotNull(output))
|
|
260
239
|
}
|
|
261
240
|
|
|
262
241
|
/** Maps Core-Telecom endpoint to a port map for the JS layer. */
|
|
263
242
|
private fun resolveOutputPort(endpoint: CallEndpointCompat): Map<String, String>? =
|
|
264
243
|
when (endpoint.type) {
|
|
265
244
|
CallEndpointCompat.TYPE_SPEAKER -> {
|
|
266
|
-
findDevicePort(
|
|
245
|
+
findDevicePort(
|
|
246
|
+
AudioManager.GET_DEVICES_OUTPUTS,
|
|
247
|
+
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
|
|
248
|
+
)
|
|
267
249
|
}
|
|
268
250
|
|
|
269
251
|
CallEndpointCompat.TYPE_EARPIECE -> {
|
|
270
|
-
findDevicePort(
|
|
252
|
+
findDevicePort(
|
|
253
|
+
AudioManager.GET_DEVICES_OUTPUTS,
|
|
254
|
+
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
|
|
255
|
+
)
|
|
271
256
|
}
|
|
272
257
|
|
|
273
258
|
CallEndpointCompat.TYPE_BLUETOOTH -> {
|
|
@@ -280,7 +265,10 @@ object CallAudioManager {
|
|
|
280
265
|
|
|
281
266
|
CallEndpointCompat.TYPE_WIRED_HEADSET -> {
|
|
282
267
|
findDevicePort(AudioManager.GET_DEVICES_OUTPUTS, AudioDeviceInfo.TYPE_WIRED_HEADSET)
|
|
283
|
-
?: findDevicePort(
|
|
268
|
+
?: findDevicePort(
|
|
269
|
+
AudioManager.GET_DEVICES_OUTPUTS,
|
|
270
|
+
AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
|
|
271
|
+
)
|
|
284
272
|
}
|
|
285
273
|
|
|
286
274
|
else -> {
|
|
@@ -301,14 +289,8 @@ object CallAudioManager {
|
|
|
301
289
|
}
|
|
302
290
|
|
|
303
291
|
/** Finds the first matching device by type and returns its port map. */
|
|
304
|
-
private fun findDevicePort(
|
|
305
|
-
direction
|
|
306
|
-
type: Int,
|
|
307
|
-
): Map<String, String>? =
|
|
308
|
-
audioManager
|
|
309
|
-
.getDevices(direction)
|
|
310
|
-
.firstOrNull { it.type == type }
|
|
311
|
-
?.let { portMap(it) }
|
|
292
|
+
private fun findDevicePort(direction: Int, type: Int): Map<String, String>? =
|
|
293
|
+
audioManager.getDevices(direction).firstOrNull { it.type == type }?.let { portMap(it) }
|
|
312
294
|
|
|
313
295
|
/** Serializes an Android audio device into the shared audio port payload shape. */
|
|
314
296
|
private fun portMap(info: AudioDeviceInfo): Map<String, String> =
|
|
@@ -326,8 +308,7 @@ object CallAudioManager {
|
|
|
326
308
|
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "builtInSpeaker"
|
|
327
309
|
|
|
328
310
|
AudioDeviceInfo.TYPE_WIRED_HEADSET,
|
|
329
|
-
AudioDeviceInfo.TYPE_WIRED_HEADPHONES
|
|
330
|
-
-> "headphones"
|
|
311
|
+
AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> "headphones"
|
|
331
312
|
|
|
332
313
|
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> "bluetoothA2DP"
|
|
333
314
|
|
|
@@ -339,12 +320,10 @@ object CallAudioManager {
|
|
|
339
320
|
|
|
340
321
|
AudioDeviceInfo.TYPE_USB_DEVICE,
|
|
341
322
|
AudioDeviceInfo.TYPE_USB_ACCESSORY,
|
|
342
|
-
AudioDeviceInfo.TYPE_USB_HEADSET
|
|
343
|
-
-> "usbAudio"
|
|
323
|
+
AudioDeviceInfo.TYPE_USB_HEADSET -> "usbAudio"
|
|
344
324
|
|
|
345
325
|
AudioDeviceInfo.TYPE_LINE_ANALOG,
|
|
346
|
-
AudioDeviceInfo.TYPE_LINE_DIGITAL
|
|
347
|
-
-> "lineOut"
|
|
326
|
+
AudioDeviceInfo.TYPE_LINE_DIGITAL -> "lineOut"
|
|
348
327
|
|
|
349
328
|
else -> "android_$type"
|
|
350
329
|
}
|