@qusaieilouti99/call-manager 0.1.99 → 0.1.101

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,3 +1,4 @@
1
+ // File: CallActivity.kt
1
2
  package com.margelo.nitro.qusaieilouti99.callmanager
2
3
 
3
4
  import android.app.Activity
@@ -49,31 +50,23 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
49
50
 
50
51
  override fun onCreate(savedInstanceState: Bundle?) {
51
52
  super.onCreate(savedInstanceState)
52
- // Samsung lock‐screen bypass flag
53
53
  val isSamsungBypass = intent.getBooleanExtra(
54
54
  "SAMSUNG_LOCK_SCREEN_BYPASS", false
55
55
  )
56
56
  setupLockScreenBypass(isSamsungBypass)
57
57
 
58
- // Read params
59
- callId = intent.getStringExtra("callId") ?: ""
60
- callType = intent.getStringExtra("callType") ?: "Audio"
58
+ callId = intent.getStringExtra("callId") ?: ""
59
+ callType = intent.getStringExtra("callType") ?: "Audio"
61
60
  val callerName = intent.getStringExtra("callerName") ?: "Unknown"
62
61
  val avatarUrl = intent.getStringExtra("callerAvatar")
63
62
 
64
- // Register for external hang‐up
65
63
  CallEngine.registerCallEndListener(this)
66
-
67
- // Build all‐code UI
68
64
  buildUi(callerName, avatarUrl)
69
-
70
- // Auto‐timeout
71
65
  uiHandler.postDelayed(timeoutRunnable, timeoutMs)
72
66
  Log.d(TAG, "CallActivity setup complete for callId=$callId")
73
67
  }
74
68
 
75
69
  private fun buildUi(name: String, avatarUrl: String?) {
76
- // Root container
77
70
  val root = FrameLayout(this).apply {
78
71
  layoutParams = ViewGroup.LayoutParams(
79
72
  ViewGroup.LayoutParams.MATCH_PARENT,
@@ -81,20 +74,17 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
81
74
  )
82
75
  }
83
76
 
84
- //
85
- // 1) Background ImageView + blur
86
- //
77
+ // 1) Full-screen background + blur
87
78
  val bg = ImageView(this).apply {
88
79
  layoutParams = FrameLayout.LayoutParams(
89
80
  ViewGroup.LayoutParams.MATCH_PARENT,
90
81
  ViewGroup.LayoutParams.MATCH_PARENT
91
82
  )
92
83
  scaleType = ImageView.ScaleType.CENTER_CROP
93
- // On API 31+ apply live RenderEffect blur
94
84
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
95
85
  setRenderEffect(
96
86
  RenderEffect.createBlurEffect(
97
- 25f, 25f, Shader.TileMode.CLAMP
87
+ 50f, 50f, Shader.TileMode.CLAMP
98
88
  )
99
89
  )
100
90
  }
@@ -102,9 +92,7 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
102
92
  root.addView(bg)
103
93
  loadAndBlurBackground(bg, avatarUrl)
104
94
 
105
- //
106
- // 2) Semi-transparent scrim
107
- //
95
+ // 2) Dark scrim
108
96
  root.addView(View(this).apply {
109
97
  layoutParams = FrameLayout.LayoutParams(
110
98
  ViewGroup.LayoutParams.MATCH_PARENT,
@@ -113,9 +101,7 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
113
101
  setBackgroundColor(Color.parseColor("#80000000"))
114
102
  })
115
103
 
116
- //
117
- // 3) Top “Incoming … call” label
118
- //
104
+ // 3) “Incoming ... call” with extra top padding (48dp)
119
105
  val typeLabel = TextView(this).apply {
120
106
  text = if (callType.equals("video", true))
121
107
  "Incoming video call"
@@ -129,14 +115,12 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
129
115
  ViewGroup.LayoutParams.WRAP_CONTENT
130
116
  ).apply {
131
117
  gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
132
- topMargin = dp(32)
118
+ topMargin = dp(48)
133
119
  }
134
120
  }
135
121
  root.addView(typeLabel)
136
122
 
137
- //
138
- // 4) Avatar + Name (in top half)
139
- //
123
+ // 4) Avatar + name in top half
140
124
  val avatarSection = LinearLayout(this).apply {
141
125
  orientation = LinearLayout.VERTICAL
142
126
  gravity = Gravity.CENTER_HORIZONTAL
@@ -162,9 +146,7 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
162
146
  })
