@kaleem766/react-native-incoming-call 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 (55) hide show
  1. package/IncomingCall.podspec +29 -0
  2. package/LICENSE +20 -0
  3. package/README.md +280 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +118 -0
  6. package/android/src/main/AndroidManifest.xml +35 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  8. package/android/src/main/java/com/margelo/nitro/incomingcall/IncomingCall.kt +219 -0
  9. package/android/src/main/java/com/margelo/nitro/incomingcall/IncomingCallActivity.kt +314 -0
  10. package/android/src/main/java/com/margelo/nitro/incomingcall/IncomingCallModule.kt +152 -0
  11. package/android/src/main/java/com/margelo/nitro/incomingcall/IncomingCallPackage.kt +31 -0
  12. package/android/src/main/java/com/margelo/nitro/incomingcall/IncomingCallService.kt +109 -0
  13. package/ios/IncomingCall.swift +28 -0
  14. package/lib/module/IncomingCall.nitro.js +4 -0
  15. package/lib/module/IncomingCall.nitro.js.map +1 -0
  16. package/lib/module/index.js +137 -0
  17. package/lib/module/index.js.map +1 -0
  18. package/lib/module/package.json +1 -0
  19. package/lib/typescript/package.json +1 -0
  20. package/lib/typescript/src/IncomingCall.nitro.d.ts +21 -0
  21. package/lib/typescript/src/IncomingCall.nitro.d.ts.map +1 -0
  22. package/lib/typescript/src/index.d.ts +43 -0
  23. package/lib/typescript/src/index.d.ts.map +1 -0
  24. package/nitro.json +17 -0
  25. package/nitrogen/generated/android/c++/JHybridIncomingCallSpec.cpp +101 -0
  26. package/nitrogen/generated/android/c++/JHybridIncomingCallSpec.hpp +73 -0
  27. package/nitrogen/generated/android/c++/views/JHybridIncomingCallStateUpdater.cpp +72 -0
  28. package/nitrogen/generated/android/c++/views/JHybridIncomingCallStateUpdater.hpp +49 -0
  29. package/nitrogen/generated/android/incomingcall+autolinking.cmake +83 -0
  30. package/nitrogen/generated/android/incomingcall+autolinking.gradle +27 -0
  31. package/nitrogen/generated/android/incomingcallOnLoad.cpp +56 -0
  32. package/nitrogen/generated/android/incomingcallOnLoad.hpp +34 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/incomingcall/HybridIncomingCallSpec.kt +87 -0
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/incomingcall/incomingcallOnLoad.kt +35 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/incomingcall/views/HybridIncomingCallManager.kt +70 -0
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/incomingcall/views/HybridIncomingCallStateUpdater.kt +23 -0
  37. package/nitrogen/generated/ios/IncomingCall+autolinking.rb +60 -0
  38. package/nitrogen/generated/ios/IncomingCall-Swift-Cxx-Bridge.cpp +33 -0
  39. package/nitrogen/generated/ios/IncomingCall-Swift-Cxx-Bridge.hpp +83 -0
  40. package/nitrogen/generated/ios/IncomingCall-Swift-Cxx-Umbrella.hpp +45 -0
  41. package/nitrogen/generated/ios/IncomingCallAutolinking.mm +33 -0
  42. package/nitrogen/generated/ios/IncomingCallAutolinking.swift +26 -0
  43. package/nitrogen/generated/ios/c++/HybridIncomingCallSpecSwift.cpp +11 -0
  44. package/nitrogen/generated/ios/c++/HybridIncomingCallSpecSwift.hpp +121 -0
  45. package/nitrogen/generated/ios/c++/views/HybridIncomingCallComponent.mm +127 -0
  46. package/nitrogen/generated/ios/swift/HybridIncomingCallSpec.swift +60 -0
  47. package/nitrogen/generated/ios/swift/HybridIncomingCallSpec_cxx.swift +270 -0
  48. package/nitrogen/generated/shared/c++/HybridIncomingCallSpec.cpp +32 -0
  49. package/nitrogen/generated/shared/c++/HybridIncomingCallSpec.hpp +73 -0
  50. package/nitrogen/generated/shared/c++/views/HybridIncomingCallComponent.cpp +127 -0
  51. package/nitrogen/generated/shared/c++/views/HybridIncomingCallComponent.hpp +115 -0
  52. package/nitrogen/generated/shared/json/IncomingCallConfig.json +14 -0
  53. package/package.json +180 -0
  54. package/src/IncomingCall.nitro.ts +27 -0
  55. package/src/index.tsx +173 -0
