expo-callkit-telecom 0.1.0
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/LICENSE +21 -0
- package/README.md +197 -0
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +33 -0
- package/android/src/main/java/expo/modules/callkittelecom/ExpoCallKitTelecomModule.kt +384 -0
- package/android/src/main/java/expo/modules/callkittelecom/IncomingCallActivity.kt +275 -0
- package/android/src/main/java/expo/modules/callkittelecom/events/CallEventEmitter.kt +151 -0
- package/android/src/main/java/expo/modules/callkittelecom/events/CallEvents.kt +59 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/CallAudioManager.kt +361 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/CallManager.kt +891 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/CallNotificationManager.kt +445 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/CaptureSessionManager.kt +27 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/DialtonePlayer.kt +171 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/FulfillRequestManager.kt +150 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/VoIPPushManager.kt +54 -0
- package/android/src/main/java/expo/modules/callkittelecom/models/CallModels.kt +269 -0
- package/android/src/main/java/expo/modules/callkittelecom/services/CallNotificationReceiver.kt +54 -0
- package/android/src/main/java/expo/modules/callkittelecom/services/ExpoCallKitTelecomMessagingService.kt +161 -0
- package/android/src/main/java/expo/modules/callkittelecom/store/CallStore.kt +181 -0
- package/android/src/main/java/expo/modules/callkittelecom/utils/CallKitTelecomLog.kt +52 -0
- package/android/src/main/java/expo/modules/callkittelecom/utils/PermissionUtils.kt +28 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_bg_answer.xml +9 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_bg_avatar.xml +5 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_bg_decline.xml +9 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_ic_answer.xml +9 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_ic_decline.xml +9 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_ic_videocam.xml +9 -0
- package/android/src/main/res/layout/activity_incoming_call.xml +169 -0
- package/app.json +8 -0
- package/app.plugin.js +1 -0
- package/build/Calls.d.ts +577 -0
- package/build/Calls.d.ts.map +1 -0
- package/build/Calls.js +715 -0
- package/build/Calls.js.map +1 -0
- package/build/Calls.types.d.ts +203 -0
- package/build/Calls.types.d.ts.map +1 -0
- package/build/Calls.types.js +2 -0
- package/build/Calls.types.js.map +1 -0
- package/build/ExpoCallKitTelecomModule.d.ts +3 -0
- package/build/ExpoCallKitTelecomModule.d.ts.map +1 -0
- package/build/ExpoCallKitTelecomModule.js +4 -0
- package/build/ExpoCallKitTelecomModule.js.map +1 -0
- package/build/hooks/index.d.ts +2 -0
- package/build/hooks/index.d.ts.map +1 -0
- package/build/hooks/index.js +2 -0
- package/build/hooks/index.js.map +1 -0
- package/build/hooks/useVoIPPushToken.d.ts +14 -0
- package/build/hooks/useVoIPPushToken.d.ts.map +1 -0
- package/build/hooks/useVoIPPushToken.js +26 -0
- package/build/hooks/useVoIPPushToken.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +10 -0
- package/ios/AppDelegateSubscriber.swift +93 -0
- package/ios/ExpoCallKitTelecom.podspec +31 -0
- package/ios/ExpoCallKitTelecomLogger.swift +55 -0
- package/ios/ExpoCallKitTelecomModule.swift +503 -0
- package/ios/Managers/AudioManager.swift +363 -0
- package/ios/Managers/CallEventEmitter.swift +199 -0
- package/ios/Managers/CallManager+CXProviderDelegate.swift +195 -0
- package/ios/Managers/CallManager.swift +714 -0
- package/ios/Managers/CaptureSessionManager.swift +54 -0
- package/ios/Managers/DialtonePlayer.swift +126 -0
- package/ios/Managers/FulfillRequestManager.swift +154 -0
- package/ios/Managers/VoIPPushManager+PKPushRegistryDelegate.swift +123 -0
- package/ios/Managers/VoIPPushManager.swift +58 -0
- package/ios/Models/CallEvents.swift +263 -0
- package/ios/Models/CallOptions.swift +15 -0
- package/ios/Models/CallParticipant.swift +37 -0
- package/ios/Models/CallSession.swift +80 -0
- package/ios/Models/IncomingCallEvent.swift +196 -0
- package/ios/Stores/CallStore.swift +149 -0
- package/package.json +56 -0
- package/plugin/build/constants.d.ts +3 -0
- package/plugin/build/constants.js +7 -0
- package/plugin/build/withExpoCallKitTelecom.d.ts +67 -0
- package/plugin/build/withExpoCallKitTelecom.js +16 -0
- package/plugin/build/withExpoCallKitTelecomAndroid.d.ts +3 -0
- package/plugin/build/withExpoCallKitTelecomAndroid.js +177 -0
- package/plugin/build/withExpoCallKitTelecomIos.d.ts +3 -0
- package/plugin/build/withExpoCallKitTelecomIos.js +195 -0
- package/plugin/src/constants.ts +4 -0
- package/plugin/src/withExpoCallKitTelecom.ts +83 -0
- package/plugin/src/withExpoCallKitTelecomAndroid.ts +293 -0
- package/plugin/src/withExpoCallKitTelecomIos.ts +276 -0
- package/src/Calls.ts +848 -0
- package/src/Calls.types.ts +275 -0
- package/src/ExpoCallKitTelecomModule.ts +4 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useVoIPPushToken.ts +34 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
package expo.modules.callkittelecom.managers
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.media.AudioDeviceCallback
|
|
5
|
+
import android.media.AudioDeviceInfo
|
|
6
|
+
import android.media.AudioManager
|
|
7
|
+
import androidx.core.telecom.CallEndpointCompat
|
|
8
|
+
import expo.modules.callkittelecom.events.CallEventEmitter
|
|
9
|
+
import expo.modules.callkittelecom.events.CallEvents
|
|
10
|
+
import expo.modules.callkittelecom.models.CallSession
|
|
11
|
+
import expo.modules.callkittelecom.utils.CallKitTelecomLog
|
|
12
|
+
import expo.modules.callkittelecom.utils.PermissionUtils
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Manages Android call audio state and routing for the shared calls API.
|
|
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.
|
|
20
|
+
*/
|
|
21
|
+
object CallAudioManager {
|
|
22
|
+
private const val TAG = "ExpoCallKitTelecom.Audio"
|
|
23
|
+
|
|
24
|
+
private lateinit var context: Context
|
|
25
|
+
private lateinit var audioManager: AudioManager
|
|
26
|
+
|
|
27
|
+
private var isInitialized = false
|
|
28
|
+
internal var isActive = false
|
|
29
|
+
private var configuredForVideo = false
|
|
30
|
+
|
|
31
|
+
/** Callback to request endpoint change on the active Core-Telecom call scope. */
|
|
32
|
+
internal var onRequestEndpointChange: ((CallEndpointCompat) -> Unit)? = null
|
|
33
|
+
|
|
34
|
+
/** Last known active endpoint from Core-Telecom. */
|
|
35
|
+
private var currentEndpoint: CallEndpointCompat? = null
|
|
36
|
+
|
|
37
|
+
/** Last known available endpoints from Core-Telecom. */
|
|
38
|
+
internal var currentAvailableEndpoints: List<CallEndpointCompat> = emptyList()
|
|
39
|
+
|
|
40
|
+
private var routeCallback: AudioDeviceCallback? = null
|
|
41
|
+
|
|
42
|
+
/** Initializes audio manager and route-change observation. Safe to call repeatedly. */
|
|
43
|
+
fun initialize(appContext: Context) {
|
|
44
|
+
if (isInitialized) return
|
|
45
|
+
|
|
46
|
+
context = appContext.applicationContext
|
|
47
|
+
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
48
|
+
|
|
49
|
+
routeCallback =
|
|
50
|
+
object : AudioDeviceCallback() {
|
|
51
|
+
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
|
|
52
|
+
CallKitTelecomLog.d(TAG) { "Audio devices added - count: ${addedDevices.size}" }
|
|
53
|
+
// During active calls, Core-Telecom handles routing via endpoint Flows
|
|
54
|
+
if (!isActive) emitRouteChanged()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
|
|
58
|
+
CallKitTelecomLog.d(TAG) { "Audio devices removed - count: ${removedDevices.size}" }
|
|
59
|
+
if (!isActive) emitRouteChanged()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
routeCallback?.let { callback ->
|
|
64
|
+
audioManager.registerAudioDeviceCallback(callback, null)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
isInitialized = true
|
|
68
|
+
CallKitTelecomLog.d(TAG) { "Initialized CallAudioManager" }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Receives Core-Telecom current endpoint updates from the active call scope. */
|
|
72
|
+
fun onEndpointChanged(endpoint: CallEndpointCompat) {
|
|
73
|
+
currentEndpoint = endpoint
|
|
74
|
+
emitRouteChanged()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Receives Core-Telecom available endpoint updates from the active call scope. */
|
|
78
|
+
fun onAvailableEndpointsChanged(endpoints: List<CallEndpointCompat>) {
|
|
79
|
+
currentAvailableEndpoints = endpoints
|
|
80
|
+
emitRouteChanged()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Returns current audio session state in the shared TypeScript shape. */
|
|
84
|
+
fun getAudioSessionState(): Map<String, Any?> {
|
|
85
|
+
val sampleRate =
|
|
86
|
+
audioManager
|
|
87
|
+
.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)
|
|
88
|
+
?.toDoubleOrNull()
|
|
89
|
+
val framesPerBuffer =
|
|
90
|
+
audioManager
|
|
91
|
+
.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)
|
|
92
|
+
?.toDoubleOrNull()
|
|
93
|
+
val ioBufferDuration =
|
|
94
|
+
if (sampleRate != null && framesPerBuffer != null) framesPerBuffer / sampleRate else null
|
|
95
|
+
|
|
96
|
+
val currentRoute = currentRouteMap()
|
|
97
|
+
|
|
98
|
+
return mapOf(
|
|
99
|
+
"isActive" to isActive,
|
|
100
|
+
"isOtherAudioPlaying" to audioManager.isMusicActive,
|
|
101
|
+
"category" to "communication",
|
|
102
|
+
"mode" to modeString(audioManager.mode),
|
|
103
|
+
"sampleRate" to sampleRate,
|
|
104
|
+
"ioBufferDuration" to ioBufferDuration,
|
|
105
|
+
"inputNumberOfChannels" to 1,
|
|
106
|
+
"outputNumberOfChannels" to 2,
|
|
107
|
+
"microphonePermission" to PermissionUtils.microphonePermission(context),
|
|
108
|
+
"currentRoute" to currentRoute,
|
|
109
|
+
"availableRoutes" to availableRoutesMap(),
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Records whether the upcoming call uses video (affects default endpoint selection). */
|
|
114
|
+
fun prepareAudioSessionForCall(hasVideo: Boolean) {
|
|
115
|
+
CallKitTelecomLog.d(TAG) { "Preparing audio session for call - hasVideo: $hasVideo" }
|
|
116
|
+
configuredForVideo = hasVideo
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** No-op on Android — Core-Telecom manages audio mode and focus. Kept for JS API compat. */
|
|
120
|
+
fun restoreAudioSession() {
|
|
121
|
+
CallKitTelecomLog.d(TAG) { "restoreAudioSession is a no-op on Android (Core-Telecom manages audio)" }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Marks audio session as active and emits `onAudioSessionActivated`.
|
|
126
|
+
*
|
|
127
|
+
* Core-Telecom handles audio focus and mode automatically.
|
|
128
|
+
*/
|
|
129
|
+
fun onAudioActivated(calls: List<CallSession>) {
|
|
130
|
+
if (!isInitialized) return
|
|
131
|
+
|
|
132
|
+
CallKitTelecomLog.d(TAG) { "Activating audio session - calls: ${calls.size}" }
|
|
133
|
+
|
|
134
|
+
isActive = true
|
|
135
|
+
|
|
136
|
+
val callInfos =
|
|
137
|
+
calls.map {
|
|
138
|
+
mapOf(
|
|
139
|
+
"id" to it.id.toString(),
|
|
140
|
+
"status" to it.status.value,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
CallEventEmitter.send(
|
|
145
|
+
CallEvents.AUDIO_SESSION_ACTIVATED,
|
|
146
|
+
mapOf("calls" to callInfos),
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Resets audio state after final call teardown and emits `onAudioSessionDeactivated`.
|
|
152
|
+
*
|
|
153
|
+
* Core-Telecom releases audio focus and restores mode when the call scope ends.
|
|
154
|
+
*/
|
|
155
|
+
fun onAudioDeactivated(calls: List<CallSession>) {
|
|
156
|
+
if (!isInitialized) return
|
|
157
|
+
|
|
158
|
+
DialtonePlayer.stop()
|
|
159
|
+
CallKitTelecomLog.d(TAG) { "Deactivating audio session - calls: ${calls.size}" }
|
|
160
|
+
|
|
161
|
+
currentEndpoint = null
|
|
162
|
+
currentAvailableEndpoints = emptyList()
|
|
163
|
+
isActive = false
|
|
164
|
+
|
|
165
|
+
emitRouteChanged()
|
|
166
|
+
|
|
167
|
+
val callInfos =
|
|
168
|
+
calls.map {
|
|
169
|
+
mapOf(
|
|
170
|
+
"id" to it.id.toString(),
|
|
171
|
+
"status" to it.status.value,
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
CallEventEmitter.send(
|
|
176
|
+
CallEvents.AUDIO_SESSION_DEACTIVATED,
|
|
177
|
+
mapOf("calls" to callInfos),
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Requests endpoint change to speaker (`true`) or best non-speaker device (`false`). */
|
|
182
|
+
fun setAudioSessionPortOverride(enabled: Boolean) {
|
|
183
|
+
CallKitTelecomLog.d(TAG) { "Setting speaker - enabled: $enabled" }
|
|
184
|
+
|
|
185
|
+
val endpoint =
|
|
186
|
+
if (enabled) {
|
|
187
|
+
currentAvailableEndpoints.firstOrNull { it.type == CallEndpointCompat.TYPE_SPEAKER }
|
|
188
|
+
} else {
|
|
189
|
+
selectBestNonSpeakerEndpoint()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (endpoint != null) {
|
|
193
|
+
onRequestEndpointChange?.invoke(endpoint)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Selects the best non-speaker endpoint matching iOS priority:
|
|
199
|
+
* Bluetooth > Wired Headset > Earpiece (audio) / Speaker (video).
|
|
200
|
+
*/
|
|
201
|
+
private fun selectBestNonSpeakerEndpoint(): CallEndpointCompat? {
|
|
202
|
+
val available = currentAvailableEndpoints
|
|
203
|
+
|
|
204
|
+
available
|
|
205
|
+
.firstOrNull { it.type == CallEndpointCompat.TYPE_BLUETOOTH }
|
|
206
|
+
?.let { return it }
|
|
207
|
+
available
|
|
208
|
+
.firstOrNull { it.type == CallEndpointCompat.TYPE_WIRED_HEADSET }
|
|
209
|
+
?.let { return it }
|
|
210
|
+
|
|
211
|
+
return if (configuredForVideo) {
|
|
212
|
+
available.firstOrNull { it.type == CallEndpointCompat.TYPE_SPEAKER }
|
|
213
|
+
} else {
|
|
214
|
+
available.firstOrNull { it.type == CallEndpointCompat.TYPE_EARPIECE }
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Emits the current route and available routes snapshot to JS listeners. */
|
|
219
|
+
private fun emitRouteChanged() {
|
|
220
|
+
CallEventEmitter.send(
|
|
221
|
+
CallEvents.AUDIO_ROUTE_CHANGED,
|
|
222
|
+
mapOf(
|
|
223
|
+
"currentRoute" to currentRouteMap(),
|
|
224
|
+
"availableRoutes" to availableRoutesMap(),
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Converts Core-Telecom available endpoints into JS-facing AudioPort objects.
|
|
231
|
+
* Each endpoint maps to a single output port.
|
|
232
|
+
*/
|
|
233
|
+
private fun availableRoutesMap(): List<Map<String, String>> =
|
|
234
|
+
currentAvailableEndpoints.mapNotNull { endpoint -> resolveOutputPort(endpoint) }
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Builds the current route payload with the single active input/output device.
|
|
238
|
+
*
|
|
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.
|
|
242
|
+
*/
|
|
243
|
+
private fun currentRouteMap(): Map<String, Any?> {
|
|
244
|
+
val endpoint = currentEndpoint
|
|
245
|
+
if (endpoint != null) {
|
|
246
|
+
val output = resolveOutputPort(endpoint)
|
|
247
|
+
val input = resolveInputPort(endpoint.type)
|
|
248
|
+
return mapOf(
|
|
249
|
+
"inputs" to listOfNotNull(input),
|
|
250
|
+
"outputs" to listOfNotNull(output),
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
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
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Maps Core-Telecom endpoint to a port map for the JS layer. */
|
|
263
|
+
private fun resolveOutputPort(endpoint: CallEndpointCompat): Map<String, String>? =
|
|
264
|
+
when (endpoint.type) {
|
|
265
|
+
CallEndpointCompat.TYPE_SPEAKER -> {
|
|
266
|
+
findDevicePort(AudioManager.GET_DEVICES_OUTPUTS, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
CallEndpointCompat.TYPE_EARPIECE -> {
|
|
270
|
+
findDevicePort(AudioManager.GET_DEVICES_OUTPUTS, AudioDeviceInfo.TYPE_BUILTIN_EARPIECE)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
CallEndpointCompat.TYPE_BLUETOOTH -> {
|
|
274
|
+
mapOf(
|
|
275
|
+
"portType" to "bluetoothHFP",
|
|
276
|
+
"portName" to endpoint.name.toString(),
|
|
277
|
+
"uid" to endpoint.identifier.toString(),
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
CallEndpointCompat.TYPE_WIRED_HEADSET -> {
|
|
282
|
+
findDevicePort(AudioManager.GET_DEVICES_OUTPUTS, AudioDeviceInfo.TYPE_WIRED_HEADSET)
|
|
283
|
+
?: findDevicePort(AudioManager.GET_DEVICES_OUTPUTS, AudioDeviceInfo.TYPE_WIRED_HEADPHONES)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
else -> {
|
|
287
|
+
null
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Infers the active input device based on the endpoint type. */
|
|
292
|
+
private fun resolveInputPort(endpointType: Int): Map<String, String>? {
|
|
293
|
+
val inputType =
|
|
294
|
+
when (endpointType) {
|
|
295
|
+
CallEndpointCompat.TYPE_BLUETOOTH -> AudioDeviceInfo.TYPE_BLUETOOTH_SCO
|
|
296
|
+
CallEndpointCompat.TYPE_WIRED_HEADSET -> AudioDeviceInfo.TYPE_WIRED_HEADSET
|
|
297
|
+
else -> AudioDeviceInfo.TYPE_BUILTIN_MIC
|
|
298
|
+
}
|
|
299
|
+
return findDevicePort(AudioManager.GET_DEVICES_INPUTS, inputType)
|
|
300
|
+
?: findDevicePort(AudioManager.GET_DEVICES_INPUTS, AudioDeviceInfo.TYPE_BUILTIN_MIC)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** 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) }
|
|
312
|
+
|
|
313
|
+
/** Serializes an Android audio device into the shared audio port payload shape. */
|
|
314
|
+
private fun portMap(info: AudioDeviceInfo): Map<String, String> =
|
|
315
|
+
mapOf(
|
|
316
|
+
"portType" to portTypeString(info.type),
|
|
317
|
+
"portName" to info.productName?.toString().orEmpty(),
|
|
318
|
+
"uid" to info.id.toString(),
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
/** Maps Android audio device constants to cross-platform port type identifiers. */
|
|
322
|
+
private fun portTypeString(type: Int): String =
|
|
323
|
+
when (type) {
|
|
324
|
+
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> "builtInReceiver"
|
|
325
|
+
|
|
326
|
+
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "builtInSpeaker"
|
|
327
|
+
|
|
328
|
+
AudioDeviceInfo.TYPE_WIRED_HEADSET,
|
|
329
|
+
AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
|
|
330
|
+
-> "headphones"
|
|
331
|
+
|
|
332
|
+
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> "bluetoothA2DP"
|
|
333
|
+
|
|
334
|
+
AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "bluetoothHFP"
|
|
335
|
+
|
|
336
|
+
AudioDeviceInfo.TYPE_BLE_HEADSET -> "bluetoothLE"
|
|
337
|
+
|
|
338
|
+
AudioDeviceInfo.TYPE_HDMI -> "hdmi"
|
|
339
|
+
|
|
340
|
+
AudioDeviceInfo.TYPE_USB_DEVICE,
|
|
341
|
+
AudioDeviceInfo.TYPE_USB_ACCESSORY,
|
|
342
|
+
AudioDeviceInfo.TYPE_USB_HEADSET,
|
|
343
|
+
-> "usbAudio"
|
|
344
|
+
|
|
345
|
+
AudioDeviceInfo.TYPE_LINE_ANALOG,
|
|
346
|
+
AudioDeviceInfo.TYPE_LINE_DIGITAL,
|
|
347
|
+
-> "lineOut"
|
|
348
|
+
|
|
349
|
+
else -> "android_$type"
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Maps Android audio mode constants to readable string values for diagnostics. */
|
|
353
|
+
private fun modeString(mode: Int): String =
|
|
354
|
+
when (mode) {
|
|
355
|
+
AudioManager.MODE_IN_COMMUNICATION -> "inCommunication"
|
|
356
|
+
AudioManager.MODE_IN_CALL -> "inCall"
|
|
357
|
+
AudioManager.MODE_NORMAL -> "normal"
|
|
358
|
+
AudioManager.MODE_RINGTONE -> "ringtone"
|
|
359
|
+
else -> "unknown"
|
|
360
|
+
}
|
|
361
|
+
}
|