163
147
  root.addView(avatarSection)
164
148
 
165
- //
166
- // 5) Bottom Answer/Decline Buttons
167
- //
149
+ // 5) Bottom buttons with extra bottom padding (64dp)
168
150
  val actions = LinearLayout(this).apply {
169
151
  orientation = LinearLayout.HORIZONTAL
170
152
  gravity = Gravity.CENTER
@@ -173,7 +155,7 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
173
155
  ViewGroup.LayoutParams.WRAP_CONTENT
174
156
  ).apply {
175
157
  gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
176
- bottomMargin = dp(48)
158
+ bottomMargin = dp(64)
177
159
  }
178
160
  }
179
161
  actions.addView(
@@ -183,7 +165,7 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
183
165
  ).apply { setOnClickListener { onDecline() } }
184
166
  )
185
167
  actions.addView(View(this).apply {
186
- layoutParams = LinearLayout.LayoutParams(dp(60), 0)
168
+ layoutParams = LinearLayout.LayoutParams(dp(100), 0)
187
169
  })
188
170
  actions.addView(
189
171
  createCircleButton(
@@ -196,22 +178,24 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
196
178
  setContentView(root)
197
179
  }
198
180
 
199
- /** Creates the top avatar circle: remote image or default + initials. */
200
- private fun createAvatarView(
201
- name: String,
202
- url: String?
203
- ): FrameLayout {
181
+ private fun createAvatarView(name: String, url: String?): FrameLayout {
204
182
  val size = dp(140)
183
+ // Gradient purple background
184
+ val gradientBg = GradientDrawable(
185
+ GradientDrawable.Orientation.TL_BR,
186
+ intArrayOf(
187
+ Color.parseColor("#8E24AA"),
188
+ Color.parseColor("#CE93D8")
189
+ )
190
+ ).apply { shape = GradientDrawable.OVAL }
191
+
205
192
  val container = FrameLayout(this).apply {
206
193
  layoutParams = LinearLayout.LayoutParams(size, size)
207
- background = GradientDrawable().apply {
208
- shape = GradientDrawable.OVAL
209
- setColor(Color.parseColor("#DDFFFFFF"))
210
- }
194
+ background = gradientBg
211
195
  clipToOutline = true
212
196
  }
213
197
 
214
- // ImageView for remote or default avatar
198
+ // ImageView (only visible when URL != null)
215
199
  val iv = ImageView(this).apply {
216
200
  layoutParams = FrameLayout.LayoutParams(
217
201
  ViewGroup.LayoutParams.MATCH_PARENT,
@@ -221,7 +205,7 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
221
205
  }
222
206
  container.addView(iv)
223
207
 
224
- // Initials overlay
208
+ // Initials TextView
225
209
  val initials = TextView(this).apply {
226
210
  layoutParams = FrameLayout.LayoutParams(
227
211
  ViewGroup.LayoutParams.MATCH_PARENT,
@@ -236,14 +220,14 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
236
220
  container.addView(initials)
237
221
 
238
222
  if (url.isNullOrEmpty()) {
239
- // No remote → show default vector + initials
240
- iv.setImageResource(R.drawable.ic_default_avatar)
223
+ iv.visibility = View.GONE
241
224
  initials.visibility = View.VISIBLE
242
225
  } else {
243
- // Remote → load + hide initials
226
+ iv.visibility = View.VISIBLE
244
227
  initials.visibility = View.GONE
245
228
  loadAvatar(iv, url)
246
229
  }
230
+
247
231
  return container
248
232
  }
249
233
 
@@ -251,13 +235,11 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
251
235
  val parts = fullName.trim().split("\\s+".toRegex())
252
236
  return when (parts.size) {
253
237
  0 -> ""
254
- 1 -> parts[0].substring(0,1).uppercase()
255
- else -> (parts[0][0].toString() + parts[1][0].toString())
256
- .uppercase()
238
+ 1 -> parts[0].substring(0, 1).uppercase()
239
+ else -> (parts[0][0].toString() + parts[1][0].toString()).uppercase()
257
240
  }
258
241
  }
259
242
 
260
- /** Downloads an image + sets it on the ImageView. */
261
243
  private fun loadAvatar(iv: ImageView, url: String) {
262
244
  Thread {
263
245
  try {
@@ -265,11 +247,10 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
265
247
  conn.doInput = true; conn.connect()
266
248
  val bmp = BitmapFactory.decodeStream(conn.inputStream)
267
249
  runOnUiThread { iv.setImageBitmap(bmp) }
268
- } catch (_: Exception) { /* ignore */ }
250
+ } catch (_: Exception) { }
269
251
  }.start()
270
252
  }
271
253
 
272
- /** Downloads bg image (remote or fallback), applies blur if needed. */
273
254
  private fun loadAndBlurBackground(iv: ImageView, url: String?) {
274
255
  Thread {
275
256
  val bmp: Bitmap? = try {
@@ -278,19 +259,16 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
278
259
  c.doInput = true; c.connect()
279
260
  BitmapFactory.decodeStream(c.inputStream)
280
261
  } else {
281
- // your fallback background
282
262
  BitmapFactory.decodeResource(
283
263
  resources, R.drawable.default_call_bg
284
264
  )
285
265
  }
286
- } catch (_: Exception) {
287
- null
288
- }
266
+ } catch (_: Exception) { null }
289
267
  bmp ?: return@Thread
290
268
 
291
269
  val finalBmp = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
292
- // quick blur: down/up-scale
293
- val factor = 8
270
+ // heavier blur via 16× down/up-scale
271
+ val factor = 16
294
272
  val w = bmp.width / factor
295
273
  val h = bmp.height / factor
296
274
  val small = Bitmap.createScaledBitmap(bmp, w, h, true)
@@ -303,18 +281,15 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
303
281
  }.start()
304
282
  }
305
283
 
306
- /** Big round button with circular ripple. */
307
- private fun createCircleButton(
308
- iconRes: Int,
309
- bgColor: Int
310
- ): FrameLayout {
284
+ private fun createCircleButton(iconRes: Int, bgColor: Int): FrameLayout {
311
285
  val size = dp(70)
312
286
  return FrameLayout(this).apply {
313
287
  layoutParams = LinearLayout.LayoutParams(size, size)
314
288
  isClickable = true
315
289
  isFocusable = true
316
290
  foreground = makeCircleRipple()
317
- // colored circle
291
+
292
+ // circular background
318
293
  addView(View(context).apply {
319
294
  layoutParams = FrameLayout.LayoutParams(
320
295
  ViewGroup.LayoutParams.MATCH_PARENT,
@@ -325,6 +300,7 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
325
300
  setColor(bgColor)
326
301
  }
327
302
  })
303
+
328
304
  // icon
329
305
  addView(ImageView(context).apply {
330
306
  layoutParams = FrameLayout.LayoutParams(dp(36), dp(36))
@@ -335,18 +311,15 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
335
311
  }
336
312
  }
337
313
 
338
- /** Circular RippleDrawable. */
339
314
  private fun makeCircleRipple(): RippleDrawable {
340
315
  val mask = GradientDrawable().apply {
341
316
  shape = GradientDrawable.OVAL
342
317
  setColor(Color.WHITE)
343
318
  }
344
- val clr = Color.parseColor("#33FFFFFF")
345
- return RippleDrawable(
346
- android.content.res.ColorStateList.valueOf(clr),
347
- null,
348
- mask
319
+ val color = android.content.res.ColorStateList.valueOf(
320
+ Color.parseColor("#33FFFFFF")
349
321
  )
322
+ return RippleDrawable(color, null, mask)
350
323
  }
351
324
 
352
325
  private fun onAnswer() {
@@ -356,6 +329,7 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
356
329
  CallEngine.answerCall(callId)
357
330
  finishCallActivity()
358
331
  }
332
+
359
333
  private fun onDecline() {
360
334
  finishReason = FinishReason.DECLINE
361
335
  CallEngine.stopRingtone()
@@ -391,8 +365,7 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
391
365
  override fun onDismissError() {
392
366
  Log.w(TAG, "Keyguard dismiss error")
393
367
  }
394
- }
395
- )
368
+ })
396
369
  }
397
370
  }
