@qusaieilouti99/call-manager 0.1.97 → 0.1.99

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.
@@ -1,14 +1,16 @@
1
- // File: CallActivity.kt
2
1
  package com.margelo.nitro.qusaieilouti99.callmanager
3
2
 
4
3
  import android.app.Activity
5
4
  import android.app.KeyguardManager
6
5
  import android.content.Context
6
+ import android.graphics.Bitmap
7
+ import android.graphics.BitmapFactory
7
8
  import android.graphics.Color
8
9
  import android.graphics.RenderEffect
9
10
  import android.graphics.Shader
10
- import android.graphics.drawable.Drawable
11
+ import android.graphics.Typeface
11
12
  import android.graphics.drawable.GradientDrawable
13
+ import android.graphics.drawable.RippleDrawable
12
14
  import android.os.Build
13
15
  import android.os.Bundle
14
16
  import android.os.Handler
@@ -23,17 +25,20 @@ import android.widget.FrameLayout
23
25
  import android.widget.ImageView
24
26
  import android.widget.LinearLayout
25
27
  import android.widget.TextView
26
- import androidx.core.content.ContextCompat
27
- import com.bumptech.glide.Glide
28
+ import java.net.HttpURLConnection
29
+ import java.net.URL
28
30
 
29
31
  class CallActivity : Activity(), CallEngine.CallEndListener {
30
- private enum class FinishReason { ANSWER, DECLINE, TIMEOUT,
31
- MANUAL_DISMISS, EXTERNAL_END }
32
+
33
+ private enum class FinishReason {
34
+ ANSWER, DECLINE, TIMEOUT, MANUAL_DISMISS, EXTERNAL_END
35
+ }
32
36
  private var finishReason: FinishReason? = null
33
- private var callId: String = ""
34
- private var callType: String = "Audio"
37
+ private var callId = ""
38
+ private var callType = "Audio"
39
+
35
40
  private val timeoutMs = 60_000L
36
- private val handler = Handler(Looper.getMainLooper())
41
+ private val uiHandler = Handler(Looper.getMainLooper())
37
42
  private val timeoutRunnable = Runnable {
38
43
  finishReason = FinishReason.TIMEOUT
39
44
  CallEngine.stopRingtone()
@@ -44,24 +49,31 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
44
49
 
45
50
  override fun onCreate(savedInstanceState: Bundle?) {
46
51
  super.onCreate(savedInstanceState)
52
+ // Samsung lock‐screen bypass flag
47
53
  val isSamsungBypass = intent.getBooleanExtra(
48
54
  "SAMSUNG_LOCK_SCREEN_BYPASS", false
49
55
  )
50
56
  setupLockScreenBypass(isSamsungBypass)
51
57
 
52
58
  // Read params
53
- callId = intent.getStringExtra("callId") ?: ""
59
+ callId = intent.getStringExtra("callId") ?: ""
54
60
  callType = intent.getStringExtra("callType") ?: "Audio"
55
61
  val callerName = intent.getStringExtra("callerName") ?: "Unknown"
56
62
  val avatarUrl = intent.getStringExtra("callerAvatar")
57
63
 
64
+ // Register for external hang‐up
58
65
  CallEngine.registerCallEndListener(this)
66
+
67
+ // Build all‐code UI
59
68
  buildUi(callerName, avatarUrl)
60
- handler.postDelayed(timeoutRunnable, timeoutMs)
61
- Log.d(TAG, "CallActivity setup complete for $callId")
69
+
70
+ // Auto‐timeout
71
+ uiHandler.postDelayed(timeoutRunnable, timeoutMs)
72
+ Log.d(TAG, "CallActivity setup complete for callId=$callId")
62
73
  }
63
74
 
64
75
  private fun buildUi(name: String, avatarUrl: String?) {
76
+ // Root container
65
77
  val root = FrameLayout(this).apply {
66
78
  layoutParams = ViewGroup.LayoutParams(
67
79
  ViewGroup.LayoutParams.MATCH_PARENT,
@@ -69,27 +81,30 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
69
81
  )
70
82
  }
71
83
 
72
- // 1) Background image + blur
84
+ //
85
+ // 1) Background ImageView + blur
86
+ //
73
87
  val bg = ImageView(this).apply {
74
88
  layoutParams = FrameLayout.LayoutParams(
75
89
  ViewGroup.LayoutParams.MATCH_PARENT,
76
90
  ViewGroup.LayoutParams.MATCH_PARENT
77
91
  )
78
92
  scaleType = ImageView.ScaleType.CENTER_CROP
79
- }
80
- if (!avatarUrl.isNullOrEmpty()) {
81
- Glide.with(this).load(avatarUrl).into(bg)
82
- } else {
83
- bg.setImageResource(android.R.drawable.sym_def_app_icon)
84
- }
85
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
86
- bg.setRenderEffect(
87
- RenderEffect.createBlurEffect(25f, 25f, Shader.TileMode.CLAMP)
88
- )
93
+ // On API 31+ apply live RenderEffect blur
94
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
95
+ setRenderEffect(
96
+ RenderEffect.createBlurEffect(
97
+ 25f, 25f, Shader.TileMode.CLAMP
98
+ )
99
+ )
100
+ }
89
101
  }
90
102
  root.addView(bg)
103
+ loadAndBlurBackground(bg, avatarUrl)
91
104
 
92
- // 2) Dark scrim
105
+ //
106
+ // 2) Semi-transparent scrim
107
+ //
93
108
  root.addView(View(this).apply {
94
109
  layoutParams = FrameLayout.LayoutParams(
95
110
  ViewGroup.LayoutParams.MATCH_PARENT,
@@ -98,12 +113,14 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
98
113
  setBackgroundColor(Color.parseColor("#80000000"))
99
114
  })
100
115
 
101
- // 3) Top call-type label
102
- val callTypeLabel = TextView(this).apply {
103
- text = when (callType.lowercase()) {
104
- "video" -> "Incoming video call"
105
- else -> "Incoming audio call"
106
- }
116
+ //
117
+ // 3) Top “Incoming … call” label
118
+ //
119
+ val typeLabel = TextView(this).apply {
120
+ text = if (callType.equals("video", true))
121
+ "Incoming video call"
122
+ else
123
+ "Incoming audio call"
107
124
  setTextColor(Color.WHITE)
108
125
  setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
109
126
  gravity = Gravity.CENTER
@@ -112,55 +129,42 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
112
129
  ViewGroup.LayoutParams.WRAP_CONTENT
113
130
  ).apply {
114
131
  gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
115
- topMargin = dp(40)
132
+ topMargin = dp(32)
116
133
  }
117
134
  }
118
- root.addView(callTypeLabel)
135
+ root.addView(typeLabel)
119
136
 
120
- // 4) Center content: profile + name
121
- val centerCol = LinearLayout(this).apply {
137
+ //
138
+ // 4) Avatar + Name (in top half)
139
+ //
140
+ val avatarSection = LinearLayout(this).apply {
122
141
  orientation = LinearLayout.VERTICAL
123
142
  gravity = Gravity.CENTER_HORIZONTAL
124
143
  layoutParams = FrameLayout.LayoutParams(
125
144
  ViewGroup.LayoutParams.MATCH_PARENT,
126
145
  ViewGroup.LayoutParams.WRAP_CONTENT
127
146
  ).apply {
128
- gravity = Gravity.CENTER
129
- topMargin = dp(20)
130
- }
131
- }
132
- // Profile circle
133
- val profile = ImageView(this).apply {
134
- val sz = dp(140)
135
- layoutParams = LinearLayout.LayoutParams(sz, sz)
136
- scaleType = ImageView.ScaleType.CENTER_CROP
137
- background = GradientDrawable().apply {
138
- shape = GradientDrawable.OVAL
139
- setColor(Color.LTGRAY)
147
+ gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
148
+ topMargin = dp(100)
140
149
  }
141
- clipToOutline = true
142
- }
143
- if (!avatarUrl.isNullOrEmpty()) {
144
- Glide.with(this).load(avatarUrl).circleCrop().into(profile)
145
150
  }
146
- centerCol.addView(profile)
147
-
148
- // Caller name
149
- centerCol.addView(TextView(this).apply {
151
+ avatarSection.addView(createAvatarView(name, avatarUrl))
152
+ avatarSection.addView(TextView(this).apply {
150
153
  text = name
151
154
  setTextColor(Color.WHITE)
152
155
  setTextSize(TypedValue.COMPLEX_UNIT_SP, 28f)
153
- typeface = android.graphics.Typeface.DEFAULT_BOLD
156
+ typeface = Typeface.DEFAULT_BOLD
154
157
  gravity = Gravity.CENTER
155
158
  layoutParams = LinearLayout.LayoutParams(
156
159
  ViewGroup.LayoutParams.WRAP_CONTENT,
157
160
  ViewGroup.LayoutParams.WRAP_CONTENT
158
- ).apply { topMargin = dp(24) }
161
+ ).apply { topMargin = dp(16) }
159
162
  })
163
+ root.addView(avatarSection)
160
164
 
161
- root.addView(centerCol)
162
-
163
- // 5) Bottom actions
165
+ //
166
+ // 5) Bottom Answer/Decline Buttons
167
+ //
164
168
  val actions = LinearLayout(this).apply {
165
169
  orientation = LinearLayout.HORIZONTAL
166
170
  gravity = Gravity.CENTER
@@ -172,34 +176,145 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
172
176
  bottomMargin = dp(48)
173
177
  }
174
178
  }
175
-
176
- val declineBtn = createCircleButton(
177
- android.R.drawable.ic_menu_close_clear_cancel,
178
- Color.parseColor("#F44336")
179
- ).apply { setOnClickListener { onDecline() } }
180
-
181
- val answerBtn = createCircleButton(
182
- android.R.drawable.ic_menu_call,
183
- Color.parseColor("#4CAF50")
184
- ).apply { setOnClickListener { onAnswer() } }
185
-
186
- actions.addView(declineBtn)
179
+ actions.addView(
180
+ createCircleButton(
181
+ android.R.drawable.ic_menu_close_clear_cancel,
182
+ Color.parseColor("#F44336")
183
+ ).apply { setOnClickListener { onDecline() } }
184
+ )
187
185
  actions.addView(View(this).apply {
188
186
  layoutParams = LinearLayout.LayoutParams(dp(60), 0)
189
187
  })
190
- actions.addView(answerBtn)
188
+ actions.addView(
189
+ createCircleButton(
190
+ android.R.drawable.ic_menu_call,
191
+ Color.parseColor("#4CAF50")
192
+ ).apply { setOnClickListener { onAnswer() } }
193
+ )
191
194
  root.addView(actions)
192
195
 
193
196
  setContentView(root)
194
197
  }
195
198
 
196
- private fun createCircleButton(iconRes: Int, bgColor: Int): FrameLayout {
199
+ /** Creates the top avatar circle: remote image or default + initials. */
200
+ private fun createAvatarView(
201
+ name: String,
202
+ url: String?
203
+ ): FrameLayout {
204
+ val size = dp(140)
205
+ val container = FrameLayout(this).apply {
206
+ layoutParams = LinearLayout.LayoutParams(size, size)
207
+ background = GradientDrawable().apply {
208
+ shape = GradientDrawable.OVAL
209
+ setColor(Color.parseColor("#DDFFFFFF"))
210
+ }
211
+ clipToOutline = true
212
+ }
213
+
214
+ // ImageView for remote or default avatar
215
+ val iv = ImageView(this).apply {
216
+ layoutParams = FrameLayout.LayoutParams(
217
+ ViewGroup.LayoutParams.MATCH_PARENT,
218
+ ViewGroup.LayoutParams.MATCH_PARENT
219
+ )
220
+ scaleType = ImageView.ScaleType.CENTER_CROP
221
+ }
222
+ container.addView(iv)
223
+
224
+ // Initials overlay
225
+ val initials = TextView(this).apply {
226
+ layoutParams = FrameLayout.LayoutParams(
227
+ ViewGroup.LayoutParams.MATCH_PARENT,
228
+ ViewGroup.LayoutParams.MATCH_PARENT
229
+ )
230
+ gravity = Gravity.CENTER
231
+ setTextColor(Color.WHITE)
232
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 32f)
233
+ typeface = Typeface.DEFAULT_BOLD
234
+ text = getInitials(name)
235
+ }
236
+ container.addView(initials)
237
+
238
+ if (url.isNullOrEmpty()) {
239
+ // No remote → show default vector + initials
240
+ iv.setImageResource(R.drawable.ic_default_avatar)
241
+ initials.visibility = View.VISIBLE
242
+ } else {
243
+ // Remote → load + hide initials
244
+ initials.visibility = View.GONE
245
+ loadAvatar(iv, url)
246
+ }
247
+ return container
248
+ }
249
+
250
+ private fun getInitials(fullName: String): String {
251
+ val parts = fullName.trim().split("\\s+".toRegex())
252
+ return when (parts.size) {
253
+ 0 -> ""
254
+ 1 -> parts[0].substring(0,1).uppercase()
255
+ else -> (parts[0][0].toString() + parts[1][0].toString())
256
+ .uppercase()
257
+ }
258
+ }
259
+
260
+ /** Downloads an image + sets it on the ImageView. */
261
+ private fun loadAvatar(iv: ImageView, url: String) {
262
+ Thread {
263
+ try {
264
+ val conn = URL(url).openConnection() as HttpURLConnection
265
+ conn.doInput = true; conn.connect()
266
+ val bmp = BitmapFactory.decodeStream(conn.inputStream)
267
+ runOnUiThread { iv.setImageBitmap(bmp) }
268
+ } catch (_: Exception) { /* ignore */ }
269
+ }.start()
270
+ }
271
+
272
+ /** Downloads bg image (remote or fallback), applies blur if needed. */
273
+ private fun loadAndBlurBackground(iv: ImageView, url: String?) {
274
+ Thread {
275
+ val bmp: Bitmap? = try {
276
+ if (!url.isNullOrEmpty()) {
277
+ val c = URL(url).openConnection() as HttpURLConnection
278
+ c.doInput = true; c.connect()
279
+ BitmapFactory.decodeStream(c.inputStream)
280
+ } else {
281
+ // your fallback background
282
+ BitmapFactory.decodeResource(
283
+ resources, R.drawable.default_call_bg
284
+ )
285
+ }
286
+ } catch (_: Exception) {
287
+ null
288
+ }
289
+ bmp ?: return@Thread
290
+
291
+ val finalBmp = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
292
+ // quick blur: down/up-scale
293
+ val factor = 8
294
+ val w = bmp.width / factor
295
+ val h = bmp.height / factor
296
+ val small = Bitmap.createScaledBitmap(bmp, w, h, true)
297
+ Bitmap.createScaledBitmap(small, bmp.width, bmp.height, true)
298
+ } else {
299
+ bmp
300
+ }
301
+
302
+ runOnUiThread { iv.setImageBitmap(finalBmp) }
303
+ }.start()
304
+ }
305
+
306
+ /** Big round button with circular ripple. */
307
+ private fun createCircleButton(
308
+ iconRes: Int,
309
+ bgColor: Int
310
+ ): FrameLayout {
197
311
  val size = dp(70)
198
312
  return FrameLayout(this).apply {
199
313
  layoutParams = LinearLayout.LayoutParams(size, size)
200
- isClickable = true; isFocusable = true
201
- foreground = getRipple()
202
- // circle bg
314
+ isClickable = true
315
+ isFocusable = true
316
+ foreground = makeCircleRipple()
317
+ // colored circle
203
318
  addView(View(context).apply {
204
319
  layoutParams = FrameLayout.LayoutParams(
205
320
  ViewGroup.LayoutParams.MATCH_PARENT,
@@ -220,12 +335,18 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
220
335
  }
221
336
  }
222
337
 
223
- private fun getRipple(): Drawable? {
224
- val tv = TypedValue()
225
- theme.resolveAttribute(
226
- android.R.attr.selectableItemBackgroundBorderless, tv, true
338
+ /** Circular RippleDrawable. */
339
+ private fun makeCircleRipple(): RippleDrawable {
340
+ val mask = GradientDrawable().apply {
341
+ shape = GradientDrawable.OVAL
342
+ setColor(Color.WHITE)
343
+ }
344
+ val clr = Color.parseColor("#33FFFFFF")
345
+ return RippleDrawable(
346
+ android.content.res.ColorStateList.valueOf(clr),
347
+ null,
348
+ mask
227
349
  )
228
- return ContextCompat.getDrawable(this, tv.resourceId)
229
350
  }
230
351
 
231
352
  private fun onAnswer() {
@@ -235,7 +356,6 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
235
356
  CallEngine.answerCall(callId)
236
357
  finishCallActivity()
237
358
  }
238
-
239
359
  private fun onDecline() {
240
360
  finishReason = FinishReason.DECLINE
241
361
  CallEngine.stopRingtone()
@@ -244,14 +364,10 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
244
364
  finishCallActivity()
245
365
  }
246
366
 
247
- private fun dp(v: Int): Int = TypedValue.applyDimension(
248
- TypedValue.COMPLEX_UNIT_DIP, v.toFloat(),
249
- resources.displayMetrics
250
- ).toInt()
251
-
252
367
  private fun setupLockScreenBypass(isSamsung: Boolean) {
253
368
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
254
- setShowWhenLocked(true); setTurnScreenOn(true)
369
+ setShowWhenLocked(true)
370
+ setTurnScreenOn(true)
255
371
  } else {
256
372
  window.addFlags(
257
373
  WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
@@ -266,19 +382,17 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
266
382
  WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or
267
383
  WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
268
384
  )
269
- val km = getSystemService(Context.KEYGUARD_SERVICE)
270
- as KeyguardManager
271
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
272
- km.requestDismissKeyguard(this,
385
+ (getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager)
386
+ .requestDismissKeyguard(this,
273
387
  object : KeyguardManager.KeyguardDismissCallback() {
274
- override fun onDismissSucceeded() {
388
+ override fun onDismissSucceeded() {
275
389
  Log.d(TAG, "Samsung keyguard dismissed")
276
390
  }
277
391
  override fun onDismissError() {
278
- Log.w(TAG, "Samsung keyguard dismiss error")
392
+ Log.w(TAG, "Keyguard dismiss error")
279
393
  }
280
- })
281
- }
394
+ }
395
+ )
282
396
  }
283
397
  }
284
398
 
@@ -300,7 +414,7 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
300
414
  override fun onDestroy() {
301
415
  super.onDestroy()
302
416
  CallEngine.unregisterCallEndListener(this)
303
- handler.removeCallbacks(timeoutRunnable)
417
+ uiHandler.removeCallbacks(timeoutRunnable)
304
418
  if (finishReason != FinishReason.ANSWER) {
305
419
  CallEngine.stopRingtone()
306
420
  CallEngine.cancelIncomingCallUI()
@@ -316,6 +430,11 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
316
430
  }
317
431
  }
318
432
 
433
+ private fun dp(v: Int): Int = TypedValue.applyDimension(
434
+ TypedValue.COMPLEX_UNIT_DIP, v.toFloat(),
435
+ resources.displayMetrics
436
+ ).toInt()
437
+
319
438
  companion object {
320
439
  private const val TAG = "CallActivity"
321
440
  }
@@ -0,0 +1,18 @@
1
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
2
+ android:width="24dp"
3
+ android:height="24dp"
4
+ android:viewportWidth="24"
5
+ android:viewportHeight="24">
6
+ <path
7
+ android:fillColor="#BDBDBD"
8
+ android:pathData="
9
+ M12,12c2.21,0 4,-1.79 4,-4
10
+ s-1.79,-4 -4,-4
11
+ s-4,1.79 -4,4
12
+ s1.79,4 4,4z
13
+ M6,20
14
+ c0,-3.31 2.69,-6 6,-6
15
+ s6,2.69 6,6
16
+ v1
17
+ H6v-1z"/>
18
+ </vector>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.97",
3
+ "version": "0.1.99",
4
4
  "description": "Call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",