react-native-telecom 0.0.0-stage → 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.
Files changed (57) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +309 -2
  3. package/Telecom.podspec +20 -0
  4. package/android/build.gradle +58 -0
  5. package/android/src/main/AndroidManifest.xml +43 -0
  6. package/android/src/main/java/com/telecom/CallSession.kt +37 -0
  7. package/android/src/main/java/com/telecom/CallSessionManager.kt +486 -0
  8. package/android/src/main/java/com/telecom/TelecomAudioRouter.kt +227 -0
  9. package/android/src/main/java/com/telecom/TelecomAvatarLoader.kt +77 -0
  10. package/android/src/main/java/com/telecom/TelecomCallService.kt +395 -0
  11. package/android/src/main/java/com/telecom/TelecomCallUi.kt +65 -0
  12. package/android/src/main/java/com/telecom/TelecomInCallActivity.kt +285 -0
  13. package/android/src/main/java/com/telecom/TelecomIncomingCallActivity.kt +171 -0
  14. package/android/src/main/java/com/telecom/TelecomModule.kt +189 -0
  15. package/android/src/main/java/com/telecom/TelecomPackage.kt +31 -0
  16. package/android/src/main/java/com/telecom/TelecomPushBridge.kt +82 -0
  17. package/android/src/main/java/com/telecom/TelecomRingtonePlayer.kt +109 -0
  18. package/android/src/main/java/com/telecom/TelecomUiConfig.kt +27 -0
  19. package/android/src/main/res/drawable/rn_telecom_action_pill.xml +6 -0
  20. package/android/src/main/res/drawable/rn_telecom_answer_circle.xml +8 -0
  21. package/android/src/main/res/drawable/rn_telecom_answer_circle_green.xml +8 -0
  22. package/android/src/main/res/drawable/rn_telecom_avatar_bg.xml +6 -0
  23. package/android/src/main/res/drawable/rn_telecom_avatar_circle.xml +12 -0
  24. package/android/src/main/res/drawable/rn_telecom_btn_answer.xml +6 -0
  25. package/android/src/main/res/drawable/rn_telecom_btn_decline.xml +6 -0
  26. package/android/src/main/res/drawable/rn_telecom_btn_message.xml +9 -0
  27. package/android/src/main/res/drawable/rn_telecom_btn_secondary.xml +6 -0
  28. package/android/src/main/res/drawable/rn_telecom_control_circle.xml +8 -0
  29. package/android/src/main/res/drawable/rn_telecom_control_circle_on.xml +8 -0
  30. package/android/src/main/res/drawable/rn_telecom_decline_circle.xml +8 -0
  31. package/android/src/main/res/drawable/rn_telecom_ic_call.xml +10 -0
  32. package/android/src/main/res/drawable/rn_telecom_ic_call_end.xml +16 -0
  33. package/android/src/main/res/drawable/rn_telecom_ic_call_white.xml +10 -0
  34. package/android/src/main/res/drawable/rn_telecom_ic_dialpad.xml +13 -0
  35. package/android/src/main/res/drawable/rn_telecom_ic_hold.xml +10 -0
  36. package/android/src/main/res/drawable/rn_telecom_ic_message.xml +10 -0
  37. package/android/src/main/res/drawable/rn_telecom_ic_mic.xml +10 -0
  38. package/android/src/main/res/drawable/rn_telecom_ic_mic_off.xml +10 -0
  39. package/android/src/main/res/drawable/rn_telecom_ic_speaker.xml +10 -0
  40. package/android/src/main/res/layout/rn_telecom_control_item.xml +36 -0
  41. package/android/src/main/res/layout/rn_telecom_incall.xml +168 -0
  42. package/android/src/main/res/layout/rn_telecom_incoming_call.xml +188 -0
  43. package/ios/Telecom.h +5 -0
  44. package/ios/Telecom.mm +66 -0
  45. package/lib/module/NativeTelecom.js +5 -0
  46. package/lib/module/NativeTelecom.js.map +1 -0
  47. package/lib/module/index.js +102 -0
  48. package/lib/module/index.js.map +1 -0
  49. package/lib/module/package.json +1 -0
  50. package/lib/typescript/package.json +1 -0
  51. package/lib/typescript/src/NativeTelecom.d.ts +40 -0
  52. package/lib/typescript/src/NativeTelecom.d.ts.map +1 -0
  53. package/lib/typescript/src/index.d.ts +104 -0
  54. package/lib/typescript/src/index.d.ts.map +1 -0
  55. package/package.json +153 -4
  56. package/src/NativeTelecom.ts +55 -0
  57. package/src/index.tsx +220 -0
