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,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
|
+
}
|