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.
Files changed (19) hide show
  1. package/README.md +11 -0
  2. package/android/src/main/java/expo/modules/callkittelecom/ExpoCallKitTelecomModule.kt +254 -282
  3. package/android/src/main/java/expo/modules/callkittelecom/IncomingCallActivity.kt +27 -34
  4. package/android/src/main/java/expo/modules/callkittelecom/events/CallEventEmitter.kt +21 -32
  5. package/android/src/main/java/expo/modules/callkittelecom/managers/CallAudioManager.kt +53 -74
  6. package/android/src/main/java/expo/modules/callkittelecom/managers/CallManager.kt +95 -156
  7. package/android/src/main/java/expo/modules/callkittelecom/managers/CallNotificationManager.kt +71 -84
  8. package/android/src/main/java/expo/modules/callkittelecom/managers/CaptureSessionManager.kt +1 -3
  9. package/android/src/main/java/expo/modules/callkittelecom/managers/DialtonePlayer.kt +6 -4
  10. package/android/src/main/java/expo/modules/callkittelecom/managers/FulfillRequestManager.kt +21 -21
  11. package/android/src/main/java/expo/modules/callkittelecom/managers/VoIPPushManager.kt +3 -5
  12. package/android/src/main/java/expo/modules/callkittelecom/models/CallModels.kt +28 -36
  13. package/android/src/main/java/expo/modules/callkittelecom/services/CallNotificationReceiver.kt +7 -8
  14. package/android/src/main/java/expo/modules/callkittelecom/services/ExpoCallKitTelecomMessagingService.kt +9 -16
  15. package/android/src/main/java/expo/modules/callkittelecom/store/CallStore.kt +32 -68
  16. package/android/src/main/java/expo/modules/callkittelecom/utils/CallKitTelecomLog.kt +6 -17
  17. package/android/src/main/java/expo/modules/callkittelecom/utils/PermissionUtils.kt +7 -7
  18. package/ios/Managers/CaptureSessionManager.swift +1 -1
  19. 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
- * dismisses when the call leaves the RINGING state (answered, declined,
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
- * so the user sees the in-call UI after unlocking.
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).setImageDrawable(
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
- * On success, displays a circular-cropped image and hides the initial letter.
130
- * On failure, silently keeps the initial letter fallback.
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
- * the main Activity so the user transitions into the in-call UI.
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
- * of whether the keyguard dismissal succeeds. This matches the behavior
189
- * of iOS CallKit where audio connects before the device is unlocked.
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
- * (answered elsewhere, timed out, or ended by the remote side).
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
- * for ENDED status — the CONNECTING transition is expected and handled by
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) { "Call no longer ringing (${session.status.value}), finishing" }
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: String,
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 status.
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) { "Start observing - event: $eventName, queuedEvents: $queueCount" }
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
- queue.removeAt(0)
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) { "Queueing event (JS not listening) - name: $name, queueSize: ${queue.size}" }
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) { "Flushing event queue - event: $eventName, count: ${queue.size}" }
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
- * endpoint state, emits route changes to JS, and requests endpoint switches
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) { "Audio devices removed - count: ${removedDevices.size}" }
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 else null
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) { "restoreAudioSession is a no-op on Android (Core-Telecom manages audio)" }
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
- * Bluetooth > Wired Headset > Earpiece (audio) / Speaker (video).
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 { return it }
188
+ ?.let {
189
+ return it
190
+ }
207
191
  available
208
192
  .firstOrNull { it.type == CallEndpointCompat.TYPE_WIRED_HEADSET }
209
- ?.let { return it }
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
- * Each endpoint maps to a single output port.
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
- * the active device (matching iOS's `AVAudioSession.currentRoute` behavior).
241
- * When no call is active, falls back to built-in defaults.
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 = findDevicePort(AudioManager.GET_DEVICES_INPUTS, AudioDeviceInfo.TYPE_BUILTIN_MIC)
255
- val output = findDevicePort(AudioManager.GET_DEVICES_OUTPUTS, AudioDeviceInfo.TYPE_BUILTIN_EARPIECE)
256
- return mapOf(
257
- "inputs" to listOfNotNull(input),
258
- "outputs" to listOfNotNull(output),
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(AudioManager.GET_DEVICES_OUTPUTS, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)
245
+ findDevicePort(
246
+ AudioManager.GET_DEVICES_OUTPUTS,
247
+ AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
248
+ )
267
249
  }
268
250
 
269
251
  CallEndpointCompat.TYPE_EARPIECE -> {
270
- findDevicePort(AudioManager.GET_DEVICES_OUTPUTS, AudioDeviceInfo.TYPE_BUILTIN_EARPIECE)
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(AudioManager.GET_DEVICES_OUTPUTS, AudioDeviceInfo.TYPE_WIRED_HEADPHONES)
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: Int,
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
  }