398
371
 
@@ -431,7 +404,8 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
431
404
  }
432
405
 
433
406
  private fun dp(v: Int): Int = TypedValue.applyDimension(
434
- TypedValue.COMPLEX_UNIT_DIP, v.toFloat(),
407
+ TypedValue.COMPLEX_UNIT_DIP,
408
+ v.toFloat(),
435
409
  resources.displayMetrics
436
410
  ).toInt()
437
411
 
@@ -38,6 +38,11 @@ import java.util.concurrent.ConcurrentHashMap
38
38
  import java.util.concurrent.CopyOnWriteArrayList
39
39
  import java.util.concurrent.atomic.AtomicBoolean
40
40
  import android.app.KeyguardManager
41
+ import android.content.ContentResolver
42
+ import android.database.ContentObserver
43
+ import android.os.Vibrator
44
+ import android.os.VibrationEffect
45
+ import android.provider.Settings
41
46
  /**
42
47
  * Core call‐management engine. Manages self-managed telecom calls,
43
48
  * audio routing, UI notifications, etc.
@@ -102,6 +107,10 @@ object CallEngine {
102
107
  private var eventHandler: ((CallEventType, String) -> Unit)? = null
103
108
  private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
104
109
 
110
+ private var ringtone: android.media.Ringtone? = null
111
+ private var vibrator: Vibrator? = null
112
+ private var volumeObserver: ContentObserver? = null
113
+
105
114
  interface LockScreenBypassCallback {
106
115
  fun onLockScreenBypassChanged(shouldBypass: Boolean)
107
116
  }
@@ -255,7 +264,7 @@ object CallEngine {
255
264
  currentCallId = callId
256
265
  Log.d(TAG, "Call $callId added to activeCalls. State: INCOMING")
257
266
 
258
- showIncomingCallUI(callId, displayName, callType)
267
+ showIncomingCallUI(callId, displayName, callType, pictureUrl)
259
268
  registerPhoneAccount()
260
269
 
261
270
  val telecomManager =
@@ -916,17 +925,17 @@ object CallEngine {
916
925
  }
917
926
  }
918
927
 
919
- private fun showIncomingCallUI(callId: String, callerName: String, callType: String) {
928
+ private fun showIncomingCallUI(callId: String, callerName: String, callType: String, callerPicUrl: String?) {
920
929
  val context = requireContext()
921
930
  Log.d(TAG, "Showing incoming call UI for $callId")
922
931
 
923
932
  // **SIMPLE LOGIC**: If locked -> overlay, if unlocked -> notification
924
933
  if (isDeviceLocked(context)) {
925
934
  Log.d(TAG, "Device is locked - using overlay approach")
926
- showCallActivityOverlay(context, callId, callerName, callType)
935
+ showCallActivityOverlay(context, callId, callerName, callType, callerPicUrl)
927
936
  } else {
928
937
  Log.d(TAG, "Device is unlocked - using standard notification")
929
- showStandardNotification(context, callId, callerName, callType)
938
+ showStandardNotification(context, callId, callerName, callType, callerPicUrl)
930
939
  }
931
940
 
932
941
  playRingtone()
@@ -946,7 +955,7 @@ object CallEngine {
946
955
  /**
947
956
  * Shows the call activity directly as overlay (when device is locked)
948
957
  */
