react-native-telecom 0.0.0-stage → 0.1.1
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 +20 -0
- package/README.md +321 -2
- package/Telecom.podspec +20 -0
- package/android/build.gradle +58 -0
- package/android/src/main/AndroidManifest.xml +43 -0
- package/android/src/main/java/com/telecom/CallSession.kt +37 -0
- package/android/src/main/java/com/telecom/CallSessionManager.kt +486 -0
- package/android/src/main/java/com/telecom/TelecomAudioRouter.kt +227 -0
- package/android/src/main/java/com/telecom/TelecomAvatarLoader.kt +77 -0
- package/android/src/main/java/com/telecom/TelecomCallService.kt +395 -0
- package/android/src/main/java/com/telecom/TelecomCallUi.kt +65 -0
- package/android/src/main/java/com/telecom/TelecomInCallActivity.kt +285 -0
- package/android/src/main/java/com/telecom/TelecomIncomingCallActivity.kt +171 -0
- package/android/src/main/java/com/telecom/TelecomModule.kt +189 -0
- package/android/src/main/java/com/telecom/TelecomPackage.kt +31 -0
- package/android/src/main/java/com/telecom/TelecomPushBridge.kt +82 -0
- package/android/src/main/java/com/telecom/TelecomRingtonePlayer.kt +109 -0
- package/android/src/main/java/com/telecom/TelecomUiConfig.kt +27 -0
- package/android/src/main/res/drawable/rn_telecom_action_pill.xml +6 -0
- package/android/src/main/res/drawable/rn_telecom_answer_circle.xml +8 -0
- package/android/src/main/res/drawable/rn_telecom_answer_circle_green.xml +8 -0
- package/android/src/main/res/drawable/rn_telecom_avatar_bg.xml +6 -0
- package/android/src/main/res/drawable/rn_telecom_avatar_circle.xml +12 -0
- package/android/src/main/res/drawable/rn_telecom_btn_answer.xml +6 -0
- package/android/src/main/res/drawable/rn_telecom_btn_decline.xml +6 -0
- package/android/src/main/res/drawable/rn_telecom_btn_message.xml +9 -0
- package/android/src/main/res/drawable/rn_telecom_btn_secondary.xml +6 -0
- package/android/src/main/res/drawable/rn_telecom_control_circle.xml +8 -0
- package/android/src/main/res/drawable/rn_telecom_control_circle_on.xml +8 -0
- package/android/src/main/res/drawable/rn_telecom_decline_circle.xml +8 -0
- package/android/src/main/res/drawable/rn_telecom_ic_call.xml +10 -0
- package/android/src/main/res/drawable/rn_telecom_ic_call_end.xml +16 -0
- package/android/src/main/res/drawable/rn_telecom_ic_call_white.xml +10 -0
- package/android/src/main/res/drawable/rn_telecom_ic_dialpad.xml +13 -0
- package/android/src/main/res/drawable/rn_telecom_ic_hold.xml +10 -0
- package/android/src/main/res/drawable/rn_telecom_ic_message.xml +10 -0
- package/android/src/main/res/drawable/rn_telecom_ic_mic.xml +10 -0
- package/android/src/main/res/drawable/rn_telecom_ic_mic_off.xml +10 -0
- package/android/src/main/res/drawable/rn_telecom_ic_speaker.xml +10 -0
- package/android/src/main/res/layout/rn_telecom_control_item.xml +36 -0
- package/android/src/main/res/layout/rn_telecom_incall.xml +168 -0
- package/android/src/main/res/layout/rn_telecom_incoming_call.xml +188 -0
- package/ios/Telecom.h +5 -0
- package/ios/Telecom.mm +66 -0
- package/lib/module/NativeTelecom.js +5 -0
- package/lib/module/NativeTelecom.js.map +1 -0
- package/lib/module/index.js +102 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeTelecom.d.ts +40 -0
- package/lib/typescript/src/NativeTelecom.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +104 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +154 -4
- package/src/NativeTelecom.ts +55 -0
- package/src/index.tsx +217 -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
|
+
}
|