@@ -0,0 +1,285 @@
1
+ package com.telecom
2
+
3
+ import android.app.Activity
4
+ import android.content.BroadcastReceiver
5
+ import android.graphics.Color
6
+ import android.graphics.Outline
7
+ import android.graphics.drawable.GradientDrawable
8
+ import android.media.AudioManager
9
+ import android.media.ToneGenerator
10
+ import android.os.Build
11
+ import android.os.Bundle
12
+ import android.view.View
13
+ import android.view.ViewOutlineProvider
14
+ import android.view.WindowManager
15
+ import android.widget.FrameLayout
16
+ import android.widget.GridLayout
17
+ import android.widget.ImageView
18
+ import android.widget.TextView
19
+ import androidx.core.view.WindowCompat
20
+ import androidx.core.view.WindowInsetsControllerCompat
21
+
22
+ /**
23
+ * Outgoing + answered in-call UI (Material You style).
24
+ * Override [R.layout.rn_telecom_incall] in the host app to restyle.
25
+ */
26
+ class TelecomInCallActivity : Activity() {
27
+ private var uuid: String = ""
28
+ private var direction: String = "outgoing"
29
+ private var muted = false
30
+ private var onHold = false
31
+ private var keypadVisible = false
32
+ private var audioRoute = "earpiece"
33
+ private var toneGenerator: ToneGenerator? = null
34
+ private var finishReceiver: BroadcastReceiver? = null
35
+
36
+ private lateinit var statusView: TextView
37
+ private lateinit var muteControl: View
38
+ private lateinit var keypadControl: View
39
+ private lateinit var speakerControl: View
40
+ private lateinit var holdControl: View
41
+ private lateinit var keypad: GridLayout
42
+ private lateinit var controlsRow: View
43
+ private lateinit var avatar: View
44
+
45
+ override fun onCreate(savedInstanceState: Bundle?) {
46
+ super.onCreate(savedInstanceState)
47
+ applyLightChrome()
48
+ setContentView(R.layout.rn_telecom_incall)
49
+
50
+ uuid = intent.getStringExtra(TelecomCallService.EXTRA_UUID).orEmpty()
51
+ val callerName =
52
+ intent.getStringExtra(TelecomCallService.EXTRA_CALLER_NAME) ?: "Unknown"
53
+ val handle = intent.getStringExtra(TelecomCallService.EXTRA_HANDLE).orEmpty()
54
+ direction = intent.getStringExtra(TelecomCallService.EXTRA_DIRECTION) ?: "outgoing"
55
+ val avatarUrl = intent.getStringExtra(TelecomCallService.EXTRA_AVATAR_URL).orEmpty()
56
+
57
+ finishReceiver =
58
+ TelecomCallUi.registerFinishReceiver(
59
+ this,
60
+ TelecomCallUi.ACTION_FINISH_INCALL,
61
+ uuid,
62
+ ) {
63
+ if (!isFinishing) finish()
64
+ }
65
+
66
+ statusView = findViewById(R.id.rn_telecom_incall_status)
67
+ statusView.text = if (direction == "incoming") "Connected" else "Calling…"
68
+ findViewById<TextView>(R.id.rn_telecom_incall_name).text = callerName
69
+ findViewById<TextView>(R.id.rn_telecom_incall_handle).text =
70
+ if (handle.isBlank()) "" else "Mobile · $handle"
71
+ avatar = findViewById(R.id.rn_telecom_incall_avatar)
72
+ avatar.post { clipCircle(avatar) }
73
+ TelecomAvatarLoader.bind(
74
+ imageView = findViewById(R.id.rn_telecom_incall_avatar_image),
75
+ initialsView = findViewById(R.id.rn_telecom_incall_initials),
76
+ avatarUrl = avatarUrl,
77
+ callerName = callerName,
78
+ )
79
+
80
+ controlsRow = findViewById(R.id.rn_telecom_controls)
81
+ keypad = findViewById(R.id.rn_telecom_keypad)
82
+
83
+ muteControl = findViewById(R.id.rn_telecom_btn_mute)
84
+ keypadControl = findViewById(R.id.rn_telecom_btn_keypad)
85
+ speakerControl = findViewById(R.id.rn_telecom_btn_speaker)
86
+ holdControl = findViewById(R.id.rn_telecom_btn_hold)
87
+
88
+ bindControl(muteControl, R.drawable.rn_telecom_ic_mic, "Mute")
89
+ bindControl(keypadControl, R.drawable.rn_telecom_ic_dialpad, "Keypad")
90
+ bindControl(speakerControl, R.drawable.rn_telecom_ic_speaker, "Audio")
91
+ bindControl(holdControl, R.drawable.rn_telecom_ic_hold, "Hold")
92
+ audioRoute = CallSessionManager.instance?.currentAudioRoute() ?: "earpiece"
93
+ refreshAudioRouteLabel()
94
+
95
+ buildKeypad(keypad)
96
+
97
+ muteControl.setOnClickListener {
98
+ muted = !muted
99
+ CallSessionManager.instance?.setMuted(uuid, muted)
100
+ setControlActive(
101
+ muteControl,
102
+ muted,
103
+ if (muted) R.drawable.rn_telecom_ic_mic_off else R.drawable.rn_telecom_ic_mic,
104
+ if (muted) "Unmute" else "Mute",
105
+ )
106
+ }
107
+
108
+ speakerControl.setOnClickListener {
109
+ audioRoute = CallSessionManager.instance?.cycleAudioRoute(uuid) ?: audioRoute
110
+ refreshAudioRouteLabel()
111
+ }
112
+
113
+ holdControl.setOnClickListener {
114
+ onHold = !onHold
115
+ CallSessionManager.instance?.setOnHold(uuid, onHold)
116
+ setControlActive(
117
+ holdControl,
118
+ onHold,
119
+ R.drawable.rn_telecom_ic_hold,
120
+ if (onHold) "Resume" else "Hold",
121
+ )
122
+ statusView.text =
123
+ when {
124
+ onHold -> "On hold"
125
+ direction == "outgoing" && !onHold -> "Connected"
126
+ else -> "Connected"
127
+ }
128
+ }
129
+
130
+ keypadControl.setOnClickListener {
131
+ keypadVisible = !keypadVisible
132
+ keypad.visibility = if (keypadVisible) View.VISIBLE else View.GONE
133
+ avatar.visibility = if (keypadVisible) View.GONE else View.VISIBLE
134
+ setControlActive(
135
+ keypadControl,
136
+ keypadVisible,
137
+ R.drawable.rn_telecom_ic_dialpad,
138
+ if (keypadVisible) "Hide" else "Keypad",
139
+ )
140
+ }
141
+
142
+ findViewById<View>(R.id.rn_telecom_btn_end).setOnClickListener {
143
+ CallSessionManager.instance?.endCall(uuid)
144
+ finish()
145
+ }
146
+
147
+ try {
148
+ toneGenerator = ToneGenerator(AudioManager.STREAM_DTMF, 80)
149
+ } catch (_: Exception) {
150
+ toneGenerator = null
151
+ }
152
+ }
153
+
154
+ private fun refreshAudioRouteLabel() {
155
+ val label =
156
+ when (audioRoute) {
157
+ "speaker" -> "Speaker"
158
+ "bluetooth" -> "Bluetooth"
159
+ else -> "Phone"
160
+ }
161
+ setControlActive(
162
+ speakerControl,
163
+ audioRoute != "earpiece",
164
+ R.drawable.rn_telecom_ic_speaker,
165
+ label,
166
+ )
167
+ }
168
+
169
+ private fun clipCircle(view: View) {
170
+ view.outlineProvider =
171
+ object : ViewOutlineProvider() {
172
+ override fun getOutline(v: View, outline: Outline) {
173
+ outline.setOval(0, 0, v.width, v.height)
174
+ }
175
+ }
176
+ view.clipToOutline = true
177
+ }
178
+
179
+ private fun applyLightChrome() {
180
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
181
+ setShowWhenLocked(true)
182
+ setTurnScreenOn(true)
183
+ }
184
+ @Suppress("DEPRECATION")
185
+ window.addFlags(
186
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
187
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
188
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
189
+ )
190
+ WindowCompat.setDecorFitsSystemWindows(window, true)
191
+ window.statusBarColor = Color.TRANSPARENT
192
+ window.navigationBarColor = Color.TRANSPARENT
193
+ WindowInsetsControllerCompat(window, window.decorView).apply {
194
+ isAppearanceLightStatusBars = true
195
+ isAppearanceLightNavigationBars = true
196
+ }
197
+ }
198
+
199
+ private fun bindControl(root: View, iconRes: Int, label: String) {
200
+ val icon = root.findViewById<ImageView>(R.id.rn_telecom_control_icon)
201
+ icon.setImageResource(iconRes)
202
+ icon.clearColorFilter()
203
+ root.findViewById<TextView>(R.id.rn_telecom_control_label).text = label
204
+ root.findViewById<FrameLayout>(R.id.rn_telecom_control_icon_bg)
205
+ .setBackgroundResource(R.drawable.rn_telecom_control_circle)
206
+ }
207
+
208
+ private fun setControlActive(root: View, active: Boolean, iconRes: Int, label: String) {
209
+ val bg = root.findViewById<FrameLayout>(R.id.rn_telecom_control_icon_bg)
210
+ val icon = root.findViewById<ImageView>(R.id.rn_telecom_control_icon)
211
+ val text = root.findViewById<TextView>(R.id.rn_telecom_control_label)
212
+ bg.setBackgroundResource(
213
+ if (active) R.drawable.rn_telecom_control_circle_on else R.drawable.rn_telecom_control_circle,
214
+ )
215
+ icon.setImageResource(iconRes)
216
+ icon.setColorFilter(if (active) Color.WHITE else Color.parseColor("#1C1B1F"))
217
+ text.text = label
218
+ }
219
+
220
+ private fun buildKeypad(grid: GridLayout) {
221
+ val keys = listOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "0", "#")
222
+ keys.forEach { key ->
223
+ val button =
224
+ TextView(this).apply {
225
+ text = key
226
+ textSize = 22f
227
+ setTextColor(Color.parseColor("#1C1B1F"))
228
+ gravity = android.view.Gravity.CENTER
229
+ background =
230
+ GradientDrawable().apply {
231
+ shape = GradientDrawable.OVAL
232
+ setColor(Color.parseColor("#E8E4DE"))
233
+ }
234
+ setOnClickListener { onDtmf(key) }
235
+ }
236
+ val size = (64 * resources.displayMetrics.density).toInt()
237
+ val lp =
238
+ GridLayout.LayoutParams().apply {
239
+ width = 0
240
+ height = size
241
+ columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f)
242
+ setMargins(10, 10, 10, 10)
243
+ }
244
+ grid.addView(button, lp)
245
+ }
246
+ grid.alignmentMode = GridLayout.ALIGN_BOUNDS
247
+ }
248
+
249
+ private fun onDtmf(digit: String) {
250
+ val tone =
251
+ when (digit) {
252
+ "0" -> ToneGenerator.TONE_DTMF_0
253
+ "1" -> ToneGenerator.TONE_DTMF_1
254
+ "2" -> ToneGenerator.TONE_DTMF_2
255
+ "3" -> ToneGenerator.TONE_DTMF_3
256
+ "4" -> ToneGenerator.TONE_DTMF_4
257
+ "5" -> ToneGenerator.TONE_DTMF_5
258
+ "6" -> ToneGenerator.TONE_DTMF_6
259
+ "7" -> ToneGenerator.TONE_DTMF_7
260
+ "8" -> ToneGenerator.TONE_DTMF_8
261
+ "9" -> ToneGenerator.TONE_DTMF_9
262
+ "*" -> ToneGenerator.TONE_DTMF_S
263
+ "#" -> ToneGenerator.TONE_DTMF_P
264
+ else -> return
265
+ }
266
+ try {
267
+ toneGenerator?.startTone(tone, 150)
268
+ } catch (_: Exception) {
269
+ }
270
+ CallSessionManager.instance?.emitDtmf(uuid, digit)
271
+ }
272
+
273
+ override fun onDestroy() {
274
+ finishReceiver?.let {
275
+ try {
276
+ unregisterReceiver(it)
277
+ } catch (_: Exception) {
278
+ }
279
+ }
280
+ finishReceiver = null
281
+ toneGenerator?.release()
282
+ toneGenerator = null
283
+ super.onDestroy()
284
+ }
285
+ }
@@ -0,0 +1,171 @@
1
+ package com.telecom
2
+
3
+ import android.app.Activity
4
+ import android.app.KeyguardManager
5
+ import android.content.BroadcastReceiver
6
+ import android.content.Intent
7
+ import android.graphics.Color
8
+ import android.graphics.Outline
9
+ import android.net.Uri
10
+ import android.os.Build
11
+ import android.os.Bundle
12
+ import android.view.View
13
+ import android.view.ViewOutlineProvider
14
+ import android.view.WindowManager
15
+ import android.widget.FrameLayout
16
+ import android.widget.ImageView
17
+ import android.widget.TextView
18
+ import androidx.core.view.WindowCompat
19
+ import androidx.core.view.WindowInsetsControllerCompat
20
+
21
+ /**
22
+ * Full-screen incoming host (Pixel-style layout + clear Decline / Answer taps).
23
+ * Shows over the lock screen via showWhenLocked + turnScreenOn.
24
+ */
25
+ class TelecomIncomingCallActivity : Activity() {
26
+ private var uuid: String = ""
27
+ private var handle: String = ""
28
+ private var finishReceiver: BroadcastReceiver? = null
29
+
30
+ override fun onCreate(savedInstanceState: Bundle?) {
31
+ super.onCreate(savedInstanceState)
32
+ applyLockScreenFlags()
33
+ setContentView(R.layout.rn_telecom_incoming_call)
34
+
35
+ val cfg = TelecomUiConfig.current
36
+ uuid = intent.getStringExtra(TelecomCallService.EXTRA_UUID).orEmpty()
37
+ val callerName =
38
+ intent.getStringExtra(TelecomCallService.EXTRA_CALLER_NAME) ?: "Unknown"
39
+ handle = intent.getStringExtra(TelecomCallService.EXTRA_HANDLE).orEmpty()
40
+ val avatarUrl = intent.getStringExtra(TelecomCallService.EXTRA_AVATAR_URL).orEmpty()
41
+
42
+ finishReceiver =
43
+ TelecomCallUi.registerFinishReceiver(
44
+ this,
45
+ TelecomCallUi.ACTION_FINISH_INCOMING,
46
+ uuid,
47
+ ) {
48
+ if (!isFinishing) finish()
49
+ }
50
+
51
+ findViewById<FrameLayout>(R.id.rn_telecom_root).setBackgroundColor(cfg.backgroundColor)
52
+
53
+ findViewById<TextView>(R.id.rn_telecom_caller_name).apply {
54
+ text = callerName
55
+ setTextColor(cfg.nameColor)
56
+ }
57
+
58
+ findViewById<TextView>(R.id.rn_telecom_handle).apply {
59
+ text =
60
+ when {
61
+ handle.isBlank() -> cfg.incomingTitle
62
+ else -> "Mobile · $handle"
63
+ }
64
+ setTextColor(cfg.handleColor)
65
+ }
66
+
67
+ findViewById<TextView>(R.id.rn_telecom_title).apply {
68
+ text = cfg.incomingTitle
69
+ setTextColor(cfg.titleColor)
70
+ }
71
+
72
+ val avatarHost =
73
+ findViewById<TextView>(R.id.rn_telecom_avatar_initials).parent as View
74
+ avatarHost.post { clipCircle(avatarHost) }
75
+ TelecomAvatarLoader.bind(
76
+ imageView = findViewById(R.id.rn_telecom_avatar_image),
77
+ initialsView = findViewById(R.id.rn_telecom_avatar_initials),
78
+ avatarUrl = avatarUrl,
79
+ callerName = callerName,
80
+ )
81
+
82
+ findViewById<TextView>(R.id.rn_telecom_message).apply {
83
+ text = cfg.messageLabel
84
+ setCompoundDrawablesRelativeWithIntrinsicBounds(
85
+ R.drawable.rn_telecom_ic_message,
86
+ 0,
87
+ 0,
88
+ 0,
89
+ )
90
+ setOnClickListener { openMessage() }
91
+ }
92
+
93
+ findViewById<TextView>(R.id.rn_telecom_decline_label).text = cfg.declineLabel
94
+ findViewById<TextView>(R.id.rn_telecom_answer_label).text = cfg.answerLabel
95
+
96
+ findViewById<View>(R.id.rn_telecom_decline).setOnClickListener { onDecline() }
97
+ findViewById<View>(R.id.rn_telecom_answer).setOnClickListener { onAnswer() }
98
+ }
99
+
100
+ private fun clipCircle(view: View) {
101
+ view.outlineProvider =
102
+ object : ViewOutlineProvider() {
103
+ override fun getOutline(v: View, outline: Outline) {
104
+ outline.setOval(0, 0, v.width, v.height)
105
+ }
106
+ }
107
+ view.clipToOutline = true
108
+ }
109
+
110
+ private fun applyLockScreenFlags() {
111
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
112
+ setShowWhenLocked(true)
113
+ setTurnScreenOn(true)
114
+ }
115
+ @Suppress("DEPRECATION")
116
+ window.addFlags(
117
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
118
+ WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON or
119
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
120
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
121
+ )
122
+
123
+ WindowCompat.setDecorFitsSystemWindows(window, true)
124
+ window.statusBarColor = Color.TRANSPARENT
125
+ window.navigationBarColor = Color.TRANSPARENT
126
+ WindowInsetsControllerCompat(window, window.decorView).apply {
127
+ isAppearanceLightStatusBars = true
128
+ isAppearanceLightNavigationBars = true
129
+ }
130
+ }
131
+
132
+ private fun onAnswer() {
133
+ TelecomRingtonePlayer.stop()
134
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
135
+ val keyguard = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
136
+ keyguard.requestDismissKeyguard(this, null)
137
+ }
138
+ CallSessionManager.instance?.answerCall(uuid)
139
+ finish()
140
+ }
141
+
142
+ private fun onDecline() {
143
+ TelecomRingtonePlayer.stop()
144
+ CallSessionManager.instance?.endCall(uuid)
145
+ finish()
146
+ }
147
+
148
+ private fun openMessage() {
149
+ if (handle.isBlank()) return
150
+ try {
151
+ val intent =
152
+ Intent(Intent.ACTION_SENDTO).apply {
153
+ data = Uri.parse("smsto:${Uri.encode(handle)}")
154
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
155
+ }
156
+ startActivity(intent)
157
+ } catch (_: Exception) {
158
+ }
159
+ }
160
+
161
+ override fun onDestroy() {
162
+ finishReceiver?.let {
163
+ try {
164
+ unregisterReceiver(it)
165
+ } catch (_: Exception) {
166
+ }
167
+ }
168
+ finishReceiver = null
169
+ super.onDestroy()
170
+ }
171
+ }
@@ -0,0 +1,189 @@
1
+ package com.telecom
2
+
3
+ import android.graphics.Color
4
+ import com.facebook.react.bridge.Promise
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.bridge.ReadableMap
7
+ import com.facebook.react.bridge.WritableMap
8
+ import com.facebook.react.module.annotations.ReactModule
9
+ import com.facebook.react.modules.core.DeviceEventManagerModule
10
+
11
+ @ReactModule(name = TelecomModule.NAME)
12
+ class TelecomModule(reactContext: ReactApplicationContext) :
13
+ NativeTelecomSpec(reactContext) {
14
+
15
+ private val sessionManager: CallSessionManager =
16
+ CallSessionManager.instance?.also { existing ->
17
+ existing.rebindEmitter { eventName, params -> emitEvent(eventName, params) }
18
+ }
19
+ ?: CallSessionManager(reactContext.applicationContext) { eventName, params ->
20
+ emitEvent(eventName, params)
21
+ }
22
+
23
+ override fun setup(options: ReadableMap, promise: Promise) {
24
+ try {
25
+ val supportsVideo =
26
+ if (options.hasKey("supportsVideo")) {
27
+ options.getBoolean("supportsVideo")
28
+ } else {
29
+ true
30
+ }
31
+
32
+ val ui = parseUiConfig(options)
33
+ val accepted = sessionManager.setup(supportsVideo, ui)
34
+ promise.resolve(accepted)
35
+ } catch (error: Exception) {
36
+ promise.reject("E_TELECOM_SETUP", error.message, error)
37
+ }
38
+ }
39
+
40
+ override fun displayIncomingCall(
41
+ uuid: String,
42
+ handle: String,
43
+ callerName: String,
44
+ hasVideo: Boolean,
45
+ autoAnswer: Boolean,
46
+ avatarUrl: String,
47
+ ) {
48
+ sessionManager.displayIncomingCall(
49
+ uuid,
50
+ handle,
51
+ callerName,
52
+ hasVideo,
53
+ autoAnswer,
54
+ avatarUrl,
55
+ )
56
+ }
57
+
58
+ override fun startCall(
59
+ uuid: String,
60
+ handle: String,
61
+ callerName: String,
62
+ hasVideo: Boolean,
63
+ avatarUrl: String,
64
+ ) {
65
+ sessionManager.startCall(uuid, handle, callerName, hasVideo, avatarUrl)
66
+ }
67
+
68
+ override fun answerCall(uuid: String) {
69
+ sessionManager.answerCall(uuid)
70
+ }
71
+
72
+ override fun endCall(uuid: String) {
73
+ sessionManager.endCall(uuid)
74
+ }
75
+
76
+ override fun endAllCalls() {
77
+ sessionManager.endAllCalls()
78
+ }
79
+
80
+ override fun setMuted(uuid: String, muted: Boolean) {
81
+ sessionManager.setMuted(uuid, muted)
82
+ }
83
+
84
+ override fun setOnHold(uuid: String, hold: Boolean) {
85
+ sessionManager.setOnHold(uuid, hold)
86
+ }
87
+
88
+ override fun setAudioRoute(uuid: String, route: String) {
89
+ sessionManager.setAudioRoute(uuid, route)
90
+ }
91
+
92
+ override fun getAvailableAudioRoutes(promise: Promise) {
93
+ try {
94
+ promise.resolve(sessionManager.getAvailableAudioRoutes())
95
+ } catch (error: Exception) {
96
+ promise.reject("E_AUDIO_ROUTES", error.message, error)
97
+ }
98
+ }
99
+
100
+ override fun sendDtmf(uuid: String, digits: String) {
101
+ sessionManager.sendDtmf(uuid, digits)
102
+ }
103
+
104
+ override fun handlePushMessage(data: ReadableMap, promise: Promise) {
105
+ try {
106
+ val map = HashMap<String, String>()
107
+ val iterator = data.keySetIterator()
108
+ while (iterator.hasNextKey()) {
109
+ val key = iterator.nextKey()
110
+ when (data.getType(key)) {
111
+ com.facebook.react.bridge.ReadableType.String ->
112
+ map[key] = data.getString(key).orEmpty()
113
+ com.facebook.react.bridge.ReadableType.Boolean ->
114
+ map[key] = if (data.getBoolean(key)) "true" else "false"
115
+ com.facebook.react.bridge.ReadableType.Number ->
116
+ map[key] = data.getDouble(key).toString()
117
+ else -> Unit
118
+ }
119
+ }
120
+ val handled =
121
+ TelecomPushBridge.handleIncomingCall(reactApplicationContext, map)
122
+ promise.resolve(handled)
123
+ } catch (error: Exception) {
124
+ promise.reject("E_PUSH", error.message, error)
125
+ }
126
+ }
127
+
128
+ override fun addListener(eventName: String) {
129
+ // Required for NativeEventEmitter / RCTDeviceEventEmitter.
130
+ }
131
+
132
+ override fun removeListeners(count: Double) {
133
+ // Required for NativeEventEmitter / RCTDeviceEventEmitter.
134
+ }
135
+
136
+ private fun parseUiConfig(options: ReadableMap): TelecomUiConfig? {
137
+ if (!options.hasKey("ui") || options.isNull("ui")) {
138
+ return null
139
+ }
140
+ val map = options.getMap("ui") ?: return null
141
+ val base = TelecomUiConfig.current
142
+
143
+ return base.copy(
144
+ incomingTitle = map.stringOr(base.incomingTitle, "incomingTitle"),
145
+ answerLabel = map.stringOr(base.answerLabel, "answerLabel"),
146
+ declineLabel = map.stringOr(base.declineLabel, "declineLabel"),
147
+ backgroundColor = map.colorOr(base.backgroundColor, "backgroundColor"),
148
+ titleColor = map.colorOr(base.titleColor, "titleColor"),
149
+ nameColor = map.colorOr(base.nameColor, "nameColor"),
150
+ handleColor = map.colorOr(base.handleColor, "handleColor"),
151
+ answerColor = map.colorOr(base.answerColor, "answerColor"),
152
+ declineColor = map.colorOr(base.declineColor, "declineColor"),
153
+ launchFullScreenOnIncoming =
154
+ if (map.hasKey("launchFullScreenOnIncoming") && !map.isNull("launchFullScreenOnIncoming")) {
155
+ map.getBoolean("launchFullScreenOnIncoming")
156
+ } else {
157
+ base.launchFullScreenOnIncoming
158
+ },
159
+ )
160
+ }
161
+
162
+ private fun ReadableMap.stringOr(fallback: String, key: String): String {
163
+ return if (hasKey(key) && !isNull(key)) getString(key) ?: fallback else fallback
164
+ }
165
+
166
+ private fun ReadableMap.colorOr(fallback: Int, key: String): Int {
167
+ if (!hasKey(key) || isNull(key)) return fallback
168
+ val raw = getString(key) ?: return fallback
169
+ return try {
170
+ Color.parseColor(raw)
171
+ } catch (_: Exception) {
172
+ fallback
173
+ }
174
+ }
175
+
176
+ private fun emitEvent(eventName: String, params: WritableMap) {
177
+ if (!reactApplicationContext.hasActiveReactInstance()) {
178
+ return
179
+ }
180
+
181
+ reactApplicationContext
182
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
183
+ .emit(eventName, params)
184
+ }
185
+
186
+ companion object {
187
+ const val NAME = "Telecom"
188
+ }
189
+ }
@@ -0,0 +1,31 @@
1
+ package com.telecom
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import java.util.HashMap
9
+
10
+ class TelecomPackage : BaseReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12
+ return if (name == TelecomModule.NAME) {
13
+ TelecomModule(reactContext)
14
+ } else {
15
+ null
16
+ }
17
+ }
18
+
19
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
20
+ mapOf(
21
+ TelecomModule.NAME to ReactModuleInfo(
22
+ name = TelecomModule.NAME,
23
+ className = TelecomModule.NAME,
24
+ canOverrideExistingModule = false,
25
+ needsEagerInit = false,
26
+ isCxxModule = false,
27
+ isTurboModule = true
28
+ )
29
+ )
30
+ }
31
+ }