949
- private fun showCallActivityOverlay(context: Context, callId: String, callerName: String, callType: String) {
958
+ private fun showCallActivityOverlay(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
950
959
  val overlayIntent = Intent(context, CallActivity::class.java).apply {
951
960
  addFlags(
952
961
  Intent.FLAG_ACTIVITY_NEW_TASK or
@@ -957,6 +966,10 @@ object CallEngine {
957
966
  putExtra("callId", callId)
958
967
  putExtra("callerName", callerName)
959
968
  putExtra("callType", callType)
969
+ if (callerPicUrl != null) {
970
+ putExtra("callAvatar", callerPicUrl)
971
+ }
972
+
960
973
  putExtra("LOCK_SCREEN_MODE", true) // Flag to identify lock screen mode
961
974
  }
962
975
 
@@ -978,14 +991,14 @@ object CallEngine {
978
991
  } catch (e: Exception) {
979
992
  Log.e(TAG, "Overlay failed, falling back to notification: ${e.message}")
980
993
  // If overlay fails, fall back to notification
981
- showStandardNotification(context, callId, callerName, callType)
994
+ showStandardNotification(context, callId, callerName, callType, callerPicUrl)
982
995
  }
983
996
  }
984
997
 
985
998
  /**
986
999
  * Shows standard notification with full-screen intent (when device is unlocked)
987
1000
  */
988
- private fun showStandardNotification(context: Context, callId: String, callerName: String, callType: String) {
1001
+ private fun showStandardNotification(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
989
1002
  createNotificationChannel()
990
1003
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
991
1004
 
@@ -994,6 +1007,9 @@ object CallEngine {
994
1007
  putExtra("callId", callId)
995
1008
  putExtra("callerName", callerName)
996
1009
  putExtra("callType", callType)
1010
+ if (callerPicUrl != null) {
1011
+ putExtra("callAvatar", callerPicUrl)
1012
+ }
997
1013
  // No lock screen flag - standard notification mode
998
1014
  }
999
1015
 
@@ -1195,13 +1211,46 @@ object CallEngine {
1195
1211
  // --- Media Management ---
1196
1212
  private fun playRingtone() {
1197
1213
  val context = requireContext()
1214
+
1215
+ // 1) start a repeating vibration
1216
+ vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
1217
+ vibrator?.let { v ->
1218
+ val pattern = longArrayOf(0L, 500L, 500L) // wait, vibrate, pause
1219
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1220
+ v.vibrate(VibrationEffect.createWaveform(pattern, 0 /* repeat idx */))
1221
+ } else {
1222
+ @Suppress("DEPRECATION")
1223
+ v.vibrate(pattern, 0)
1224
+ }
1225
+ }
1226
+
1227
+ // 2) play the ringtone
1198
1228
  try {
1199
1229
  val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
1200
1230
  ringtone = RingtoneManager.getRingtone(context, uri)
1201
1231
  ringtone?.play()
1202
- Log.d(TAG, "Ringtone started playing")
1203
1232
  } catch (e: Exception) {
1204
- Log.e(TAG, "Failed to play ringtone: ${e.message}", e)
1233
+ Log.e(TAG, "Failed to play ringtone", e)
1234
+ }
1235
+
1236
+ // 3) register a ContentObserver on the ring-volume setting
1237
+ if (volumeObserver == null) {
1238
+ val resolver: ContentResolver = context.contentResolver
1239
+ volumeObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
1240
+ override fun onChange(selfChange: Boolean, uri: Uri?) {
1241
+ super.onChange(selfChange, uri)
1242
+ // only react to ring-volume changes
1243
+ if (uri == Settings.System.getUriFor(Settings.System.VOLUME_RING)) {
1244
+ Log.d(TAG, "Ring volume changed, silencing ringtone")
1245
+ stopRingtone()
1246
+ }
1247
+ }
1248
+ }
1249
+ resolver.registerContentObserver(
1250
+ Settings.System.getUriFor(Settings.System.VOLUME_RING),
1251
+ false,
1252
+ volumeObserver!!
1253
+ )
1205
1254
  }
1206
1255
  }
1207
1256
 
@@ -1210,9 +1259,19 @@ object CallEngine {
1210
1259
  ringtone?.stop()
1211
1260
  Log.d(TAG, "Ringtone stopped")
1212
1261
  } catch (e: Exception) {
1213
- Log.e(TAG, "Error stopping ringtone: ${e.message}")
1262
+ Log.e(TAG, "Error stopping ringtone", e)
1214
1263
  }
1215
1264
  ringtone = null
1265
+
1266
+ // cancel vibration
1267
+ vibrator?.cancel()
1268
+ vibrator = null
1269
+
1270
+ // unregister observer
1271
+ volumeObserver?.let {
1272
+ requireContext().contentResolver.unregisterContentObserver(it)
1273
+ volumeObserver = null
1274
+ }
1216
1275
  }
1217
1276
 
1218
1277
  private fun startRingback() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.99",
3
+ "version": "0.1.101",
4
4
  "description": "Call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -1,18 +0,0 @@
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>