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,77 @@
1
+ package com.telecom
2
+
3
+ import android.content.Context
4
+ import android.graphics.Bitmap
5
+ import android.graphics.BitmapFactory
6
+ import android.os.Handler
7
+ import android.os.Looper
8
+ import android.util.Log
9
+ import android.widget.ImageView
10
+ import android.widget.TextView
11
+ import java.net.HttpURLConnection
12
+ import java.net.URL
13
+ import java.util.concurrent.Executors
14
+
15
+ /** Loads a remote/local avatar into an ImageView; falls back to initials. */
16
+ internal object TelecomAvatarLoader {
17
+ private const val TAG = "RNTelecomAvatar"
18
+ private val executor = Executors.newFixedThreadPool(2)
19
+ private val main = Handler(Looper.getMainLooper())
20
+
21
+ fun bind(
22
+ imageView: ImageView,
23
+ initialsView: TextView,
24
+ avatarUrl: String?,
25
+ callerName: String,
26
+ ) {
27
+ initialsView.text = initialsFor(callerName)
28
+ initialsView.visibility = android.view.View.VISIBLE
29
+ imageView.setImageDrawable(null)
30
+ imageView.visibility = android.view.View.GONE
31
+
32
+ val url = avatarUrl?.trim().orEmpty()
33
+ if (url.isEmpty()) return
34
+
35
+ executor.execute {
36
+ val bitmap = decode(url)
37
+ main.post {
38
+ if (bitmap != null) {
39
+ imageView.setImageBitmap(bitmap)
40
+ imageView.visibility = android.view.View.VISIBLE
41
+ initialsView.visibility = android.view.View.GONE
42
+ }
43
+ }
44
+ }
45
+ }
46
+
47
+ private fun decode(url: String): Bitmap? {
48
+ return try {
49
+ when {
50
+ url.startsWith("http://") || url.startsWith("https://") -> {
51
+ val conn = URL(url).openConnection() as HttpURLConnection
52
+ conn.connectTimeout = 8_000
53
+ conn.readTimeout = 8_000
54
+ conn.instanceFollowRedirects = true
55
+ conn.inputStream.use { BitmapFactory.decodeStream(it) }
56
+ }
57
+ url.startsWith("file://") || url.startsWith("/") -> {
58
+ BitmapFactory.decodeFile(url.removePrefix("file://"))
59
+ }
60
+ url.startsWith("content://") -> null // host app can pass https/file for now
61
+ else -> BitmapFactory.decodeFile(url)
62
+ }
63
+ } catch (error: Exception) {
64
+ Log.w(TAG, "avatar load failed: ${error.message}")
65
+ null
66
+ }
67
+ }
68
+
69
+ fun initialsFor(name: String): String {
70
+ val parts = name.trim().split(Regex("\\s+")).filter { it.isNotEmpty() }
71
+ return when {
72
+ parts.isEmpty() -> "?"
73
+ parts.size == 1 -> parts[0].take(2).uppercase()
74
+ else -> "${parts[0].first()}${parts[1].first()}".uppercase()
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,395 @@
1
+ package com.telecom
2
+
3
+ import android.app.Notification
4
+ import android.app.NotificationChannel
5
+ import android.app.NotificationManager
6
+ import android.app.PendingIntent
7
+ import android.app.Service
8
+ import android.content.Context
9
+ import android.content.Intent
10
+ import android.content.pm.ServiceInfo
11
+ import android.net.Uri
12
+ import android.os.Build
13
+ import android.os.IBinder
14
+ import android.util.Log
15
+ import androidx.core.app.NotificationCompat
16
+ import androidx.core.app.Person
17
+ import androidx.core.app.ServiceCompat
18
+ import androidx.core.content.ContextCompat
19
+
20
+ /**
21
+ * Native CallStyle notification + optional full-screen Activity.
22
+ *
23
+ * Required CallStyle pieces (Android docs):
24
+ * - [Person] with non-empty name + [Person.Builder.setImportant] true
25
+ * - [NotificationCompat.CallStyle.forIncomingCall] (decline, answer) OR forOngoingCall
26
+ * - [NotificationCompat.Builder.addPerson]
27
+ * - [NotificationCompat.Builder.setOngoing] true (Android 14+ non-dismissible)
28
+ * - [NotificationCompat.Builder.setCategory] CATEGORY_CALL
29
+ * - Foreground service type phoneCall + MANAGE_OWN_CALLS
30
+ * - fullScreenIntent must be an **Activity** PendingIntent
31
+ */
32
+ class TelecomCallService : Service() {
33
+ override fun onBind(intent: Intent?): IBinder? = null
34
+
35
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
36
+ val action = intent?.action
37
+ val uuid = intent?.getStringExtra(EXTRA_UUID).orEmpty()
38
+ val callerName = intent?.getStringExtra(EXTRA_CALLER_NAME) ?: "Unknown"
39
+ val handle = intent?.getStringExtra(EXTRA_HANDLE).orEmpty()
40
+ val direction = intent?.getStringExtra(EXTRA_DIRECTION) ?: "incoming"
41
+ val avatarUrl = intent?.getStringExtra(EXTRA_AVATAR_URL).orEmpty()
42
+ val ringing = direction == "incoming"
43
+ val skipIncomingUi = intent?.getBooleanExtra(EXTRA_SKIP_INCOMING_UI, false) == true
44
+
45
+ when (action) {
46
+ ACTION_STOP -> {
47
+ TelecomRingtonePlayer.stop()
48
+ TelecomCallUi.finishAll(this)
49
+ ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
50
+ stopSelf()
51
+ return START_NOT_STICKY
52
+ }
53
+ ACTION_ANSWER -> {
54
+ TelecomRingtonePlayer.stop()
55
+ CallSessionManager.instance?.answerCall(uuid)
56
+ return START_STICKY
57
+ }
58
+ ACTION_DECLINE, ACTION_END -> {
59
+ TelecomRingtonePlayer.stop()
60
+ CallSessionManager.instance?.endCall(uuid)
61
+ return START_NOT_STICKY
62
+ }
63
+ ACTION_UPDATE_ONGOING -> {
64
+ TelecomRingtonePlayer.stop()
65
+ ensureChannel()
66
+ postNotification(uuid, callerName, handle, ringing = false, avatarUrl = avatarUrl)
67
+ return START_STICKY
68
+ }
69
+ else -> {
70
+ ensureChannel()
71
+ logFullScreenCapability()
72
+
73
+ val notification =
74
+ buildCallStyleNotification(
75
+ uuid,
76
+ callerName,
77
+ handle,
78
+ ringing && !skipIncomingUi,
79
+ avatarUrl,
80
+ )
81
+ val type =
82
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
83
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
84
+ } else {
85
+ 0
86
+ }
87
+ ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, type)
88
+
89
+ if (ringing && !skipIncomingUi) {
90
+ // OEM often mutes channel sound while app is foreground — play ourselves.
91
+ TelecomRingtonePlayer.start(this)
92
+ if (TelecomUiConfig.current.launchFullScreenOnIncoming) {
93
+ launchFullScreenActivity(uuid, callerName, handle, avatarUrl)
94
+ }
95
+ } else if (!ringing) {
96
+ TelecomRingtonePlayer.stop()
97
+ openInCall(this, uuid, callerName, handle, direction, avatarUrl)
98
+ }
99
+ return START_STICKY
100
+ }
101
+ }
102
+ }
103
+
104
+ private fun launchFullScreenActivity(
105
+ uuid: String,
106
+ callerName: String,
107
+ handle: String,
108
+ avatarUrl: String,
109
+ ) {
110
+ val fullScreen =
111
+ Intent(this, TelecomIncomingCallActivity::class.java).apply {
112
+ addFlags(
113
+ Intent.FLAG_ACTIVITY_NEW_TASK or
114
+ Intent.FLAG_ACTIVITY_CLEAR_TOP or
115
+ Intent.FLAG_ACTIVITY_SINGLE_TOP,
116
+ )
117
+ putExtra(EXTRA_UUID, uuid)
118
+ putExtra(EXTRA_CALLER_NAME, callerName)
119
+ putExtra(EXTRA_HANDLE, handle)
120
+ putExtra(EXTRA_DIRECTION, "incoming")
121
+ putExtra(EXTRA_AVATAR_URL, avatarUrl)
122
+ }
123
+ try {
124
+ startActivity(fullScreen)
125
+ Log.i(TAG, "full-screen Activity launched uuid=$uuid")
126
+ } catch (error: Exception) {
127
+ Log.e(TAG, "full-screen launch failed: ${error.message}", error)
128
+ }
129
+ }
130
+
131
+ private fun postNotification(
132
+ uuid: String,
133
+ callerName: String,
134
+ handle: String,
135
+ ringing: Boolean,
136
+ avatarUrl: String = "",
137
+ ) {
138
+ val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
139
+ if (!ringing) {
140
+ // Channel/FLAG_INSISTENT alerts keep ringing on some OEMs until the
141
+ // notification is cancelled — kill it, then post quiet ongoing.
142
+ TelecomRingtonePlayer.stop()
143
+ manager.cancel(NOTIFICATION_ID)
144
+ }
145
+ manager.notify(
146
+ NOTIFICATION_ID,
147
+ buildCallStyleNotification(uuid, callerName, handle, ringing, avatarUrl),
148
+ )
149
+ }
150
+
151
+ private fun logFullScreenCapability() {
152
+ if (Build.VERSION.SDK_INT < 34) return
153
+ val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
154
+ Log.i(TAG, "canUseFullScreenIntent=${manager.canUseFullScreenIntent()}")
155
+ }
156
+
157
+ private fun ensureChannel() {
158
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
159
+ val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
160
+ // Remove older channels that still had ringtone/vibration attached.
161
+ listOf(
162
+ "rn_telecom_calls",
163
+ "rn_telecom_calls_v2",
164
+ "rn_telecom_calls_v3",
165
+ ).forEach { manager.deleteNotificationChannel(it) }
166
+
167
+ // Silent channel: ringtone/vibrate are owned by TelecomRingtonePlayer so
168
+ // answering can stop sound immediately without waiting for notification dismiss.
169
+ val channel =
170
+ NotificationChannel(
171
+ CHANNEL_ID,
172
+ "Calls",
173
+ NotificationManager.IMPORTANCE_HIGH,
174
+ ).apply {
175
+ description = "CallStyle incoming / ongoing (silent — app plays ringtone)"
176
+ enableVibration(false)
177
+ lockscreenVisibility = Notification.VISIBILITY_PUBLIC
178
+ setBypassDnd(true)
179
+ setShowBadge(false)
180
+ setSound(null, null)
181
+ }
182
+ manager.createNotificationChannel(channel)
183
+ }
184
+
185
+ private fun buildCallStyleNotification(
186
+ uuid: String,
187
+ callerName: String,
188
+ handle: String,
189
+ ringing: Boolean,
190
+ avatarUrl: String = "",
191
+ ): Notification {
192
+ val displayName = callerName.ifBlank { handle }.ifBlank { "Unknown" }
193
+
194
+ // Required: non-empty name + important=true for CallStyle ranking.
195
+ val personBuilder =
196
+ Person.Builder()
197
+ .setName(displayName)
198
+ .setImportant(true)
199
+
200
+ if (handle.isNotBlank()) {
201
+ personBuilder.setUri(Uri.fromParts("tel", handle, null).toString())
202
+ }
203
+ val person = personBuilder.build()
204
+
205
+ val contentActivityIntent =
206
+ if (ringing) {
207
+ Intent(this, TelecomIncomingCallActivity::class.java)
208
+ } else {
209
+ Intent(this, TelecomInCallActivity::class.java)
210
+ }.apply {
211
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
212
+ putExtra(EXTRA_UUID, uuid)
213
+ putExtra(EXTRA_CALLER_NAME, callerName)
214
+ putExtra(EXTRA_HANDLE, handle)
215
+ putExtra(EXTRA_DIRECTION, if (ringing) "incoming" else "outgoing")
216
+ putExtra(EXTRA_AVATAR_URL, avatarUrl)
217
+ }
218
+
219
+ val contentPending =
220
+ PendingIntent.getActivity(
221
+ this,
222
+ REQUEST_FULL_SCREEN,
223
+ contentActivityIntent,
224
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
225
+ )
226
+
227
+ // CallStyle action intents (service keeps Telecom session in sync).
228
+ val answerIntent =
229
+ PendingIntent.getService(
230
+ this,
231
+ REQUEST_ANSWER,
232
+ Intent(this, TelecomCallService::class.java).apply {
233
+ action = ACTION_ANSWER
234
+ putExtra(EXTRA_UUID, uuid)
235
+ putExtra(EXTRA_CALLER_NAME, callerName)
236
+ putExtra(EXTRA_HANDLE, handle)
237
+ putExtra(EXTRA_DIRECTION, "outgoing")
238
+ },
239
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
240
+ )
241
+
242
+ val declineOrEndIntent =
243
+ PendingIntent.getService(
244
+ this,
245
+ REQUEST_DECLINE,
246
+ Intent(this, TelecomCallService::class.java).apply {
247
+ action = if (ringing) ACTION_DECLINE else ACTION_END
248
+ putExtra(EXTRA_UUID, uuid)
249
+ },
250
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
251
+ )
252
+
253
+ // Parameter order for incoming: (person, declineIntent, answerIntent)
254
+ val style =
255
+ if (ringing) {
256
+ NotificationCompat.CallStyle.forIncomingCall(person, declineOrEndIntent, answerIntent)
257
+ } else {
258
+ NotificationCompat.CallStyle.forOngoingCall(person, declineOrEndIntent)
259
+ }
260
+
261
+ val builder =
262
+ NotificationCompat.Builder(this, CHANNEL_ID)
263
+ .setSmallIcon(android.R.drawable.sym_call_incoming)
264
+ .setContentTitle(displayName)
265
+ .setContentText(handle)
266
+ .setCategory(NotificationCompat.CATEGORY_CALL)
267
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
268
+ .setPriority(NotificationCompat.PRIORITY_MAX)
269
+ .setOngoing(true) // Android 14+: non-dismissible call notification
270
+ .setOnlyAlertOnce(true)
271
+ .setSilent(true) // never use notification sound — RingtonePlayer owns audio
272
+ .setStyle(style)
273
+ .addPerson(person)
274
+ .setContentIntent(contentPending)
275
+ .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
276
+
277
+ // API 30 and below: colorized helps CallStyle-like ranking.
278
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
279
+ builder.setColorized(true).setColor(0xFF1B5E20.toInt())
280
+ }
281
+
282
+ if (ringing) {
283
+ // MUST be Activity PendingIntent — never a Service.
284
+ builder.setFullScreenIntent(contentPending, true)
285
+ }
286
+
287
+ return builder.build().also { notification ->
288
+ notification.flags = notification.flags or Notification.FLAG_NO_CLEAR
289
+ }
290
+ }
291
+
292
+ companion object {
293
+ private const val TAG = "RNTelecomCallUI"
294
+ const val CHANNEL_ID = "rn_telecom_calls_v4"
295
+ const val NOTIFICATION_ID = 71001
296
+
297
+ private const val REQUEST_FULL_SCREEN = 10
298
+ private const val REQUEST_ANSWER = 11
299
+ private const val REQUEST_DECLINE = 12
300
+
301
+ const val ACTION_START = "com.telecom.action.START_CALL_UI"
302
+ const val ACTION_STOP = "com.telecom.action.STOP_CALL_UI"
303
+ const val ACTION_ANSWER = "com.telecom.action.ANSWER"
304
+ const val ACTION_DECLINE = "com.telecom.action.DECLINE"
305
+ const val ACTION_END = "com.telecom.action.END"
306
+ const val ACTION_UPDATE_ONGOING = "com.telecom.action.UPDATE_ONGOING"
307
+
308
+ const val EXTRA_UUID = "uuid"
309
+ const val EXTRA_CALLER_NAME = "callerName"
310
+ const val EXTRA_HANDLE = "handle"
311
+ const val EXTRA_DIRECTION = "direction"
312
+ const val EXTRA_SKIP_INCOMING_UI = "skipIncomingUi"
313
+ const val EXTRA_AVATAR_URL = "avatarUrl"
314
+
315
+ fun start(
316
+ context: Context,
317
+ uuid: String,
318
+ callerName: String,
319
+ handle: String,
320
+ direction: String,
321
+ skipIncomingUi: Boolean = false,
322
+ avatarUrl: String = "",
323
+ ) {
324
+ val intent =
325
+ Intent(context, TelecomCallService::class.java).apply {
326
+ action = ACTION_START
327
+ putExtra(EXTRA_UUID, uuid)
328
+ putExtra(EXTRA_CALLER_NAME, callerName)
329
+ putExtra(EXTRA_HANDLE, handle)
330
+ putExtra(EXTRA_DIRECTION, direction)
331
+ putExtra(EXTRA_SKIP_INCOMING_UI, skipIncomingUi)
332
+ putExtra(EXTRA_AVATAR_URL, avatarUrl)
333
+ }
334
+ ContextCompat.startForegroundService(context, intent)
335
+ }
336
+
337
+ fun updateOngoing(
338
+ context: Context,
339
+ uuid: String,
340
+ callerName: String,
341
+ handle: String,
342
+ avatarUrl: String = "",
343
+ ) {
344
+ val intent =
345
+ Intent(context, TelecomCallService::class.java).apply {
346
+ action = ACTION_UPDATE_ONGOING
347
+ putExtra(EXTRA_UUID, uuid)
348
+ putExtra(EXTRA_CALLER_NAME, callerName)
349
+ putExtra(EXTRA_HANDLE, handle)
350
+ putExtra(EXTRA_DIRECTION, "outgoing")
351
+ putExtra(EXTRA_AVATAR_URL, avatarUrl)
352
+ }
353
+ context.startService(intent)
354
+ }
355
+
356
+ fun stop(context: Context) {
357
+ TelecomRingtonePlayer.stop()
358
+ TelecomCallUi.finishAll(context)
359
+ val intent =
360
+ Intent(context, TelecomCallService::class.java).apply {
361
+ action = ACTION_STOP
362
+ }
363
+ context.startService(intent)
364
+ }
365
+
366
+ fun openInCall(
367
+ context: Context,
368
+ uuid: String,
369
+ callerName: String,
370
+ handle: String,
371
+ direction: String,
372
+ avatarUrl: String = "",
373
+ ) {
374
+ val intent =
375
+ Intent(context, TelecomInCallActivity::class.java).apply {
376
+ addFlags(
377
+ Intent.FLAG_ACTIVITY_NEW_TASK or
378
+ Intent.FLAG_ACTIVITY_CLEAR_TOP or
379
+ Intent.FLAG_ACTIVITY_SINGLE_TOP,
380
+ )
381
+ putExtra(EXTRA_UUID, uuid)
382
+ putExtra(EXTRA_CALLER_NAME, callerName)
383
+ putExtra(EXTRA_HANDLE, handle)
384
+ putExtra(EXTRA_DIRECTION, direction)
385
+ putExtra(EXTRA_AVATAR_URL, avatarUrl)
386
+ }
387
+ try {
388
+ context.startActivity(intent)
389
+ Log.i(TAG, "in-call Activity launched uuid=$uuid direction=$direction")
390
+ } catch (error: Exception) {
391
+ Log.e(TAG, "in-call launch failed: ${error.message}", error)
392
+ }
393
+ }
394
+ }
395
+ }
@@ -0,0 +1,65 @@
1
+ package com.telecom
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.content.IntentFilter
7
+ import android.app.Activity
8
+ import android.os.Build
9
+
10
+ /**
11
+ * Closes call Activities when the session is answered / ended from any path
12
+ * (auto-answer, notification, JS, other Activity).
13
+ */
14
+ internal object TelecomCallUi {
15
+ const val ACTION_FINISH_INCOMING = "com.telecom.action.FINISH_INCOMING"
16
+ const val ACTION_FINISH_INCALL = "com.telecom.action.FINISH_INCALL"
17
+ const val EXTRA_UUID = "uuid"
18
+
19
+ fun finishIncoming(context: Context, uuid: String? = null) {
20
+ context.sendBroadcast(
21
+ Intent(ACTION_FINISH_INCOMING).apply {
22
+ setPackage(context.packageName)
23
+ if (!uuid.isNullOrBlank()) putExtra(EXTRA_UUID, uuid)
24
+ },
25
+ )
26
+ }
27
+
28
+ fun finishInCall(context: Context, uuid: String? = null) {
29
+ context.sendBroadcast(
30
+ Intent(ACTION_FINISH_INCALL).apply {
31
+ setPackage(context.packageName)
32
+ if (!uuid.isNullOrBlank()) putExtra(EXTRA_UUID, uuid)
33
+ },
34
+ )
35
+ }
36
+
37
+ fun finishAll(context: Context, uuid: String? = null) {
38
+ finishIncoming(context, uuid)
39
+ finishInCall(context, uuid)
40
+ }
41
+
42
+ fun registerFinishReceiver(
43
+ activity: Activity,
44
+ action: String,
45
+ uuid: String,
46
+ onFinish: () -> Unit,
47
+ ): BroadcastReceiver {
48
+ val receiver =
49
+ object : BroadcastReceiver() {
50
+ override fun onReceive(context: Context?, intent: Intent?) {
51
+ val target = intent?.getStringExtra(EXTRA_UUID)
52
+ if (target.isNullOrBlank() || target == uuid) {
53
+ onFinish()
54
+ }
55
+ }
56
+ }
57
+ val filter = IntentFilter(action)
58
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
59
+ activity.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)
60
+ } else {
61
+ activity.registerReceiver(receiver, filter)
62
+ }
63
+ return receiver
64
+ }
65
+ }