@qusaieilouti99/call-manager 0.1.98 → 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,15 +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
7
  import android.graphics.BitmapFactory
8
8
  import android.graphics.Color
9
9
  import android.graphics.RenderEffect
10
10
  import android.graphics.Shader
11
- import android.graphics.drawable.Drawable
11
+ import android.graphics.Typeface
12
12
  import android.graphics.drawable.GradientDrawable
13
+ import android.graphics.drawable.RippleDrawable
13
14
  import android.os.Build
14
15
  import android.os.Bundle
15
16
  import android.os.Handler
@@ -24,19 +25,20 @@ import android.widget.FrameLayout
24
25
  import android.widget.ImageView
25
26
  import android.widget.LinearLayout
26
27
  import android.widget.TextView
27
- import androidx.core.content.ContextCompat
28
28
  import java.net.HttpURLConnection
29
29
  import java.net.URL
30
30
 
31
31
  class CallActivity : Activity(), CallEngine.CallEndListener {
32
32
 
33
- private enum class FinishReason { ANSWER, DECLINE, TIMEOUT,
34
- MANUAL_DISMISS, EXTERNAL_END }
33
+ private enum class FinishReason {
34
+ ANSWER, DECLINE, TIMEOUT, MANUAL_DISMISS, EXTERNAL_END
35
+ }
35
36
  private var finishReason: FinishReason? = null
36
- private var callId: String = ""
37
- private var callType: String = "Audio"
37
+ private var callId = ""
38
+ private var callType = "Audio"
39
+
38
40
  private val timeoutMs = 60_000L
39
- private val handler = Handler(Looper.getMainLooper())
41
+ private val uiHandler = Handler(Looper.getMainLooper())
40
42
  private val timeoutRunnable = Runnable {
41
43
  finishReason = FinishReason.TIMEOUT
42
44
  CallEngine.stopRingtone()
@@ -47,24 +49,31 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
47
49
 
48
50
  override fun onCreate(savedInstanceState: Bundle?) {
49
51
  super.onCreate(savedInstanceState)
52
+ // Samsung lock‐screen bypass flag
50
53
  val isSamsungBypass = intent.getBooleanExtra(
51
54
  "SAMSUNG_LOCK_SCREEN_BYPASS", false
52
55
  )
53
56
  setupLockScreenBypass(isSamsungBypass)
54
57
 
55
58
  // Read params
56
- callId = intent.getStringExtra("callId") ?: ""
57
- callType = intent.getStringExtra("callType") ?: "Audio"
59
+ callId = intent.getStringExtra("callId") ?: ""
60
+ callType = intent.getStringExtra("callType") ?: "Audio"
58
61
  val callerName = intent.getStringExtra("callerName") ?: "Unknown"
59
62
  val avatarUrl = intent.getStringExtra("callerAvatar")
60
63
 
64
+ // Register for external hang‐up
61
65
  CallEngine.registerCallEndListener(this)
66
+
67
+ // Build all‐code UI
62
68
  buildUi(callerName, avatarUrl)
63
- handler.postDelayed(timeoutRunnable, timeoutMs)
69
+
70
+ // Auto‐timeout
71
+ uiHandler.postDelayed(timeoutRunnable, timeoutMs)
64
72
  Log.d(TAG, "CallActivity setup complete for callId=$callId")
65
73
  }
66
74
 
67
75
  private fun buildUi(name: String, avatarUrl: String?) {
76
+ // Root container
68
77
  val root = FrameLayout(this).apply {
69
78
  layoutParams = ViewGroup.LayoutParams(
70
79
  ViewGroup.LayoutParams.MATCH_PARENT,
@@ -72,14 +81,16 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
72
81
  )
73
82
  }
74
83
 
75
- // 1) Background ImageView + optional blur
84
+ //
85
+ // 1) Background ImageView + blur
86
+ //
76
87
  val bg = ImageView(this).apply {
77
88
  layoutParams = FrameLayout.LayoutParams(
78
89
  ViewGroup.LayoutParams.MATCH_PARENT,
79
90
  ViewGroup.LayoutParams.MATCH_PARENT
80
91
  )
81
92
  scaleType = ImageView.ScaleType.CENTER_CROP
82
- // apply RenderEffect blur on API31+
93
+ // On API 31+ apply live RenderEffect blur
83
94
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
84
95
  setRenderEffect(
85
96
  RenderEffect.createBlurEffect(
@@ -91,7 +102,9 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
91
102
  root.addView(bg)
92
103
  loadAndBlurBackground(bg, avatarUrl)
93
104
 
94
- // 2) Dark scrim
105
+ //
106
+ // 2) Semi-transparent scrim
107
+ //
95
108
  root.addView(View(this).apply {
96
109
  layoutParams = FrameLayout.LayoutParams(
97
110
  ViewGroup.LayoutParams.MATCH_PARENT,
@@ -100,12 +113,14 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
100
113
  setBackgroundColor(Color.parseColor("#80000000"))
101
114
  })
102
115
 
103
- // 3) Top call-type label
104
- val callTypeLabel = TextView(this).apply {
105
- text = when (callType.lowercase()) {
106
- "video" -> "Incoming video call"
107
- else -> "Incoming audio call"
108
- }
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"
109
124
  setTextColor(Color.WHITE)
110
125
  setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
111
126
  gravity = Gravity.CENTER
@@ -114,55 +129,42 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
114
129
  ViewGroup.LayoutParams.WRAP_CONTENT
115
130
  ).apply {
116
131
  gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
117
- topMargin = dp(40)
132
+ topMargin = dp(32)
118
133
  }
119
134
  }
120
- root.addView(callTypeLabel)
135
+ root.addView(typeLabel)
121
136
 
122
- // 4) Center content: profile + name
123
- val centerCol = LinearLayout(this).apply {
137
+ //
138
+ // 4) Avatar + Name (in top half)
139
+ //
140
+ val avatarSection = LinearLayout(this).apply {
124
141
  orientation = LinearLayout.VERTICAL
125
142
  gravity = Gravity.CENTER_HORIZONTAL
126
143
  layoutParams = FrameLayout.LayoutParams(
127
144
  ViewGroup.LayoutParams.MATCH_PARENT,
128
145
  ViewGroup.LayoutParams.WRAP_CONTENT
129
146
  ).apply {
130
- gravity = Gravity.CENTER
131
- topMargin = dp(20)
132
- }
133
- }
134
-
135
- // Profile circle
136
- val profile = ImageView(this).apply {
137
- val sz = dp(140)
138
- layoutParams = LinearLayout.LayoutParams(sz, sz)
139
- scaleType = ImageView.ScaleType.CENTER_CROP
140
- background = GradientDrawable().apply {
141
- shape = GradientDrawable.OVAL
142
- setColor(Color.LTGRAY)
147
+ gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
148
+ topMargin = dp(100)
143
149
  }
144
- // enable clipping to our oval background
145
- clipToOutline = true
146
150
  }
147
- centerCol.addView(profile)
148
- loadAvatar(profile, avatarUrl)
149
-
150
- // Caller name
151
- centerCol.addView(TextView(this).apply {
151
+ avatarSection.addView(createAvatarView(name, avatarUrl))
152
+ avatarSection.addView(TextView(this).apply {
152
153
  text = name
153
154
  setTextColor(Color.WHITE)
154
155
  setTextSize(TypedValue.COMPLEX_UNIT_SP, 28f)
155
- typeface = android.graphics.Typeface.DEFAULT_BOLD
156
+ typeface = Typeface.DEFAULT_BOLD
156
157
  gravity = Gravity.CENTER
157
158
  layoutParams = LinearLayout.LayoutParams(
158
159
  ViewGroup.LayoutParams.WRAP_CONTENT,
159
160
  ViewGroup.LayoutParams.WRAP_CONTENT
160
- ).apply { topMargin = dp(24) }
161
+ ).apply { topMargin = dp(16) }
161
162
  })
163
+ root.addView(avatarSection)
162
164
 
163
- root.addView(centerCol)
164
-
165
- // 5) Bottom actions
165
+ //
166
+ // 5) Bottom Answer/Decline Buttons
167
+ //
166
168
  val actions = LinearLayout(this).apply {
167
169
  orientation = LinearLayout.HORIZONTAL
168
170
  gravity = Gravity.CENTER
@@ -174,30 +176,89 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
174
176
  bottomMargin = dp(48)
175
177
  }
176
178
  }
177
-
178
- val declineBtn = createCircleButton(
179
- android.R.drawable.ic_menu_close_clear_cancel,
180
- Color.parseColor("#F44336")
181
- ).apply { setOnClickListener { onDecline() } }
182
-
183
- val answerBtn = createCircleButton(
184
- android.R.drawable.ic_menu_call,
185
- Color.parseColor("#4CAF50")
186
- ).apply { setOnClickListener { onAnswer() } }
187
-
188
- 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
+ )
189
185
  actions.addView(View(this).apply {
190
186
  layoutParams = LinearLayout.LayoutParams(dp(60), 0)
191
187
  })
192
- actions.addView(answerBtn)
188
+ actions.addView(
189
+ createCircleButton(
190
+ android.R.drawable.ic_menu_call,
191
+ Color.parseColor("#4CAF50")
192
+ ).apply { setOnClickListener { onAnswer() } }
193
+ )
193
194
  root.addView(actions)
194
195
 
195
196
  setContentView(root)
196
197
  }
197
198
 
198
- // Plain HTTP loader + setImageBitmap
199
- private fun loadAvatar(iv: ImageView, url: String?) {
200
- if (url.isNullOrEmpty()) return
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) {
201
262
  Thread {
202
263
  try {
203
264
  val conn = URL(url).openConnection() as HttpURLConnection
@@ -208,25 +269,51 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
208
269
  }.start()
209
270
  }
210
271
 
211
- // Same loader for background
272
+ /** Downloads bg image (remote or fallback), applies blur if needed. */
212
273
  private fun loadAndBlurBackground(iv: ImageView, url: String?) {
213
- if (url.isNullOrEmpty()) return
214
274
  Thread {
215
- try {
216
- val conn = URL(url).openConnection() as HttpURLConnection
217
- conn.doInput = true; conn.connect()
218
- val bmp = BitmapFactory.decodeStream(conn.inputStream)
219
- runOnUiThread { iv.setImageBitmap(bmp) }
220
- } catch (_: Exception) { /* ignore */ }
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) }
221
303
  }.start()
222
304
  }
223
305
 
224
- private fun createCircleButton(iconRes: Int, bgColor: Int): FrameLayout {
306
+ /** Big round button with circular ripple. */
307
+ private fun createCircleButton(
308
+ iconRes: Int,
309
+ bgColor: Int
310
+ ): FrameLayout {
225
311
  val size = dp(70)
226
312
  return FrameLayout(this).apply {
227
313
  layoutParams = LinearLayout.LayoutParams(size, size)
228
- isClickable = true; isFocusable = true
229
- foreground = getRipple()
314
+ isClickable = true
315
+ isFocusable = true
316
+ foreground = makeCircleRipple()
230
317
  // colored circle
231
318
  addView(View(context).apply {
232
319
  layoutParams = FrameLayout.LayoutParams(
@@ -248,12 +335,18 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
248
335
  }
249
336
  }
250
337
 
251
- private fun getRipple(): Drawable? {
252
- val tv = TypedValue()
253
- theme.resolveAttribute(
254
- 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
255
349
  )
256
- return ContextCompat.getDrawable(this, tv.resourceId)
257
350
  }
258
351
 
259
352
  private fun onAnswer() {
@@ -263,7 +356,6 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
263
356
  CallEngine.answerCall(callId)
264
357
  finishCallActivity()
265
358
  }
266
-
267
359
  private fun onDecline() {
268
360
  finishReason = FinishReason.DECLINE
269
361
  CallEngine.stopRingtone()
@@ -272,14 +364,10 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
272
364
  finishCallActivity()
273
365
  }
274
366
 
275
- private fun dp(v: Int): Int = TypedValue.applyDimension(
276
- TypedValue.COMPLEX_UNIT_DIP, v.toFloat(),
277
- resources.displayMetrics
278
- ).toInt()
279
-
280
367
  private fun setupLockScreenBypass(isSamsung: Boolean) {
281
368
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
282
- setShowWhenLocked(true); setTurnScreenOn(true)
369
+ setShowWhenLocked(true)
370
+ setTurnScreenOn(true)
283
371
  } else {
284
372
  window.addFlags(
285
373
  WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
@@ -294,19 +382,17 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
294
382
  WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or
295
383
  WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
296
384
  )
297
- val km = getSystemService(Context.KEYGUARD_SERVICE)
298
- as KeyguardManager
299
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
300
- km.requestDismissKeyguard(this,
385
+ (getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager)
386
+ .requestDismissKeyguard(this,
301
387
  object : KeyguardManager.KeyguardDismissCallback() {
302
388
  override fun onDismissSucceeded() {
303
389
  Log.d(TAG, "Samsung keyguard dismissed")
304
390
  }
305
391
  override fun onDismissError() {
306
- Log.w(TAG, "Samsung keyguard dismiss error")
392
+ Log.w(TAG, "Keyguard dismiss error")
307
393
  }
308
- })
309
- }
394
+ }
395
+ )
310
396
  }
311
397
  }
312
398
 
@@ -328,7 +414,7 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
328
414
  override fun onDestroy() {
329
415
  super.onDestroy()
330
416
  CallEngine.unregisterCallEndListener(this)
331
- handler.removeCallbacks(timeoutRunnable)
417
+ uiHandler.removeCallbacks(timeoutRunnable)
332
418
  if (finishReason != FinishReason.ANSWER) {
333
419
  CallEngine.stopRingtone()
334
420
  CallEngine.cancelIncomingCallUI()
@@ -344,6 +430,11 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
344
430
  }
345
431
  }
346
432
 
433
+ private fun dp(v: Int): Int = TypedValue.applyDimension(
434
+ TypedValue.COMPLEX_UNIT_DIP, v.toFloat(),
435
+ resources.displayMetrics
436
+ ).toInt()
437
+
347
438
  companion object {
348
439
  private const val TAG = "CallActivity"
349
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.98",
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",