@@ -0,0 +1,219 @@
1
+ package com.margelo.nitro.incomingcall
2
+
3
+ import android.graphics.Color
4
+ import android.graphics.drawable.GradientDrawable
5
+ import android.util.TypedValue
6
+ import android.view.Gravity
7
+ import android.view.View
8
+ import android.widget.LinearLayout
9
+ import android.widget.RelativeLayout
10
+ import android.widget.TextView
11
+ import androidx.core.graphics.toColorInt
12
+ import com.facebook.proguard.annotations.DoNotStrip
13
+ import com.facebook.react.uimanager.ThemedReactContext
14
+
15
+ /**
16
+ * NitroView for displaying an inline incoming call UI inside a React Native screen
17
+ * (foreground / in-app use). For background / lock-screen display use the
18
+ * IncomingCallModule.display() API which starts the full-screen Activity.
19
+ */
20
+ @DoNotStrip
21
+ class HybridIncomingCall(private val ctx: ThemedReactContext) : HybridIncomingCallSpec() {
22
+
23
+ // ── Root layout ─────────────────────────────────────────────────────────────
24
+ private val rootLayout = RelativeLayout(ctx).apply {
25
+ setBackgroundColor(Color.parseColor("#1A1A2E"))
26
+ }
27
+
28
+ private val avatarView = TextView(ctx).apply {
29
+ gravity = Gravity.CENTER
30
+ setTextColor(Color.WHITE)
31
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 28f)
32
+ background = GradientDrawable().apply {
33
+ shape = GradientDrawable.OVAL
34
+ setColor(Color.parseColor("#3D5AFE"))
35
+ }
36
+ }
37
+
38
+ private val callerNameView = TextView(ctx).apply {
39
+ setTextColor(Color.WHITE)
40
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
41
+ gravity = Gravity.CENTER
42
+ }
43
+
44
+ private val subtitleView = TextView(ctx).apply {
45
+ setTextColor(Color.parseColor("#AAAAAA"))
46
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
47
+ gravity = Gravity.CENTER
48
+ text = "Incoming audio call"
49
+ }
50
+
51
+ override val view: View = rootLayout
52
+
53
+ init {
54
+ buildLayout()
55
+ }
56
+
57
+ private fun buildLayout() {
58
+ val dp = { v: Int -> (v * ctx.resources.displayMetrics.density).toInt() }
59
+
60
+ val avatarSize = dp(72)
61
+ avatarView.apply {
62
+ id = View.generateViewId()
63
+ layoutParams = RelativeLayout.LayoutParams(avatarSize, avatarSize).apply {
64
+ addRule(RelativeLayout.CENTER_HORIZONTAL)
65
+ topMargin = dp(20)
66
+ }
67
+ text = "?"
68
+ }
69
+ rootLayout.addView(avatarView)
70
+
71
+ subtitleView.apply {
72
+ id = View.generateViewId()
73
+ layoutParams = RelativeLayout.LayoutParams(
74
+ RelativeLayout.LayoutParams.MATCH_PARENT,
75
+ RelativeLayout.LayoutParams.WRAP_CONTENT
76
+ ).apply {
77
+ addRule(RelativeLayout.BELOW, avatarView.id)
78
+ topMargin = dp(8)
79
+ }
80
+ }
81
+ rootLayout.addView(subtitleView)
82
+
83
+ callerNameView.apply {
84
+ id = View.generateViewId()
85
+ layoutParams = RelativeLayout.LayoutParams(
86
+ RelativeLayout.LayoutParams.MATCH_PARENT,
87
+ RelativeLayout.LayoutParams.WRAP_CONTENT
88
+ ).apply {
89
+ addRule(RelativeLayout.BELOW, subtitleView.id)
90
+ topMargin = dp(4)
91
+ }
92
+ }
93
+ rootLayout.addView(callerNameView)
94
+
95
+ // Answer / Reject buttons row
96
+ val buttonsRow = LinearLayout(ctx).apply {
97
+ id = View.generateViewId()
98
+ orientation = LinearLayout.HORIZONTAL
99
+ gravity = Gravity.CENTER
100
+ layoutParams = RelativeLayout.LayoutParams(
101
+ RelativeLayout.LayoutParams.MATCH_PARENT,
102
+ RelativeLayout.LayoutParams.WRAP_CONTENT
103
+ ).apply {
104
+ addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
105
+ bottomMargin = dp(16)
106
+ }
107
+ }
108
+
109
+ val btnSize = dp(52)
110
+
111
+ val rejectBtn = TextView(ctx).apply {
112
+ layoutParams = LinearLayout.LayoutParams(btnSize, btnSize).apply {
113
+ rightMargin = dp(40)
114
+ }
115
+ text = "✕"
116
+ setTextColor(Color.WHITE)
117
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)
118
+ gravity = Gravity.CENTER
119
+ background = GradientDrawable().apply {
120
+ shape = GradientDrawable.OVAL
121
+ setColor(Color.parseColor("#E53935"))
122
+ }
123
+ setOnClickListener { rejectCall() }
124
+ }
125
+
126
+ val answerBtn = TextView(ctx).apply {
127
+ layoutParams = LinearLayout.LayoutParams(btnSize, btnSize)
128
+ text = "✆"
129
+ setTextColor(Color.WHITE)
130
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)
131
+ gravity = Gravity.CENTER
132
+ background = GradientDrawable().apply {
133
+ shape = GradientDrawable.OVAL
134
+ setColor(Color.parseColor("#43A047"))
135
+ }
136
+ setOnClickListener { answerCall() }
137
+ }
138
+
139
+ buttonsRow.addView(rejectBtn)
140
+ buttonsRow.addView(answerBtn)
141
+ rootLayout.addView(buttonsRow)
142
+ }
143
+
144
+ // ── Props ────────────────────────────────────────────────────────────────────
145
+
146
+ private var _color = "#1A1A2E"
147
+ override var color: String
148
+ get() = _color
149
+ set(value) {
150
+ _color = value
151
+ try { rootLayout.setBackgroundColor(value.toColorInt()) } catch (_: Exception) {}
152
+ }
153
+
154
+ private var _callerName: String? = null
155
+ override var callerName: String?
156
+ get() = _callerName
157
+ set(value) {
158
+ _callerName = value
159
+ callerNameView.text = value ?: ""
160
+ updateAvatar()
161
+ }
162
+
163
+ private var _avatar: String? = null
164
+ override var avatar: String?
165
+ get() = _avatar
166
+ set(value) {
167
+ _avatar = value
168
+ // Avatar URL loading can be added via an image-loading library;
169
+ // for now we keep the initials fallback.
170
+ updateAvatar()
171
+ }
172
+
173
+ private var _callType: String? = "audio"
174
+ override var callType: String?
175
+ get() = _callType
176
+ set(value) {
177
+ _callType = value ?: "audio"
178
+ subtitleView.text = "Incoming ${_callType} call"
179
+ }
180
+
181
+ private var _timeout: Double? = 30000.0
182
+ override var timeout: Double?
183
+ get() = _timeout
184
+ set(value) { _timeout = value }
185
+
186
+ // ── Methods ──────────────────────────────────────────────────────────────────
187
+
188
+ override fun answerCall() {
189
+ val uuid = IncomingCallModule.currentCallUuid ?: ""
190
+ val intent = android.content.Intent(IncomingCallModule.ACTION_ANSWER).apply {
191
+ putExtra("uuid", uuid)
192
+ `package` = ctx.packageName
193
+ }
194
+ ctx.sendBroadcast(intent)
195
+ }
196
+
197
+ override fun rejectCall() {
198
+ val uuid = IncomingCallModule.currentCallUuid ?: ""
199
+ val intent = android.content.Intent(IncomingCallModule.ACTION_REJECT).apply {
200
+ putExtra("uuid", uuid)
201
+ `package` = ctx.packageName
202
+ }
203
+ ctx.sendBroadcast(intent)
204
+ }
205
+
206
+ // ── Helpers ──────────────────────────────────────────────────────────────────
207
+
208
+ private fun updateAvatar() {
209
+ val name = _callerName ?: ""
210
+ val initials = name
211
+ .trim()
212
+ .split(" ")
213
+ .filter { it.isNotEmpty() }
214
+ .take(2)
215
+ .joinToString("") { it[0].uppercaseChar().toString() }
216
+ .ifEmpty { "?" }
217
+ avatarView.text = initials
218
+ }
219
+ }
@@ -0,0 +1,314 @@
1
+ package com.margelo.nitro.incomingcall
2
+
3
+ import android.app.KeyguardManager
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.graphics.Color
7
+ import android.graphics.drawable.GradientDrawable
8
+ import android.os.Build
9
+ import android.os.Bundle
10
+ import android.os.CountDownTimer
11
+ import android.util.TypedValue
12
+ import android.view.Gravity
13
+ import android.view.View
14
+ import android.view.WindowManager
15
+ import android.widget.LinearLayout
16
+ import android.widget.RelativeLayout
17
+ import android.widget.TextView
18
+ import androidx.appcompat.app.AppCompatActivity
19
+
20
+ class IncomingCallActivity : AppCompatActivity() {
21
+
22
+ private var uuid: String = ""
23
+ private var callerName: String = "Unknown"
24
+ private var callType: String = "audio"
25
+ private var timeout: Long = 30000L
26
+ private var countDownTimer: CountDownTimer? = null
27
+ private var timerView: TextView? = null
28
+
29
+ override fun onCreate(savedInstanceState: Bundle?) {
30
+ super.onCreate(savedInstanceState)
31
+
32
+ setupLockScreenFlags()
33
+
34
+ uuid = intent.getStringExtra("uuid") ?: ""
35
+ callerName = intent.getStringExtra("callerName") ?: "Unknown"
36
+ callType = intent.getStringExtra("callType") ?: "audio"
37
+ timeout = intent.getLongExtra("timeout", 30000L)
38
+ val backgroundColor = intent.getStringExtra("backgroundColor")
39
+
40
+ val bgColor = try {
41
+ if (backgroundColor != null) Color.parseColor(backgroundColor)
42
+ else Color.parseColor("#1A1A2E")
43
+ } catch (_: Exception) {
44
+ Color.parseColor("#1A1A2E")
45
+ }
46
+
47
+ buildUI(bgColor)
48
+ startCountdown()
49
+ }
50
+
51
+ // ──────────────────────────────────────────────────────────────────────────
52
+ // Lock screen / wake flags
53
+ // ──────────────────────────────────────────────────────────────────────────
54
+
55
+ private fun setupLockScreenFlags() {
56
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
57
+ setShowWhenLocked(true)
58
+ setTurnScreenOn(true)
59
+ } else {
60
+ @Suppress("DEPRECATION")
61
+ window.addFlags(
62
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
63
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
64
+ WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
65
+ )
66
+ }
67
+ window.addFlags(
68
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
69
+ WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
70
+ )
71
+
72
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
73
+ val km = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
74
+ km.requestDismissKeyguard(this, null)
75
+ }
76
+ }
77
+
78
+ // ──────────────────────────────────────────────────────────────────────────
79
+ // UI (built programmatically – no XML resources needed from the library)
80
+ // ──────────────────────────────────────────────────────────────────────────
81
+
82
+ private fun buildUI(bgColor: Int) {
83
+ val root = RelativeLayout(this).apply {
84
+ setBackgroundColor(bgColor)
85
+ layoutParams = RelativeLayout.LayoutParams(
86
+ RelativeLayout.LayoutParams.MATCH_PARENT,
87
+ RelativeLayout.LayoutParams.MATCH_PARENT
88
+ )
89
+ }
90
+
91
+ // ── Avatar (circle with initials) ──────────────────────────────────
92
+ val initials = callerName
93
+ .trim()
94
+ .split(" ")
95
+ .filter { it.isNotEmpty() }
96
+ .take(2)
97
+ .joinToString("") { it[0].uppercaseChar().toString() }
98
+ .ifEmpty { "?" }
99
+
100
+ val avatarSize = dp(100)
101
+ val avatar = TextView(this).apply {
102
+ id = View.generateViewId()
103
+ layoutParams = RelativeLayout.LayoutParams(avatarSize, avatarSize).apply {
104
+ addRule(RelativeLayout.CENTER_HORIZONTAL)
105
+ topMargin = dp(110)
106
+ }
107
+ text = initials
108
+ setTextColor(Color.WHITE)
109
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 38f)
110
+ gravity = Gravity.CENTER
111
+ background = circle(Color.parseColor("#3D5AFE"))
112
+ }
113
+ root.addView(avatar)
114
+
115
+ // ── "Incoming call" subtitle ────────────────────────────────────────
116
+ val subtitle = TextView(this).apply {
117
+ id = View.generateViewId()
118
+ layoutParams = RelativeLayout.LayoutParams(
119
+ RelativeLayout.LayoutParams.WRAP_CONTENT,
120
+ RelativeLayout.LayoutParams.WRAP_CONTENT
121
+ ).apply {
122
+ addRule(RelativeLayout.CENTER_HORIZONTAL)
123
+ addRule(RelativeLayout.BELOW, avatar.id)
124
+ topMargin = dp(14)
125
+ }
126
+ text = "Incoming ${callType} call"
127
+ setTextColor(Color.parseColor("#AAAAAA"))
128
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
129
+ }
130
+ root.addView(subtitle)
131
+
132
+ // ── Caller name ─────────────────────────────────────────────────────
133
+ val nameView = TextView(this).apply {
134
+ id = View.generateViewId()
135
+ layoutParams = RelativeLayout.LayoutParams(
136
+ RelativeLayout.LayoutParams.WRAP_CONTENT,
137
+ RelativeLayout.LayoutParams.WRAP_CONTENT
138
+ ).apply {
139
+ addRule(RelativeLayout.CENTER_HORIZONTAL)
140
+ addRule(RelativeLayout.BELOW, subtitle.id)
141
+ topMargin = dp(6)
142
+ }
143
+ text = callerName
144
+ setTextColor(Color.WHITE)
145
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 28f)
146
+ }
147
+ root.addView(nameView)
148
+
149
+ // ── Countdown timer ─────────────────────────────────────────────────
150
+ timerView = TextView(this).apply {
151
+ id = View.generateViewId()
152
+ layoutParams = RelativeLayout.LayoutParams(
153
+ RelativeLayout.LayoutParams.WRAP_CONTENT,
154
+ RelativeLayout.LayoutParams.WRAP_CONTENT
155
+ ).apply {
156
+ addRule(RelativeLayout.CENTER_HORIZONTAL)
157
+ addRule(RelativeLayout.BELOW, nameView.id)
158
+ topMargin = dp(10)
159
+ }
160
+ setTextColor(Color.parseColor("#AAAAAA"))
161
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
162
+ }
163
+ root.addView(timerView)
164
+
165
+ // ── Bottom action buttons ───────────────────────────────────────────
166
+ val buttonsRow = LinearLayout(this).apply {
167
+ id = View.generateViewId()
168
+ layoutParams = RelativeLayout.LayoutParams(
169
+ RelativeLayout.LayoutParams.MATCH_PARENT,
170
+ RelativeLayout.LayoutParams.WRAP_CONTENT
171
+ ).apply {
172
+ addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
173
+ bottomMargin = dp(80)
174
+ }
175
+ orientation = LinearLayout.HORIZONTAL
176
+ gravity = Gravity.CENTER
177
+ }
178
+
179
+ // Decline button group
180
+ val declineGroup = buttonGroup(
181
+ label = "Decline",
182
+ emoji = "✕",
183
+ color = Color.parseColor("#E53935"),
184
+ onClick = { onRejected() }
185
+ )
186
+
187
+ // Accept button group
188
+ val acceptGroup = buttonGroup(
189
+ label = "Accept",
190
+ emoji = "✆",
191
+ color = Color.parseColor("#43A047"),
192
+ onClick = { onAnswered() }
193
+ )
194
+ (acceptGroup.layoutParams as LinearLayout.LayoutParams).leftMargin = dp(80)
195
+
196
+ buttonsRow.addView(declineGroup)
197
+ buttonsRow.addView(acceptGroup)
198
+ root.addView(buttonsRow)
199
+
200
+ setContentView(root)
201
+ }
202
+
203
+ /** Creates a vertical group: circle button + label */
204
+ private fun buttonGroup(
205
+ label: String,
206
+ emoji: String,
207
+ color: Int,
208
+ onClick: () -> Unit
209
+ ): LinearLayout {
210
+ val group = LinearLayout(this).apply {
211
+ orientation = LinearLayout.VERTICAL
212
+ gravity = Gravity.CENTER
213
+ layoutParams = LinearLayout.LayoutParams(
214
+ LinearLayout.LayoutParams.WRAP_CONTENT,
215
+ LinearLayout.LayoutParams.WRAP_CONTENT
216
+ )
217
+ }
218
+
219
+ val btnSize = dp(72)
220
+ val btn = TextView(this).apply {
221
+ layoutParams = LinearLayout.LayoutParams(btnSize, btnSize)
222
+ text = emoji
223
+ setTextColor(Color.WHITE)
224
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 26f)
225
+ gravity = Gravity.CENTER
226
+ background = circle(color)
227
+ setOnClickListener { onClick() }
228
+ }
229
+ group.addView(btn)
230
+
231
+ val lbl = TextView(this).apply {
232
+ layoutParams = LinearLayout.LayoutParams(
233
+ LinearLayout.LayoutParams.WRAP_CONTENT,
234
+ LinearLayout.LayoutParams.WRAP_CONTENT
235
+ ).apply { topMargin = dp(8) }
236
+ text = label
237
+ setTextColor(Color.WHITE)
238
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
239
+ gravity = Gravity.CENTER
240
+ }
241
+ group.addView(lbl)
242
+ return group
243
+ }
244
+
245
+ private fun circle(color: Int) = GradientDrawable().apply {
246
+ shape = GradientDrawable.OVAL
247
+ setColor(color)
248
+ }
249
+
250
+ private fun dp(value: Int) = (value * resources.displayMetrics.density).toInt()
251
+
252
+ // ──────────────────────────────────────────────────────────────────────────
253
+ // Timer
254
+ // ──────────────────────────────────────────────────────────────────────────
255
+
256
+ private fun startCountdown() {
257
+ countDownTimer = object : CountDownTimer(timeout, 1000) {
258
+ override fun onTick(millisUntilFinished: Long) {
259
+ timerView?.text = "Auto-declining in ${millisUntilFinished / 1000}s"
260
+ }
261
+ override fun onFinish() {
262
+ onTimeout()
263
+ }
264
+ }.start()
265
+ }
266
+
267
+ // ──────────────────────────────────────────────────────────────────────────
268
+ // Call actions
269
+ // ──────────────────────────────────────────────────────────────────────────
270
+
271
+ private fun onAnswered() {
272
+ countDownTimer?.cancel()
273
+ broadcast(IncomingCallModule.ACTION_ANSWER)
274
+ cleanup()
275
+ }
276
+
277
+ private fun onRejected() {
278
+ countDownTimer?.cancel()
279
+ broadcast(IncomingCallModule.ACTION_REJECT)
280
+ cleanup()
281
+ }
282
+
283
+ private fun onTimeout() {
284
+ broadcast(IncomingCallModule.ACTION_TIMEOUT)
285
+ cleanup()
286
+ }
287
+
288
+ private fun broadcast(action: String) {
289
+ val intent = Intent(action).apply {
290
+ putExtra("uuid", uuid)
291
+ `package` = packageName
292
+ }
293
+ sendBroadcast(intent)
294
+ }
295
+
296
+ private fun cleanup() {
297
+ stopService(Intent(this, IncomingCallService::class.java))
298
+ finish()
299
+ }
300
+
301
+ // ──────────────────────────────────────────────────────────────────────────
302
+ // Lifecycle
303
+ // ──────────────────────────────────────────────────────────────────────────
304
+
305
+ override fun onDestroy() {
306
+ super.onDestroy()
307
+ countDownTimer?.cancel()
308
+ }
309
+
310
+ @Deprecated("Deprecated in Java")
311
+ override fun onBackPressed() {
312
+ // Prevent back button from dismissing the incoming call screen
313
+ }
314
+ }
@@ -0,0 +1,152 @@
1
+ package com.margelo.nitro.incomingcall
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.content.IntentFilter
7
+ import android.os.Build
8
+ import androidx.core.content.ContextCompat
9
+ import com.facebook.react.bridge.Arguments
10
+ import com.facebook.react.bridge.Promise
11
+ import com.facebook.react.bridge.ReactApplicationContext
12
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
13
+ import com.facebook.react.bridge.ReactMethod
14
+ import com.facebook.react.bridge.ReadableMap
15
+ import com.facebook.react.modules.core.DeviceEventManagerModule
16
+
17
+ class IncomingCallModule(private val reactContext: ReactApplicationContext) :
18
+ ReactContextBaseJavaModule(reactContext) {
19
+
20
+ companion object {
21
+ const val MODULE_NAME = "IncomingCallModule"
22
+ const val ACTION_ANSWER = "com.margelo.nitro.incomingcall.ANSWER"
23
+ const val ACTION_REJECT = "com.margelo.nitro.incomingcall.REJECT"
24
+ const val ACTION_TIMEOUT = "com.margelo.nitro.incomingcall.TIMEOUT"
25
+
26
+ // Shared call state accessible from Activity/Service
27
+ var currentCallUuid: String? = null
28
+ }
29
+
30
+ private val broadcastReceiver = object : BroadcastReceiver() {
31
+ override fun onReceive(context: Context?, intent: Intent?) {
32
+ val uuid = intent?.getStringExtra("uuid") ?: currentCallUuid ?: ""
33
+ when (intent?.action) {
34
+ ACTION_ANSWER -> sendEvent("onAnswer", uuid)
35
+ ACTION_REJECT -> sendEvent("onReject", uuid)
36
+ ACTION_TIMEOUT -> sendEvent("onTimeout", uuid)
37
+ }
38
+ }
39
+ }
40
+
41
+ override fun getName() = MODULE_NAME
42
+
43
+ override fun initialize() {
44
+ super.initialize()
45
+ val filter = IntentFilter().apply {
46
+ addAction(ACTION_ANSWER)
47
+ addAction(ACTION_REJECT)
48
+ addAction(ACTION_TIMEOUT)
49
+ }
50
+ ContextCompat.registerReceiver(
51
+ reactContext,
52
+ broadcastReceiver,
53
+ filter,
54
+ ContextCompat.RECEIVER_NOT_EXPORTED
55
+ )
56
+ }
57
+
58
+ override fun onCatalystInstanceDestroy() {
59
+ super.onCatalystInstanceDestroy()
60
+ try {
61
+ reactContext.unregisterReceiver(broadcastReceiver)
62
+ } catch (_: Exception) {}
63
+ }
64
+
65
+ @ReactMethod
66
+ fun displayIncomingCall(options: ReadableMap, promise: Promise) {
67
+ try {
68
+ val uuid = options.getString("uuid") ?: ""
69
+ val callerName = options.getString("callerName") ?: "Unknown"
70
+ val avatar = if (options.hasKey("avatar")) options.getString("avatar") else null
71
+ val backgroundColor = if (options.hasKey("backgroundColor")) options.getString("backgroundColor") else null
72
+ val callType = if (options.hasKey("callType")) options.getString("callType") else "audio"
73
+ val timeout = if (options.hasKey("timeout")) options.getDouble("timeout").toLong() else 30000L
74
+
75
+ currentCallUuid = uuid
76
+
77
+ val serviceIntent = Intent(reactContext, IncomingCallService::class.java).apply {
78
+ putExtra("uuid", uuid)
79
+ putExtra("callerName", callerName)
80
+ putExtra("avatar", avatar)
81
+ putExtra("backgroundColor", backgroundColor)
82
+ putExtra("callType", callType)
83
+ putExtra("timeout", timeout)
84
+ }
85
+
86
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
87
+ reactContext.startForegroundService(serviceIntent)
88
+ } else {
89
+ reactContext.startService(serviceIntent)
90
+ }
91
+
92
+ promise.resolve(null)
93
+ } catch (e: Exception) {
94
+ promise.reject("DISPLAY_ERROR", e.message, e)
95
+ }
96
+ }
97
+
98
+ @ReactMethod
99
+ fun answerCall(uuid: String, promise: Promise) {
100
+ try {
101
+ stopService()
102
+ sendEvent("onAnswer", uuid)
103
+ promise.resolve(null)
104
+ } catch (e: Exception) {
105
+ promise.reject("ANSWER_ERROR", e.message, e)
106
+ }
107
+ }
108
+
109
+ @ReactMethod
110
+ fun rejectCall(uuid: String, promise: Promise) {
111
+ try {
112
+ stopService()
113
+ sendEvent("onReject", uuid)
114
+ promise.resolve(null)
115
+ } catch (e: Exception) {
116
+ promise.reject("REJECT_ERROR", e.message, e)
117
+ }
118
+ }
119
+
120
+ @ReactMethod
121
+ fun endCall(uuid: String, promise: Promise) {
122
+ try {
123
+ stopService()
124
+ promise.resolve(null)
125
+ } catch (e: Exception) {
126
+ promise.reject("END_ERROR", e.message, e)
127
+ }
128
+ }
129
+
130
+ // Required by NativeEventEmitter on JS side
131
+ @ReactMethod
132
+ fun addListener(eventName: String) {}
133
+
134
+ @ReactMethod
135
+ fun removeListeners(count: Int) {}
136
+
137
+ private fun stopService() {
138
+ currentCallUuid = null
139
+ val serviceIntent = Intent(reactContext, IncomingCallService::class.java)
140
+ reactContext.stopService(serviceIntent)
141
+ }
142
+
143
+ private fun sendEvent(eventName: String, uuid: String) {
144
+ val params = Arguments.createMap().apply {
145
+ putString("uuid", uuid)
146
+ putDouble("timestamp", System.currentTimeMillis().toDouble())
147
+ }
148
+ reactContext
149
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
150
+ .emit(eventName, params)
151
+ }
152
+ }