@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.
- package/IncomingCall.podspec +29 -0
- package/LICENSE +20 -0
- package/README.md +280 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +118 -0
- package/android/src/main/AndroidManifest.xml +35 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/incomingcall/IncomingCall.kt +219 -0
- package/android/src/main/java/com/margelo/nitro/incomingcall/IncomingCallActivity.kt +314 -0
- package/android/src/main/java/com/margelo/nitro/incomingcall/IncomingCallModule.kt +152 -0
- package/android/src/main/java/com/margelo/nitro/incomingcall/IncomingCallPackage.kt +31 -0
- package/android/src/main/java/com/margelo/nitro/incomingcall/IncomingCallService.kt +109 -0
- package/ios/IncomingCall.swift +28 -0
- package/lib/module/IncomingCall.nitro.js +4 -0
- package/lib/module/IncomingCall.nitro.js.map +1 -0
- package/lib/module/index.js +137 -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/IncomingCall.nitro.d.ts +21 -0
- package/lib/typescript/src/IncomingCall.nitro.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +43 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +17 -0
- package/nitrogen/generated/android/c++/JHybridIncomingCallSpec.cpp +101 -0
- package/nitrogen/generated/android/c++/JHybridIncomingCallSpec.hpp +73 -0
- package/nitrogen/generated/android/c++/views/JHybridIncomingCallStateUpdater.cpp +72 -0
- package/nitrogen/generated/android/c++/views/JHybridIncomingCallStateUpdater.hpp +49 -0
- package/nitrogen/generated/android/incomingcall+autolinking.cmake +83 -0
- package/nitrogen/generated/android/incomingcall+autolinking.gradle +27 -0
- package/nitrogen/generated/android/incomingcallOnLoad.cpp +56 -0
- package/nitrogen/generated/android/incomingcallOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/incomingcall/HybridIncomingCallSpec.kt +87 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/incomingcall/incomingcallOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/incomingcall/views/HybridIncomingCallManager.kt +70 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/incomingcall/views/HybridIncomingCallStateUpdater.kt +23 -0
- package/nitrogen/generated/ios/IncomingCall+autolinking.rb +60 -0
- package/nitrogen/generated/ios/IncomingCall-Swift-Cxx-Bridge.cpp +33 -0
- package/nitrogen/generated/ios/IncomingCall-Swift-Cxx-Bridge.hpp +83 -0
- package/nitrogen/generated/ios/IncomingCall-Swift-Cxx-Umbrella.hpp +45 -0
- package/nitrogen/generated/ios/IncomingCallAutolinking.mm +33 -0
- package/nitrogen/generated/ios/IncomingCallAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridIncomingCallSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridIncomingCallSpecSwift.hpp +121 -0
- package/nitrogen/generated/ios/c++/views/HybridIncomingCallComponent.mm +127 -0
- package/nitrogen/generated/ios/swift/HybridIncomingCallSpec.swift +60 -0
- package/nitrogen/generated/ios/swift/HybridIncomingCallSpec_cxx.swift +270 -0
- package/nitrogen/generated/shared/c++/HybridIncomingCallSpec.cpp +32 -0
- package/nitrogen/generated/shared/c++/HybridIncomingCallSpec.hpp +73 -0
- package/nitrogen/generated/shared/c++/views/HybridIncomingCallComponent.cpp +127 -0
- package/nitrogen/generated/shared/c++/views/HybridIncomingCallComponent.hpp +115 -0
- package/nitrogen/generated/shared/json/IncomingCallConfig.json +14 -0
- package/package.json +180 -0
- package/src/IncomingCall.nitro.ts +27 -0
- 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
|
+
}
|