@qusaieilouti99/call-manager 0.1.147 → 0.1.148
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,7 +1,6 @@
|
|
|
1
1
|
package com.margelo.nitro.qusaieilouti99.callmanager
|
|
2
2
|
|
|
3
3
|
import android.app.ActivityManager
|
|
4
|
-
import android.app.KeyguardManager
|
|
5
4
|
import android.app.Notification
|
|
6
5
|
import android.app.NotificationChannel
|
|
7
6
|
import android.app.NotificationManager
|
|
@@ -9,6 +8,8 @@ import android.app.PendingIntent
|
|
|
9
8
|
import android.content.ComponentName
|
|
10
9
|
import android.content.Context
|
|
11
10
|
import android.content.Intent
|
|
11
|
+
import android.graphics.Color
|
|
12
|
+
import android.media.AudioAttributes
|
|
12
13
|
import android.media.AudioDeviceCallback
|
|
13
14
|
import android.media.AudioDeviceInfo
|
|
14
15
|
import android.media.AudioManager
|
|
@@ -20,8 +21,6 @@ import android.os.Bundle
|
|
|
20
21
|
import android.os.Handler
|
|
21
22
|
import android.os.Looper
|
|
22
23
|
import android.os.PowerManager
|
|
23
|
-
import android.os.VibrationEffect
|
|
24
|
-
import android.os.Vibrator
|
|
25
24
|
import android.telecom.Connection
|
|
26
25
|
import android.telecom.DisconnectCause
|
|
27
26
|
import android.telecom.PhoneAccount
|
|
@@ -33,857 +32,1357 @@ import org.json.JSONObject
|
|
|
33
32
|
import java.util.concurrent.ConcurrentHashMap
|
|
34
33
|
import java.util.concurrent.CopyOnWriteArrayList
|
|
35
34
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
35
|
+
import android.app.KeyguardManager
|
|
36
|
+
import android.os.Vibrator
|
|
37
|
+
import android.os.VibrationEffect
|
|
36
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Core call‐management engine. Manages self-managed telecom calls,
|
|
41
|
+
* audio routing, UI notifications, etc.
|
|
42
|
+
*
|
|
43
|
+
* NOTE: Volume key silencing is now handled by the system via `Connection.onSilence()`,
|
|
44
|
+
* which calls `silenceIncomingCall()` on this object.
|
|
45
|
+
*/
|
|
37
46
|
object CallEngine {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
private const val TAG = "CallEngine"
|
|
48
|
+
private const val PHONE_ACCOUNT_ID = "com.qusaieilouti99.callmanager.SELF_MANAGED"
|
|
49
|
+
private const val NOTIF_CHANNEL_ID = "incoming_call_channel"
|
|
50
|
+
private const val NOTIF_ID = 2001
|
|
51
|
+
|
|
52
|
+
interface CallEndListener {
|
|
53
|
+
fun onCallEnded(callId: String)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private val callEndListeners = CopyOnWriteArrayList<CallEndListener>()
|
|
57
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
58
|
+
|
|
59
|
+
fun registerCallEndListener(l: CallEndListener) {
|
|
60
|
+
callEndListeners.add(l)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fun unregisterCallEndListener(l: CallEndListener) {
|
|
64
|
+
callEndListeners.remove(l)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Volatile private var appContext: Context? = null
|
|
68
|
+
private val isInitialized = AtomicBoolean(false)
|
|
69
|
+
private val initializationLock = Any()
|
|
70
|
+
|
|
71
|
+
private var ringtone: android.media.Ringtone? = null
|
|
72
|
+
private var ringbackPlayer: MediaPlayer? = null
|
|
73
|
+
private var vibrator: Vibrator? = null
|
|
74
|
+
private var audioManager: AudioManager? = null
|
|
75
|
+
private var wakeLock: PowerManager.WakeLock? = null
|
|
76
|
+
|
|
77
|
+
private val activeCalls = ConcurrentHashMap<String, CallInfo>()
|
|
78
|
+
private val telecomConnections = ConcurrentHashMap<String, Connection>()
|
|
79
|
+
private val callMetadata = ConcurrentHashMap<String, String>()
|
|
80
|
+
|
|
81
|
+
// NEW: Track incoming calls to prevent duplicates
|
|
82
|
+
private val incomingCallIds = ConcurrentHashMap<String, Long>()
|
|
83
|
+
|
|
84
|
+
private var currentCallId: String? = null
|
|
85
|
+
private var canMakeMultipleCalls: Boolean = false
|
|
86
|
+
private var lastAudioRoutesInfo: AudioRoutesInfo? = null
|
|
87
|
+
private var lockScreenBypassActive = false
|
|
88
|
+
private val lockScreenBypassCallbacks = mutableSetOf<LockScreenBypassCallback>()
|
|
89
|
+
private var eventHandler: ((CallEventType, String) -> Unit)? = null
|
|
90
|
+
private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
|
|
91
|
+
|
|
92
|
+
// NEW: Track ringtone state to prevent double ringing
|
|
93
|
+
private var isCustomRingtoneActive = false
|
|
94
|
+
private val ringtoneStateLock = Any()
|
|
95
|
+
|
|
96
|
+
interface LockScreenBypassCallback {
|
|
97
|
+
fun onLockScreenBypassChanged(shouldBypass: Boolean)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fun initialize(context: Context) {
|
|
101
|
+
synchronized(initializationLock) {
|
|
102
|
+
if (isInitialized.compareAndSet(false, true)) {
|
|
103
|
+
appContext = context.applicationContext
|
|
104
|
+
audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
|
105
|
+
Log.d(TAG, "CallEngine initialized successfully")
|
|
106
|
+
if (isCallActive()) {
|
|
107
|
+
startForegroundService()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
45
110
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fun isInitialized(): Boolean = isInitialized.get()
|
|
114
|
+
|
|
115
|
+
private fun requireContext(): Context {
|
|
116
|
+
return appContext ?: throw IllegalStateException(
|
|
117
|
+
"CallEngine not initialized. Call initialize() in Application.onCreate()"
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get the application context. Returns null if not initialized.
|
|
123
|
+
*/
|
|
124
|
+
fun getContext(): Context? = appContext
|
|
125
|
+
|
|
126
|
+
fun setEventHandler(handler: ((CallEventType, String) -> Unit)?) {
|
|
127
|
+
Log.d(TAG, "setEventHandler called. Handler present: ${handler != null}")
|
|
128
|
+
eventHandler = handler
|
|
129
|
+
handler?.let { h ->
|
|
130
|
+
if (cachedEvents.isNotEmpty()) {
|
|
131
|
+
Log.d(TAG, "Emitting ${cachedEvents.size} cached events.")
|
|
132
|
+
cachedEvents.forEach { (type, data) -> h.invoke(type, data) }
|
|
133
|
+
cachedEvents.clear()
|
|
134
|
+
}
|
|
49
135
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fun emitEvent(type: CallEventType, data: JSONObject) {
|
|
139
|
+
Log.d(TAG, "Emitting event: $type")
|
|
140
|
+
val dataString = data.toString()
|
|
141
|
+
if (eventHandler != null) {
|
|
142
|
+
eventHandler?.invoke(type, dataString)
|
|
143
|
+
} else {
|
|
144
|
+
Log.d(TAG, "No event handler, caching event: $type")
|
|
145
|
+
cachedEvents.add(Pair(type, dataString))
|
|
56
146
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* NEW: Check if device supports CallStyle notifications
|
|
151
|
+
*/
|
|
152
|
+
private fun supportsCallStyleNotifications(): Boolean {
|
|
153
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return false
|
|
154
|
+
|
|
155
|
+
val manufacturer = Build.MANUFACTURER.lowercase()
|
|
156
|
+
val brand = Build.BRAND.lowercase()
|
|
157
|
+
|
|
158
|
+
// Known good manufacturers that support CallStyle properly
|
|
159
|
+
val supportedManufacturers = setOf(
|
|
160
|
+
"google", "samsung", "oneplus", "motorola", "sony", "lg", "htc"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
val supportedBrands = setOf(
|
|
164
|
+
"google", "samsung", "oneplus", "motorola", "sony", "lg", "htc", "pixel"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
val isSupported = supportedManufacturers.contains(manufacturer) ||
|
|
168
|
+
supportedBrands.contains(brand) ||
|
|
169
|
+
manufacturer.contains("google") ||
|
|
170
|
+
brand.contains("pixel")
|
|
171
|
+
|
|
172
|
+
Log.d(TAG, "CallStyle support check - Manufacturer: $manufacturer, Brand: $brand, Supported: $isSupported")
|
|
173
|
+
return isSupported
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Silences the incoming call ringtone. This is called by `Connection.onSilence()`
|
|
178
|
+
* when the user presses a volume key during ringing.
|
|
179
|
+
*/
|
|
180
|
+
fun silenceIncomingCall() {
|
|
181
|
+
Log.d(TAG, "Silencing incoming call ringtone via Connection.onSilence()")
|
|
182
|
+
stopRingtone()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fun registerLockScreenBypassCallback(callback: LockScreenBypassCallback) {
|
|
186
|
+
lockScreenBypassCallbacks.add(callback)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fun unregisterLockScreenBypassCallback(callback: LockScreenBypassCallback) {
|
|
190
|
+
lockScreenBypassCallbacks.remove(callback)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private fun updateLockScreenBypass() {
|
|
194
|
+
val shouldBypass = isCallActive()
|
|
195
|
+
if (lockScreenBypassActive != shouldBypass) {
|
|
196
|
+
lockScreenBypassActive = shouldBypass
|
|
197
|
+
Log.d(TAG, "Lock screen bypass state changed: $lockScreenBypassActive")
|
|
198
|
+
lockScreenBypassCallbacks.forEach { callback ->
|
|
199
|
+
try {
|
|
200
|
+
callback.onLockScreenBypassChanged(shouldBypass)
|
|
201
|
+
} catch (e: Exception) {
|
|
202
|
+
Log.w(TAG, "Error notifying lock screen bypass callback", e)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
60
205
|
}
|
|
206
|
+
}
|
|
61
207
|
|
|
62
|
-
|
|
63
|
-
private val isInitialized = AtomicBoolean(false)
|
|
64
|
-
private val initializationLock = Any()
|
|
65
|
-
|
|
66
|
-
private var ringtone: android.media.Ringtone? = null
|
|
67
|
-
private var ringbackPlayer: MediaPlayer? = null
|
|
68
|
-
private var vibrator: Vibrator? = null
|
|
69
|
-
private var audioManager: AudioManager? = null
|
|
70
|
-
private var wakeLock: PowerManager.WakeLock? = null
|
|
208
|
+
fun isLockScreenBypassActive(): Boolean = lockScreenBypassActive
|
|
71
209
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
210
|
+
fun addTelecomConnection(callId: String, connection: Connection) {
|
|
211
|
+
telecomConnections[callId] = connection
|
|
212
|
+
Log.d(TAG, "Added Telecom Connection for callId: $callId")
|
|
213
|
+
}
|
|
76
214
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
215
|
+
fun removeTelecomConnection(callId: String) {
|
|
216
|
+
telecomConnections.remove(callId)
|
|
217
|
+
Log.d(TAG, "Removed Telecom Connection for callId: $callId")
|
|
218
|
+
}
|
|
81
219
|
|
|
82
|
-
|
|
83
|
-
private val audioStateLock = Any()
|
|
220
|
+
fun getTelecomConnection(callId: String): Connection? = telecomConnections[callId]
|
|
84
221
|
|
|
85
|
-
|
|
86
|
-
|
|
222
|
+
fun setCanMakeMultipleCalls(allow: Boolean) {
|
|
223
|
+
canMakeMultipleCalls = allow
|
|
224
|
+
Log.d(TAG, "canMakeMultipleCalls set to: $allow")
|
|
225
|
+
}
|
|
87
226
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
227
|
+
fun getCurrentCallState(): String {
|
|
228
|
+
val calls = getActiveCalls()
|
|
229
|
+
val jsonArray = JSONArray()
|
|
230
|
+
calls.forEach {
|
|
231
|
+
jsonArray.put(it.toJsonObject())
|
|
232
|
+
}
|
|
233
|
+
return jsonArray.toString()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fun reportIncomingCall(
|
|
237
|
+
context: Context,
|
|
238
|
+
callId: String,
|
|
239
|
+
callType: String,
|
|
240
|
+
displayName: String,
|
|
241
|
+
pictureUrl: String? = null,
|
|
242
|
+
metadata: String? = null
|
|
243
|
+
) {
|
|
244
|
+
if (!isInitialized.get()) {
|
|
245
|
+
initialize(context)
|
|
97
246
|
}
|
|
98
247
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
248
|
+
// NEW: Guard against duplicate calls
|
|
249
|
+
val currentTime = System.currentTimeMillis()
|
|
250
|
+
val lastCallTime = incomingCallIds[callId]
|
|
251
|
+
if (lastCallTime != null && (currentTime - lastCallTime) < 5000) {
|
|
252
|
+
Log.w(TAG, "Ignoring duplicate incoming call for callId: $callId (last call ${currentTime - lastCallTime}ms ago)")
|
|
253
|
+
return
|
|
105
254
|
}
|
|
255
|
+
incomingCallIds[callId] = currentTime
|
|
106
256
|
|
|
107
|
-
|
|
257
|
+
// Clean up old entries (older than 30 seconds)
|
|
258
|
+
val cutoffTime = currentTime - 30000
|
|
259
|
+
incomingCallIds.entries.removeAll { it.value < cutoffTime }
|
|
108
260
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
}
|
|
261
|
+
Log.d(TAG, "reportIncomingCall: callId=$callId, type=$callType, name=$displayName")
|
|
262
|
+
metadata?.let { callMetadata[callId] = it }
|
|
263
|
+
|
|
264
|
+
val incomingCall = activeCalls.values.find { it.state == CallState.INCOMING }
|
|
265
|
+
if (incomingCall != null && incomingCall.callId != callId) {
|
|
266
|
+
Log.d(TAG, "Incoming call collision detected. Auto-rejecting new call: $callId")
|
|
267
|
+
rejectIncomingCallCollision(callId, "Another call is already incoming")
|
|
268
|
+
return
|
|
119
269
|
}
|
|
120
270
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
cachedEvents.add(Pair(type, dataString))
|
|
129
|
-
}
|
|
271
|
+
val activeCall = activeCalls.values.find {
|
|
272
|
+
it.state == CallState.ACTIVE || it.state == CallState.HELD
|
|
273
|
+
}
|
|
274
|
+
if (activeCall != null && !canMakeMultipleCalls) {
|
|
275
|
+
Log.d(TAG, "Active call exists when receiving incoming call. Auto-rejecting: $callId")
|
|
276
|
+
rejectIncomingCallCollision(callId, "Another call is already active")
|
|
277
|
+
return
|
|
130
278
|
}
|
|
131
279
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
280
|
+
val isVideoCall = callType == "Video"
|
|
281
|
+
if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
|
|
282
|
+
activeCalls.values.forEach {
|
|
283
|
+
if (it.state == CallState.ACTIVE) {
|
|
284
|
+
holdCallInternal(it.callId, heldBySystem = false)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
137
287
|
}
|
|
138
288
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
289
|
+
activeCalls[callId] =
|
|
290
|
+
CallInfo(callId, callType, displayName, pictureUrl, CallState.INCOMING)
|
|
291
|
+
currentCallId = callId
|
|
292
|
+
Log.d(TAG, "Call $callId added to activeCalls. State: INCOMING")
|
|
293
|
+
|
|
294
|
+
showIncomingCallUI(callId, displayName, callType, pictureUrl)
|
|
295
|
+
registerPhoneAccount()
|
|
296
|
+
|
|
297
|
+
val telecomManager =
|
|
298
|
+
requireContext().getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
299
|
+
val phoneAccountHandle = getPhoneAccountHandle()
|
|
300
|
+
val extras = Bundle().apply {
|
|
301
|
+
putString(MyConnectionService.EXTRA_CALL_ID, callId)
|
|
302
|
+
putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
|
|
303
|
+
putString(MyConnectionService.EXTRA_DISPLAY_NAME, displayName)
|
|
304
|
+
putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, isVideoCall)
|
|
305
|
+
pictureUrl?.let { putString(MyConnectionService.EXTRA_PICTURE_URL, it) }
|
|
142
306
|
}
|
|
143
307
|
|
|
144
|
-
|
|
145
|
-
|
|
308
|
+
try {
|
|
309
|
+
telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
|
|
310
|
+
startForegroundService()
|
|
311
|
+
Log.d(TAG, "Successfully reported incoming call to TelecomManager for $callId")
|
|
312
|
+
} catch (e: Exception) {
|
|
313
|
+
Log.e(TAG, "Failed to report incoming call: ${e.message}", e)
|
|
314
|
+
endCallInternal(callId)
|
|
146
315
|
}
|
|
147
316
|
|
|
148
|
-
|
|
149
|
-
|
|
317
|
+
updateLockScreenBypass()
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
fun startOutgoingCall(
|
|
321
|
+
callId: String,
|
|
322
|
+
callType: String,
|
|
323
|
+
targetName: String,
|
|
324
|
+
metadata: String? = null
|
|
325
|
+
) {
|
|
326
|
+
val context = requireContext()
|
|
327
|
+
Log.d(TAG, "startOutgoingCall: callId=$callId, type=$callType, target=$targetName")
|
|
328
|
+
metadata?.let { callMetadata[callId] = it }
|
|
329
|
+
|
|
330
|
+
if (!validateOutgoingCallRequest()) {
|
|
331
|
+
Log.w(TAG, "Rejecting outgoing call - incoming/active call exists")
|
|
332
|
+
emitEvent(CallEventType.CALL_REJECTED, JSONObject().apply {
|
|
333
|
+
put("callId", callId)
|
|
334
|
+
put("reason", "Cannot start outgoing call while incoming or active call exists")
|
|
335
|
+
})
|
|
336
|
+
return
|
|
150
337
|
}
|
|
151
338
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
lockScreenBypassCallbacks.forEach { callback ->
|
|
158
|
-
try {
|
|
159
|
-
callback.onLockScreenBypassChanged(shouldBypass)
|
|
160
|
-
} catch (e: Exception) {
|
|
161
|
-
Log.w(TAG, "Error notifying lock screen bypass callback", e)
|
|
162
|
-
}
|
|
163
|
-
}
|
|
339
|
+
val isVideoCall = callType == "Video"
|
|
340
|
+
if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
|
|
341
|
+
activeCalls.values.forEach {
|
|
342
|
+
if (it.state == CallState.ACTIVE) {
|
|
343
|
+
holdCallInternal(it.callId, heldBySystem = false)
|
|
164
344
|
}
|
|
345
|
+
}
|
|
165
346
|
}
|
|
166
347
|
|
|
167
|
-
|
|
348
|
+
activeCalls[callId] = CallInfo(callId, callType, targetName, null, CallState.DIALING)
|
|
349
|
+
currentCallId = callId
|
|
350
|
+
Log.d(TAG, "Call $callId added to activeCalls. State: DIALING")
|
|
351
|
+
|
|
352
|
+
setAudioMode()
|
|
353
|
+
registerPhoneAccount()
|
|
354
|
+
|
|
355
|
+
val telecomManager =
|
|
356
|
+
context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
357
|
+
val phoneAccountHandle = getPhoneAccountHandle()
|
|
358
|
+
val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
|
|
168
359
|
|
|
169
|
-
|
|
170
|
-
|
|
360
|
+
val outgoingExtras = Bundle().apply {
|
|
361
|
+
putString(MyConnectionService.EXTRA_CALL_ID, callId)
|
|
362
|
+
putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
|
|
363
|
+
putString(MyConnectionService.EXTRA_DISPLAY_NAME, targetName)
|
|
364
|
+
putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, isVideoCall)
|
|
365
|
+
metadata?.let { putString("metadata", it) }
|
|
171
366
|
}
|
|
172
367
|
|
|
173
|
-
|
|
174
|
-
|
|
368
|
+
val extras = Bundle().apply {
|
|
369
|
+
putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
|
|
370
|
+
putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
|
|
371
|
+
putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, isVideoCall)
|
|
175
372
|
}
|
|
176
373
|
|
|
177
|
-
|
|
374
|
+
try {
|
|
375
|
+
telecomManager.placeCall(addressUri, extras)
|
|
376
|
+
startForegroundService()
|
|
377
|
+
startRingback()
|
|
378
|
+
bringAppToForeground()
|
|
379
|
+
keepScreenAwake(true)
|
|
380
|
+
Log.d(TAG, "Successfully reported outgoing call to TelecomManager")
|
|
381
|
+
} catch (e: Exception) {
|
|
382
|
+
Log.e(TAG, "Failed to start outgoing call: ${e.message}", e)
|
|
383
|
+
endCallInternal(callId)
|
|
384
|
+
}
|
|
178
385
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
386
|
+
updateLockScreenBypass()
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
fun startCall(
|
|
390
|
+
callId: String,
|
|
391
|
+
callType: String,
|
|
392
|
+
targetName: String,
|
|
393
|
+
metadata: String? = null
|
|
394
|
+
) {
|
|
395
|
+
Log.d(TAG, "startCall: callId=$callId, type=$callType, target=$targetName")
|
|
396
|
+
metadata?.let { callMetadata[callId] = it }
|
|
397
|
+
|
|
398
|
+
if (activeCalls.containsKey(callId)) {
|
|
399
|
+
Log.w(TAG, "Call $callId already exists, cannot start again")
|
|
400
|
+
return
|
|
401
|
+
}
|
|
190
402
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
incomingCallIds[callId] = currentTime
|
|
197
|
-
|
|
198
|
-
Log.d(TAG, "reportIncomingCall: callId=$callId, type=$callType, name=$displayName")
|
|
199
|
-
metadata?.let { callMetadata[callId] = it }
|
|
200
|
-
|
|
201
|
-
activeCalls[callId] =
|
|
202
|
-
CallInfo(callId, callType, displayName, pictureUrl, CallState.INCOMING)
|
|
203
|
-
currentCallId = callId
|
|
204
|
-
|
|
205
|
-
showIncomingCallUI(callId, displayName, callType, pictureUrl)
|
|
206
|
-
registerPhoneAccount()
|
|
207
|
-
|
|
208
|
-
val telecomManager =
|
|
209
|
-
requireContext().getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
210
|
-
val phoneAccountHandle = getPhoneAccountHandle()
|
|
211
|
-
val extras = Bundle().apply {
|
|
212
|
-
putString(MyConnectionService.EXTRA_CALL_ID, callId)
|
|
213
|
-
putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
|
|
214
|
-
putString(MyConnectionService.EXTRA_DISPLAY_NAME, displayName)
|
|
215
|
-
putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, callType == "Video")
|
|
216
|
-
pictureUrl?.let { putString(MyConnectionService.EXTRA_PICTURE_URL, it) }
|
|
403
|
+
if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
|
|
404
|
+
activeCalls.values.forEach {
|
|
405
|
+
if (it.state == CallState.ACTIVE) {
|
|
406
|
+
holdCallInternal(it.callId, heldBySystem = false)
|
|
217
407
|
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
218
410
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
411
|
+
activeCalls[callId] =
|
|
412
|
+
CallInfo(callId, callType, targetName, null, CallState.ACTIVE)
|
|
413
|
+
currentCallId = callId
|
|
414
|
+
Log.d(TAG, "Call $callId started as ACTIVE")
|
|
415
|
+
|
|
416
|
+
registerPhoneAccount()
|
|
417
|
+
setAudioMode()
|
|
418
|
+
bringAppToForeground()
|
|
419
|
+
startForegroundService()
|
|
420
|
+
keepScreenAwake(true)
|
|
421
|
+
|
|
422
|
+
// NEW: Improved initial audio route setting with better timing
|
|
423
|
+
mainHandler.postDelayed({
|
|
424
|
+
setInitialAudioRoute(callType, isCallStart = true)
|
|
425
|
+
}, 500L)
|
|
426
|
+
|
|
427
|
+
updateLockScreenBypass()
|
|
428
|
+
emitOutgoingCallAnsweredWithMetadata(callId)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
fun callAnsweredFromJS(callId: String) {
|
|
432
|
+
Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
|
|
433
|
+
coreCallAnswered(callId, isLocalAnswer = false)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
fun answerCall(callId: String) {
|
|
437
|
+
Log.d(TAG, "answerCall: $callId - local party answering")
|
|
438
|
+
coreCallAnswered(callId, isLocalAnswer = true)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
|
|
442
|
+
Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
|
|
443
|
+
val callInfo = activeCalls[callId]
|
|
444
|
+
if (callInfo == null) {
|
|
445
|
+
Log.w(TAG, "Cannot answer call $callId - not found in active calls")
|
|
446
|
+
return
|
|
227
447
|
}
|
|
228
448
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
targetName: String,
|
|
233
|
-
metadata: String? = null
|
|
234
|
-
) {
|
|
235
|
-
val context = requireContext()
|
|
236
|
-
Log.d(TAG, "startOutgoingCall: callId=$callId, type=$callType, target=$targetName")
|
|
237
|
-
metadata?.let { callMetadata[callId] = it }
|
|
238
|
-
|
|
239
|
-
activeCalls[callId] = CallInfo(callId, callType, targetName, null, CallState.DIALING)
|
|
240
|
-
currentCallId = callId
|
|
241
|
-
Log.d(TAG, "Call $callId added to activeCalls. State: DIALING")
|
|
242
|
-
|
|
243
|
-
registerPhoneAccount()
|
|
244
|
-
|
|
245
|
-
val telecomManager =
|
|
246
|
-
context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
247
|
-
val phoneAccountHandle = getPhoneAccountHandle()
|
|
248
|
-
val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
|
|
249
|
-
|
|
250
|
-
val outgoingExtras = Bundle().apply {
|
|
251
|
-
putString(MyConnectionService.EXTRA_CALL_ID, callId)
|
|
252
|
-
putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
|
|
253
|
-
putString(MyConnectionService.EXTRA_DISPLAY_NAME, targetName)
|
|
254
|
-
putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, callType == "Video")
|
|
255
|
-
metadata?.let { putString("metadata", it) }
|
|
256
|
-
}
|
|
449
|
+
activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
|
|
450
|
+
currentCallId = callId
|
|
451
|
+
Log.d(TAG, "Call $callId set to ACTIVE state")
|
|
257
452
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, callType == "Video")
|
|
262
|
-
}
|
|
453
|
+
stopRingtone()
|
|
454
|
+
stopRingback()
|
|
455
|
+
cancelIncomingCallUI()
|
|
263
456
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
bringAppToForeground()
|
|
269
|
-
keepScreenAwake(true)
|
|
270
|
-
Log.d(TAG, "Successfully reported outgoing call to TelecomManager")
|
|
271
|
-
} catch (e: Exception) {
|
|
272
|
-
Log.e(TAG, "Failed to start outgoing call: ${e.message}", e)
|
|
273
|
-
endCallInternal(callId)
|
|
457
|
+
if (!canMakeMultipleCalls) {
|
|
458
|
+
activeCalls.filter { it.key != callId }.values.forEach { otherCall ->
|
|
459
|
+
if (otherCall.state == CallState.ACTIVE) {
|
|
460
|
+
holdCallInternal(otherCall.callId, heldBySystem = false)
|
|
274
461
|
}
|
|
275
|
-
|
|
462
|
+
}
|
|
276
463
|
}
|
|
277
464
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
metadata: String? = null
|
|
283
|
-
) {
|
|
284
|
-
Log.d(TAG, "startCall: callId=$callId, type=$callType, target=$targetName")
|
|
285
|
-
metadata?.let { callMetadata[callId] = it }
|
|
286
|
-
|
|
287
|
-
if (activeCalls.containsKey(callId)) {
|
|
288
|
-
Log.w(TAG, "Call $callId already exists, cannot start again")
|
|
289
|
-
return
|
|
290
|
-
}
|
|
465
|
+
bringAppToForeground()
|
|
466
|
+
startForegroundService()
|
|
467
|
+
keepScreenAwake(true)
|
|
468
|
+
updateLockScreenBypass()
|
|
291
469
|
|
|
292
|
-
|
|
293
|
-
CallInfo(callId, callType, targetName, null, CallState.ACTIVE)
|
|
294
|
-
currentCallId = callId
|
|
295
|
-
Log.d(TAG, "Call $callId started as ACTIVE")
|
|
470
|
+
setAudioMode()
|
|
296
471
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
keepScreenAwake(true)
|
|
472
|
+
// NEW: Improved initial audio route with longer delay and retry mechanism
|
|
473
|
+
mainHandler.postDelayed({
|
|
474
|
+
setInitialAudioRoute(callInfo.callType, isCallStart = true)
|
|
301
475
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
476
|
+
// Retry after additional delay if needed for video calls
|
|
477
|
+
if (callInfo.callType == "Video") {
|
|
305
478
|
mainHandler.postDelayed({
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
coreCallAnswered(callId, isLocalAnswer = false)
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
fun answerCall(callId: String) {
|
|
323
|
-
Log.d(TAG, "answerCall: $callId - local party answering")
|
|
324
|
-
coreCallAnswered(callId, isLocalAnswer = true)
|
|
479
|
+
val currentRoute = getCurrentAudioRoute()
|
|
480
|
+
if (currentRoute != "Speaker") {
|
|
481
|
+
Log.d(TAG, "Retrying audio route for video call - current: $currentRoute")
|
|
482
|
+
setAudioRoute("Speaker")
|
|
483
|
+
}
|
|
484
|
+
}, 1000L)
|
|
485
|
+
}
|
|
486
|
+
}, 800L)
|
|
487
|
+
|
|
488
|
+
if (isLocalAnswer) {
|
|
489
|
+
emitCallAnsweredWithMetadata(callId)
|
|
490
|
+
} else {
|
|
491
|
+
emitOutgoingCallAnsweredWithMetadata(callId)
|
|
325
492
|
}
|
|
326
493
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
|
|
330
|
-
currentCallId = callId
|
|
494
|
+
Log.d(TAG, "Call $callId successfully answered")
|
|
495
|
+
}
|
|
331
496
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
497
|
+
private fun emitCallAnsweredWithMetadata(callId: String) {
|
|
498
|
+
val callInfo = activeCalls[callId] ?: return
|
|
499
|
+
val metadata = callMetadata[callId]
|
|
335
500
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
501
|
+
emitEvent(CallEventType.CALL_ANSWERED, JSONObject().apply {
|
|
502
|
+
put("callId", callId)
|
|
503
|
+
put("callType", callInfo.callType)
|
|
504
|
+
put("displayName", callInfo.displayName)
|
|
505
|
+
callInfo.pictureUrl?.let { put("pictureUrl", it) }
|
|
506
|
+
metadata?.let {
|
|
507
|
+
try {
|
|
508
|
+
put("metadata", JSONObject(it))
|
|
509
|
+
} catch (e: Exception) {
|
|
510
|
+
put("metadata", it)
|
|
342
511
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
512
|
+
}
|
|
513
|
+
})
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private fun emitOutgoingCallAnsweredWithMetadata(callId: String) {
|
|
517
|
+
val callInfo = activeCalls[callId] ?: return
|
|
518
|
+
val metadata = callMetadata[callId]
|
|
519
|
+
|
|
520
|
+
emitEvent(CallEventType.OUTGOING_CALL_ANSWERED, JSONObject().apply {
|
|
521
|
+
put("callId", callId)
|
|
522
|
+
put("callType", callInfo.callType)
|
|
523
|
+
put("displayName", callInfo.displayName)
|
|
524
|
+
callInfo.pictureUrl?.let { put("pictureUrl", it) }
|
|
525
|
+
metadata?.let {
|
|
526
|
+
try {
|
|
527
|
+
put("metadata", JSONObject(it))
|
|
528
|
+
} catch (e: Exception) {
|
|
529
|
+
put("metadata", it)
|
|
349
530
|
}
|
|
350
|
-
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
fun holdCall(callId: String) {
|
|
536
|
+
holdCallInternal(callId, heldBySystem = false)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
fun setOnHold(callId: String, onHold: Boolean) {
|
|
540
|
+
Log.d(TAG, "setOnHold: $callId, onHold: $onHold")
|
|
541
|
+
val callInfo = activeCalls[callId]
|
|
542
|
+
if (callInfo == null) {
|
|
543
|
+
Log.w(TAG, "Cannot set hold state for call $callId - not found")
|
|
544
|
+
return
|
|
351
545
|
}
|
|
352
546
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
put("callType", callInfo.callType)
|
|
358
|
-
put("displayName", callInfo.displayName)
|
|
359
|
-
callInfo.pictureUrl?.let { put("pictureUrl", it) }
|
|
360
|
-
})
|
|
547
|
+
if (onHold && callInfo.state == CallState.ACTIVE) {
|
|
548
|
+
holdCallInternal(callId, heldBySystem = false)
|
|
549
|
+
} else if (!onHold && callInfo.state == CallState.HELD) {
|
|
550
|
+
unholdCallInternal(callId, resumedBySystem = false)
|
|
361
551
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private fun holdCallInternal(callId: String, heldBySystem: Boolean) {
|
|
555
|
+
Log.d(TAG, "holdCallInternal: $callId, heldBySystem: $heldBySystem")
|
|
556
|
+
val callInfo = activeCalls[callId]
|
|
557
|
+
if (callInfo?.state != CallState.ACTIVE) {
|
|
558
|
+
Log.w(TAG, "Cannot hold call $callId - not in active state")
|
|
559
|
+
return
|
|
369
560
|
}
|
|
370
561
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
562
|
+
activeCalls[callId] = callInfo.copy(
|
|
563
|
+
state = CallState.HELD,
|
|
564
|
+
wasHeldBySystem = heldBySystem
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
telecomConnections[callId]?.setOnHold()
|
|
568
|
+
updateForegroundNotification()
|
|
569
|
+
emitEvent(CallEventType.CALL_HELD, JSONObject().put("callId", callId))
|
|
570
|
+
updateLockScreenBypass()
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
fun unholdCall(callId: String) {
|
|
574
|
+
unholdCallInternal(callId, resumedBySystem = false)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private fun unholdCallInternal(callId: String, resumedBySystem: Boolean) {
|
|
578
|
+
Log.d(TAG, "unholdCallInternal: $callId, resumedBySystem: $resumedBySystem")
|
|
579
|
+
val callInfo = activeCalls[callId]
|
|
580
|
+
if (callInfo?.state != CallState.HELD) {
|
|
581
|
+
Log.w(TAG, "Cannot unhold call $callId - not in held state")
|
|
582
|
+
return
|
|
382
583
|
}
|
|
383
584
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
585
|
+
activeCalls[callId] = callInfo.copy(
|
|
586
|
+
state = CallState.ACTIVE,
|
|
587
|
+
wasHeldBySystem = false
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
telecomConnections[callId]?.setActive()
|
|
591
|
+
updateForegroundNotification()
|
|
592
|
+
emitEvent(CallEventType.CALL_UNHELD, JSONObject().put("callId", callId))
|
|
593
|
+
updateLockScreenBypass()
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
fun muteCall(callId: String) {
|
|
597
|
+
setMutedInternal(callId, true)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
fun unmuteCall(callId: String) {
|
|
601
|
+
setMutedInternal(callId, false)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
fun setMuted(callId: String, muted: Boolean) {
|
|
605
|
+
setMutedInternal(callId, muted)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private fun setMutedInternal(callId: String, muted: Boolean) {
|
|
609
|
+
val callInfo = activeCalls[callId]
|
|
610
|
+
if (callInfo == null) {
|
|
611
|
+
Log.w(TAG, "Cannot set mute state for call $callId - not found")
|
|
612
|
+
return
|
|
395
613
|
}
|
|
396
614
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
615
|
+
val context = requireContext()
|
|
616
|
+
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
400
617
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}
|
|
618
|
+
val wasMuted = audioManager?.isMicrophoneMute ?: false
|
|
619
|
+
audioManager?.isMicrophoneMute = muted
|
|
404
620
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
return
|
|
410
|
-
}
|
|
411
|
-
val am = audioManager ?: return
|
|
412
|
-
am.isMicrophoneMute = muted
|
|
413
|
-
val eventType = if (muted) CallEventType.CALL_MUTED else CallEventType.CALL_UNMUTED
|
|
414
|
-
emitEvent(eventType, JSONObject().put("callId", callId))
|
|
621
|
+
if (wasMuted != muted) {
|
|
622
|
+
val eventType = if (muted) CallEventType.CALL_MUTED else CallEventType.CALL_UNMUTED
|
|
623
|
+
emitEvent(eventType, JSONObject().put("callId", callId))
|
|
624
|
+
Log.d(TAG, "Call $callId mute state changed to: $muted")
|
|
415
625
|
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
fun endCall(callId: String) {
|
|
629
|
+
Log.d(TAG, "endCall: $callId")
|
|
630
|
+
endCallInternal(callId)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
fun endAllCalls() {
|
|
634
|
+
Log.d(TAG, "endAllCalls: Ending all active calls")
|
|
635
|
+
if (activeCalls.isEmpty()) return
|
|
416
636
|
|
|
417
|
-
|
|
418
|
-
|
|
637
|
+
activeCalls.keys.toList().forEach { callId ->
|
|
638
|
+
endCallInternal(callId)
|
|
419
639
|
}
|
|
420
640
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
641
|
+
activeCalls.clear()
|
|
642
|
+
telecomConnections.clear()
|
|
643
|
+
callMetadata.clear()
|
|
644
|
+
incomingCallIds.clear() // NEW: Clear duplicate tracking
|
|
645
|
+
currentCallId = null
|
|
424
646
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
647
|
+
cleanup()
|
|
648
|
+
updateLockScreenBypass()
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
private fun endCallInternal(callId: String) {
|
|
652
|
+
Log.d(TAG, "endCallInternal: $callId")
|
|
653
|
+
|
|
654
|
+
val callInfo = activeCalls[callId] ?: run {
|
|
655
|
+
Log.w(TAG, "Call $callId not found in active calls")
|
|
656
|
+
return
|
|
429
657
|
}
|
|
430
658
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
callMetadata.remove(callId)
|
|
659
|
+
val metadata = callMetadata.remove(callId)
|
|
660
|
+
activeCalls.remove(callId)
|
|
661
|
+
incomingCallIds.remove(callId) // NEW: Clean up duplicate tracking
|
|
435
662
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
663
|
+
stopRingback()
|
|
664
|
+
stopRingtone()
|
|
665
|
+
cancelIncomingCallUI()
|
|
439
666
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
667
|
+
if (currentCallId == callId) {
|
|
668
|
+
currentCallId =
|
|
669
|
+
activeCalls.filter { it.value.state != CallState.ENDED }.keys.firstOrNull()
|
|
670
|
+
}
|
|
444
671
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
672
|
+
val context = requireContext()
|
|
673
|
+
val closeActivityIntent = Intent("com.qusaieilouti99.callmanager.CLOSE_CALL_ACTIVITY")
|
|
674
|
+
.setPackage(context.packageName)
|
|
675
|
+
.putExtra("callId", callId)
|
|
450
676
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
677
|
+
try {
|
|
678
|
+
context.sendBroadcast(closeActivityIntent)
|
|
679
|
+
Log.d(TAG, "Sent close broadcast for CallActivity: $callId")
|
|
680
|
+
} catch (e: Exception) {
|
|
681
|
+
Log.w(TAG, "Failed to send close broadcast: ${e.message}")
|
|
454
682
|
}
|
|
455
683
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
it.value.state == CallState.HELD
|
|
684
|
+
telecomConnections[callId]?.let { connection ->
|
|
685
|
+
connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
|
686
|
+
connection.destroy()
|
|
687
|
+
removeTelecomConnection(callId)
|
|
461
688
|
}
|
|
462
689
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
updateAndApplyAudioRoute()
|
|
690
|
+
if (activeCalls.isEmpty()) {
|
|
691
|
+
cleanup()
|
|
692
|
+
} else {
|
|
693
|
+
updateForegroundNotification()
|
|
469
694
|
}
|
|
470
695
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
696
|
+
updateLockScreenBypass()
|
|
697
|
+
|
|
698
|
+
for (listener in callEndListeners) {
|
|
699
|
+
mainHandler.post {
|
|
700
|
+
try {
|
|
701
|
+
listener.onCallEnded(callId)
|
|
702
|
+
} catch (_: Throwable) {
|
|
703
|
+
// swallow
|
|
478
704
|
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
479
707
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
708
|
+
emitEvent(CallEventType.CALL_ENDED, JSONObject().apply {
|
|
709
|
+
put("callId", callId)
|
|
710
|
+
metadata?.let {
|
|
711
|
+
try { put("metadata", JSONObject(it)) }
|
|
712
|
+
catch (e: Exception) { put("metadata", it) }
|
|
713
|
+
}
|
|
714
|
+
})
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
fun getAudioDevices(): AudioRoutesInfo {
|
|
718
|
+
val context = requireContext()
|
|
719
|
+
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
|
720
|
+
?: return AudioRoutesInfo(emptyArray(), "Unknown")
|
|
721
|
+
|
|
722
|
+
val devices = mutableSetOf<String>()
|
|
723
|
+
devices.add("Speaker")
|
|
724
|
+
devices.add("Earpiece")
|
|
725
|
+
|
|
726
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
727
|
+
val infos = audioManager?.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
|
728
|
+
infos?.forEach { d ->
|
|
729
|
+
when (d.type) {
|
|
730
|
+
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
|
|
731
|
+
AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> devices.add("Bluetooth")
|
|
732
|
+
AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
|
|
733
|
+
AudioDeviceInfo.TYPE_WIRED_HEADSET -> devices.add("Headset")
|
|
484
734
|
}
|
|
735
|
+
}
|
|
736
|
+
} else {
|
|
737
|
+
@Suppress("DEPRECATION")
|
|
738
|
+
if (audioManager?.isBluetoothA2dpOn == true || audioManager?.isBluetoothScoOn == true)
|
|
739
|
+
devices.add("Bluetooth")
|
|
740
|
+
@Suppress("DEPRECATION")
|
|
741
|
+
if (audioManager?.isWiredHeadsetOn == true) devices.add("Headset")
|
|
742
|
+
}
|
|
485
743
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
val availableDevices = getAvailableAudioDevices()
|
|
489
|
-
|
|
490
|
-
val targetRoute = if (userSelectedAudioRoute != null && availableDevices.contains(userSelectedAudioRoute)) {
|
|
491
|
-
userSelectedAudioRoute!!
|
|
492
|
-
} else {
|
|
493
|
-
when {
|
|
494
|
-
availableDevices.contains("Bluetooth") -> "Bluetooth"
|
|
495
|
-
availableDevices.contains("Headset") -> "Headset"
|
|
496
|
-
call.callType == "Video" -> "Speaker"
|
|
497
|
-
else -> "Earpiece"
|
|
498
|
-
}
|
|
499
|
-
}
|
|
744
|
+
val current = getCurrentAudioRoute()
|
|
745
|
+
Log.d(TAG, "Available audio devices: ${devices.toList()}, current: $current")
|
|
500
746
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
"Bluetooth" -> {
|
|
507
|
-
am.isSpeakerphoneOn = false
|
|
508
|
-
am.startBluetoothSco()
|
|
509
|
-
am.isBluetoothScoOn = true
|
|
510
|
-
}
|
|
511
|
-
"Headset" -> {
|
|
512
|
-
am.isSpeakerphoneOn = false
|
|
513
|
-
am.stopBluetoothSco()
|
|
514
|
-
am.isBluetoothScoOn = false
|
|
515
|
-
}
|
|
516
|
-
"Speaker" -> {
|
|
517
|
-
am.isSpeakerphoneOn = true
|
|
518
|
-
am.stopBluetoothSco()
|
|
519
|
-
am.isBluetoothScoOn = false
|
|
520
|
-
}
|
|
521
|
-
"Earpiece" -> {
|
|
522
|
-
am.isSpeakerphoneOn = false
|
|
523
|
-
am.stopBluetoothSco()
|
|
524
|
-
am.isBluetoothScoOn = false
|
|
525
|
-
}
|
|
526
|
-
}
|
|
747
|
+
// Convert strings to StringHolder objects
|
|
748
|
+
val deviceHolders = devices.map { StringHolder(it) }.toTypedArray()
|
|
749
|
+
lastAudioRoutesInfo = AudioRoutesInfo(deviceHolders, current)
|
|
750
|
+
return AudioRoutesInfo(deviceHolders, current)
|
|
751
|
+
}
|
|
527
752
|
|
|
528
|
-
|
|
529
|
-
|
|
753
|
+
fun setAudioRoute(route: String) {
|
|
754
|
+
Log.d(TAG, "setAudioRoute called: $route")
|
|
755
|
+
|
|
756
|
+
val ctx = requireContext()
|
|
757
|
+
if (audioManager == null) {
|
|
758
|
+
audioManager = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
530
759
|
}
|
|
760
|
+
val am = audioManager!!
|
|
531
761
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
devices.add("Earpiece")
|
|
536
|
-
devices.add("Speaker")
|
|
762
|
+
if (am.mode != AudioManager.MODE_IN_COMMUNICATION) {
|
|
763
|
+
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
764
|
+
}
|
|
537
765
|
|
|
766
|
+
when (route) {
|
|
767
|
+
"Speaker" -> {
|
|
768
|
+
am.isSpeakerphoneOn = true
|
|
769
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
|
|
770
|
+
am.stopBluetoothSco()
|
|
771
|
+
am.isBluetoothScoOn = false
|
|
772
|
+
}
|
|
773
|
+
Log.d(TAG, "Audio routed to SPEAKER")
|
|
774
|
+
}
|
|
775
|
+
"Earpiece" -> {
|
|
776
|
+
am.isSpeakerphoneOn = false
|
|
777
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
|
|
778
|
+
am.stopBluetoothSco()
|
|
779
|
+
am.isBluetoothScoOn = false
|
|
780
|
+
}
|
|
781
|
+
Log.d(TAG, "Audio routed to EARPIECE")
|
|
782
|
+
}
|
|
783
|
+
"Bluetooth" -> {
|
|
784
|
+
am.isSpeakerphoneOn = false
|
|
538
785
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
AudioDeviceInfo.TYPE_BLUETOOTH_SCO, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> devices.add("Bluetooth")
|
|
543
|
-
AudioDeviceInfo.TYPE_WIRED_HEADSET, AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> devices.add("Headset")
|
|
544
|
-
}
|
|
545
|
-
}
|
|
786
|
+
am.startBluetoothSco()
|
|
787
|
+
am.isBluetoothScoOn = true
|
|
788
|
+
Log.d(TAG, "Audio routed to BLUETOOTH")
|
|
546
789
|
} else {
|
|
547
|
-
|
|
548
|
-
if (am.isBluetoothScoAvailableOffCall) devices.add("Bluetooth")
|
|
549
|
-
@Suppress("DEPRECATION")
|
|
550
|
-
if (am.isWiredHeadsetOn) devices.add("Headset")
|
|
790
|
+
Log.w(TAG, "Bluetooth SCO not supported on this OS version")
|
|
551
791
|
}
|
|
552
|
-
|
|
792
|
+
}
|
|
793
|
+
"Headset" -> {
|
|
794
|
+
am.isSpeakerphoneOn = false
|
|
795
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
|
|
796
|
+
am.stopBluetoothSco()
|
|
797
|
+
am.isBluetoothScoOn = false
|
|
798
|
+
}
|
|
799
|
+
Log.d(TAG, "Audio routed to HEADSET")
|
|
800
|
+
}
|
|
801
|
+
else -> {
|
|
802
|
+
Log.w(TAG, "Unknown audio route: $route")
|
|
803
|
+
return
|
|
804
|
+
}
|
|
553
805
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
806
|
+
emitAudioRouteChanged()
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
private fun getCurrentAudioRoute(): String {
|
|
810
|
+
return when {
|
|
811
|
+
audioManager?.isBluetoothScoOn == true -> "Bluetooth"
|
|
812
|
+
audioManager?.isSpeakerphoneOn == true -> "Speaker"
|
|
813
|
+
audioManager?.isWiredHeadsetOn == true -> "Headset"
|
|
814
|
+
else -> "Earpiece"
|
|
559
815
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// NEW: Improved initial audio route setting
|
|
819
|
+
private fun setInitialAudioRoute(callType: String, isCallStart: Boolean = false) {
|
|
820
|
+
val avail = getAudioDevices()
|
|
821
|
+
// Extract string values for comparison
|
|
822
|
+
val deviceStrings = avail.devices.map { it.value }
|
|
823
|
+
|
|
824
|
+
val defaultRoute = when {
|
|
825
|
+
deviceStrings.contains("Bluetooth") -> "Bluetooth"
|
|
826
|
+
deviceStrings.contains("Headset") -> "Headset"
|
|
827
|
+
callType == "Video" -> "Speaker"
|
|
828
|
+
else -> "Earpiece"
|
|
569
829
|
}
|
|
570
830
|
|
|
571
|
-
|
|
572
|
-
val info = getAudioDevices()
|
|
573
|
-
val payload = JSONObject().apply {
|
|
574
|
-
put("devices", JSONArray(info.devices.map { it.value }))
|
|
575
|
-
put("currentRoute", info.currentRoute)
|
|
576
|
-
}
|
|
577
|
-
emitEvent(CallEventType.AUDIO_ROUTE_CHANGED, payload)
|
|
578
|
-
Log.d(TAG, "Emitted AUDIO_ROUTE_CHANGED: Current='${info.currentRoute}', Available=${info.devices.map { it.value }}")
|
|
579
|
-
}
|
|
831
|
+
Log.d(TAG, "Setting initial audio route: $defaultRoute for call type: $callType (isCallStart: $isCallStart)")
|
|
580
832
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
else -> ""
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
if (removedDeviceTypes.contains(userSelectedAudioRoute)) {
|
|
600
|
-
userSelectedAudioRoute = null
|
|
601
|
-
}
|
|
833
|
+
// For call start, ensure audio mode is properly set first
|
|
834
|
+
if (isCallStart) {
|
|
835
|
+
setAudioMode()
|
|
836
|
+
|
|
837
|
+
// Additional delay for audio system to be ready
|
|
838
|
+
if (callType == "Video") {
|
|
839
|
+
mainHandler.postDelayed({
|
|
840
|
+
setAudioRoute(defaultRoute)
|
|
841
|
+
|
|
842
|
+
// Force speaker for video calls with additional verification
|
|
843
|
+
mainHandler.postDelayed({
|
|
844
|
+
val currentRoute = getCurrentAudioRoute()
|
|
845
|
+
if (currentRoute != "Speaker" && !deviceStrings.contains("Bluetooth") && !deviceStrings.contains("Headset")) {
|
|
846
|
+
Log.d(TAG, "Forcing speaker for video call - current route was: $currentRoute")
|
|
847
|
+
setAudioRoute("Speaker")
|
|
602
848
|
}
|
|
603
|
-
|
|
604
|
-
}
|
|
849
|
+
}, 300L)
|
|
850
|
+
}, 200L)
|
|
851
|
+
} else {
|
|
852
|
+
setAudioRoute(defaultRoute)
|
|
853
|
+
}
|
|
854
|
+
} else {
|
|
855
|
+
setAudioRoute(defaultRoute)
|
|
605
856
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private fun setAudioMode() {
|
|
860
|
+
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
861
|
+
Log.d(TAG, "Audio mode set to MODE_IN_COMMUNICATION")
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
private fun resetAudioMode() {
|
|
865
|
+
if (activeCalls.isEmpty()) {
|
|
866
|
+
audioManager?.mode = AudioManager.MODE_NORMAL
|
|
867
|
+
audioManager?.stopBluetoothSco()
|
|
868
|
+
audioManager?.isBluetoothScoOn = false
|
|
869
|
+
audioManager?.isSpeakerphoneOn = false
|
|
870
|
+
Log.d(TAG, "Audio mode reset to MODE_NORMAL")
|
|
609
871
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
audioManager?.isSpeakerphoneOn = false
|
|
620
|
-
audioManager?.stopBluetoothSco()
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
private fun emitAudioRouteChanged() {
|
|
875
|
+
val info = getAudioDevices()
|
|
876
|
+
// Extract string values from StringHolder objects
|
|
877
|
+
val deviceStrings = info.devices.map { it.value }
|
|
878
|
+
val payload = JSONObject().apply {
|
|
879
|
+
put("devices", JSONArray(deviceStrings))
|
|
880
|
+
put("currentRoute", info.currentRoute)
|
|
621
881
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
).apply {
|
|
631
|
-
description = "Notifications for incoming calls"
|
|
632
|
-
setBypassDnd(true)
|
|
633
|
-
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
634
|
-
setSound(null, null)
|
|
635
|
-
}
|
|
636
|
-
val manager = context.getSystemService(NotificationManager::class.java)
|
|
637
|
-
manager.createNotificationChannel(channel)
|
|
638
|
-
}
|
|
882
|
+
emitEvent(CallEventType.AUDIO_ROUTE_CHANGED, payload)
|
|
883
|
+
Log.d(TAG, "Audio route changed: ${info.currentRoute}, available: $deviceStrings")
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
private val audioDeviceCallback = object : AudioDeviceCallback() {
|
|
887
|
+
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
|
|
888
|
+
Log.d(TAG, "Audio devices added")
|
|
889
|
+
emitAudioDevicesChanged()
|
|
639
890
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
Log.d(TAG, "Showing incoming call UI for $callId")
|
|
644
|
-
createNotificationChannel()
|
|
645
|
-
|
|
646
|
-
showCallActivityOverlay(context, callId, callerName, callType, callerPicUrl)
|
|
647
|
-
showStandardNotification(context, callId, callerName, callType, callerPicUrl)
|
|
648
|
-
|
|
649
|
-
playRingtone()
|
|
891
|
+
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
|
|
892
|
+
Log.d(TAG, "Audio devices removed")
|
|
893
|
+
emitAudioDevicesChanged()
|
|
650
894
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
try {
|
|
661
|
-
context.startActivity(overlayIntent)
|
|
662
|
-
} catch (e: Exception) {
|
|
663
|
-
Log.e(TAG, "Failed to launch CallActivity overlay: ${e.message}")
|
|
664
|
-
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
private fun emitAudioDevicesChanged() {
|
|
898
|
+
val info = getAudioDevices()
|
|
899
|
+
// Extract string values from StringHolder objects
|
|
900
|
+
val deviceStrings = info.devices.map { it.value }
|
|
901
|
+
val payload = JSONObject().apply {
|
|
902
|
+
put("devices", JSONArray(deviceStrings))
|
|
903
|
+
put("currentRoute", info.currentRoute)
|
|
665
904
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
905
|
+
emitEvent(CallEventType.AUDIO_DEVICES_CHANGED, payload)
|
|
906
|
+
Log.d(TAG, "Audio devices changed: available: $deviceStrings")
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
fun registerAudioDeviceCallback() {
|
|
910
|
+
val context = requireContext()
|
|
911
|
+
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
912
|
+
audioManager?.registerAudioDeviceCallback(audioDeviceCallback, null)
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
fun unregisterAudioDeviceCallback() {
|
|
916
|
+
val context = requireContext()
|
|
917
|
+
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
918
|
+
audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
fun keepScreenAwake(keepAwake: Boolean) {
|
|
922
|
+
val context = requireContext()
|
|
923
|
+
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
|
924
|
+
if (keepAwake) {
|
|
925
|
+
if (wakeLock == null || wakeLock!!.isHeld.not()) {
|
|
926
|
+
wakeLock = powerManager.newWakeLock(
|
|
927
|
+
PowerManager.SCREEN_DIM_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
|
928
|
+
"CallEngine:WakeLock"
|
|
674
929
|
)
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
.setOngoing(true)
|
|
684
|
-
|
|
685
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && supportsCallStyleNotifications()) {
|
|
686
|
-
val person = android.app.Person.Builder().setName(callerName).build()
|
|
687
|
-
val answerIntent = Intent(context, CallNotificationActionReceiver::class.java).apply { action = "com.qusaieilouti99.callmanager.ANSWER_CALL"; putExtra("callId", callId) }
|
|
688
|
-
val declineIntent = Intent(context, CallNotificationActionReceiver::class.java).apply { action = "com.qusaieilouti99.callmanager.DECLINE_CALL"; putExtra("callId", callId) }
|
|
689
|
-
val answerPI = PendingIntent.getBroadcast(context, callId.hashCode() + 1, answerIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
690
|
-
val declinePI = PendingIntent.getBroadcast(context, callId.hashCode() + 2, declineIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
691
|
-
builder.setStyle(Notification.CallStyle.forIncomingCall(person, declinePI, answerPI))
|
|
930
|
+
wakeLock?.acquire(10 * 60 * 1000L)
|
|
931
|
+
Log.d(TAG, "Acquired SCREEN_DIM_WAKE_LOCK")
|
|
932
|
+
}
|
|
933
|
+
} else {
|
|
934
|
+
wakeLock?.let {
|
|
935
|
+
if (it.isHeld) {
|
|
936
|
+
it.release()
|
|
937
|
+
Log.d(TAG, "Released SCREEN_DIM_WAKE_LOCK")
|
|
692
938
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
notificationManager.notify(NOTIF_ID, builder.build())
|
|
939
|
+
}
|
|
940
|
+
wakeLock = null
|
|
696
941
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
fun getActiveCalls(): List<CallInfo> = activeCalls.values.toList()
|
|
945
|
+
fun getCurrentCallId(): String? = currentCallId
|
|
946
|
+
fun isCallActive(): Boolean = activeCalls.any {
|
|
947
|
+
it.value.state == CallState.ACTIVE ||
|
|
948
|
+
it.value.state == CallState.INCOMING ||
|
|
949
|
+
it.value.state == CallState.DIALING ||
|
|
950
|
+
it.value.state == CallState.HELD
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
private fun validateOutgoingCallRequest(): Boolean {
|
|
954
|
+
return !activeCalls.any {
|
|
955
|
+
it.value.state == CallState.INCOMING || it.value.state == CallState.ACTIVE
|
|
703
956
|
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
private fun rejectIncomingCallCollision(callId: String, reason: String) {
|
|
960
|
+
callMetadata.remove(callId)
|
|
961
|
+
incomingCallIds.remove(callId) // NEW: Clean up duplicate tracking
|
|
962
|
+
emitEvent(CallEventType.CALL_REJECTED, JSONObject().apply {
|
|
963
|
+
put("callId", callId)
|
|
964
|
+
put("reason", reason)
|
|
965
|
+
})
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
private fun createNotificationChannel() {
|
|
969
|
+
val context = requireContext()
|
|
970
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
971
|
+
val channel = NotificationChannel(
|
|
972
|
+
NOTIF_CHANNEL_ID,
|
|
973
|
+
"Incoming Call Channel",
|
|
974
|
+
NotificationManager.IMPORTANCE_HIGH
|
|
975
|
+
)
|
|
976
|
+
channel.description = "Notifications for incoming calls"
|
|
977
|
+
channel.enableLights(true)
|
|
978
|
+
channel.lightColor = Color.GREEN
|
|
979
|
+
channel.enableVibration(true)
|
|
980
|
+
channel.setBypassDnd(true)
|
|
981
|
+
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
982
|
+
|
|
983
|
+
// NEW: Improved sound handling to prevent double ringing
|
|
984
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
|
985
|
+
channel.setSound(
|
|
986
|
+
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE),
|
|
987
|
+
AudioAttributes.Builder()
|
|
988
|
+
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
|
989
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
990
|
+
.build()
|
|
991
|
+
)
|
|
992
|
+
} else {
|
|
993
|
+
// For API 31+, disable notification sound to prevent conflicts with custom ringtone
|
|
994
|
+
channel.setSound(null, null)
|
|
995
|
+
channel.importance = NotificationManager.IMPORTANCE_HIGH
|
|
996
|
+
}
|
|
997
|
+
val manager = context.getSystemService(NotificationManager::class.java)
|
|
998
|
+
manager.createNotificationChannel(channel)
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
704
1001
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
audioManager?.mode = AudioManager.MODE_RINGTONE
|
|
1002
|
+
private fun showIncomingCallUI(callId: String, callerName: String, callType: String, callerPicUrl: String?) {
|
|
1003
|
+
val context = requireContext()
|
|
1004
|
+
Log.d(TAG, "Showing incoming call UI for $callId")
|
|
709
1005
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
713
|
-
vibrator?.vibrate(VibrationEffect.createWaveform(pattern, 0))
|
|
714
|
-
} else {
|
|
715
|
-
@Suppress("DEPRECATION")
|
|
716
|
-
vibrator?.vibrate(pattern, 0)
|
|
717
|
-
}
|
|
1006
|
+
val useCallStyleNotification = supportsCallStyleNotifications()
|
|
1007
|
+
Log.d(TAG, "Using CallStyle notification: $useCallStyleNotification")
|
|
718
1008
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
Log.e(TAG, "Failed to play ringtone", e)
|
|
726
|
-
}
|
|
1009
|
+
if (isDeviceLocked(context) || !useCallStyleNotification) {
|
|
1010
|
+
Log.d(TAG, "Device is locked or CallStyle not supported - using overlay/fallback approach")
|
|
1011
|
+
showCallActivityOverlay(context, callId, callerName, callType, callerPicUrl)
|
|
1012
|
+
} else {
|
|
1013
|
+
Log.d(TAG, "Device is unlocked and supports CallStyle - using enhanced notification")
|
|
1014
|
+
showStandardNotification(context, callId, callerName, callType, callerPicUrl)
|
|
727
1015
|
}
|
|
728
1016
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1017
|
+
// NEW: Improved ringtone handling to prevent double ringing
|
|
1018
|
+
playRingtone()
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
private fun isDeviceLocked(context: Context): Boolean {
|
|
1022
|
+
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
|
1023
|
+
return keyguardManager.isKeyguardLocked
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
private fun showCallActivityOverlay(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
|
|
1027
|
+
val overlayIntent = Intent(context, CallActivity::class.java).apply {
|
|
1028
|
+
addFlags(
|
|
1029
|
+
Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
1030
|
+
Intent.FLAG_ACTIVITY_CLEAR_TASK or
|
|
1031
|
+
Intent.FLAG_ACTIVITY_NO_ANIMATION or
|
|
1032
|
+
Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
|
1033
|
+
)
|
|
1034
|
+
putExtra("callId", callId)
|
|
1035
|
+
putExtra("callerName", callerName)
|
|
1036
|
+
putExtra("callType", callType)
|
|
1037
|
+
callerPicUrl?.let { putExtra("callerAvatar", it) }
|
|
1038
|
+
putExtra("LOCK_SCREEN_MODE", true)
|
|
735
1039
|
}
|
|
736
1040
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1041
|
+
try {
|
|
1042
|
+
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
|
1043
|
+
val wakeLock = powerManager.newWakeLock(
|
|
1044
|
+
PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
|
1045
|
+
"CallEngine:LockScreenWake"
|
|
1046
|
+
)
|
|
1047
|
+
wakeLock.acquire(5000)
|
|
1048
|
+
context.startActivity(overlayIntent)
|
|
1049
|
+
Log.d(TAG, "Successfully launched CallActivity overlay")
|
|
1050
|
+
} catch (e: Exception) {
|
|
1051
|
+
Log.e(TAG, "Overlay failed, falling back to standard notification: ${e.message}")
|
|
1052
|
+
showStandardNotification(context, callId, callerName, callType, callerPicUrl)
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
private fun showStandardNotification(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
|
|
1057
|
+
createNotificationChannel()
|
|
1058
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
1059
|
+
|
|
1060
|
+
val fullScreenIntent = Intent(context, CallActivity::class.java).apply {
|
|
1061
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
1062
|
+
putExtra("callId", callId)
|
|
1063
|
+
putExtra("callerName", callerName)
|
|
1064
|
+
putExtra("callType", callType)
|
|
1065
|
+
callerPicUrl?.let { putExtra("callerAvatar", it) }
|
|
1066
|
+
}
|
|
740
1067
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1068
|
+
val fullScreenPendingIntent = PendingIntent.getActivity(
|
|
1069
|
+
context, callId.hashCode(), fullScreenIntent,
|
|
1070
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
val answerIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
|
|
1074
|
+
action = "com.qusaieilouti99.callmanager.ANSWER_CALL"
|
|
1075
|
+
putExtra("callId", callId)
|
|
1076
|
+
}
|
|
1077
|
+
val answerPendingIntent = PendingIntent.getBroadcast(
|
|
1078
|
+
context, 0, answerIntent,
|
|
1079
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
val declineIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
|
|
1083
|
+
action = "com.qusaieilouti99.callmanager.DECLINE_CALL"
|
|
1084
|
+
putExtra("callId", callId)
|
|
1085
|
+
}
|
|
1086
|
+
val declinePendingIntent = PendingIntent.getBroadcast(
|
|
1087
|
+
context, 1, declineIntent,
|
|
1088
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
val notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && supportsCallStyleNotifications()) {
|
|
1092
|
+
val person = android.app.Person.Builder()
|
|
1093
|
+
.setName(callerName)
|
|
1094
|
+
.setImportant(true)
|
|
1095
|
+
.build()
|
|
1096
|
+
Notification.Builder(context, NOTIF_CHANNEL_ID)
|
|
1097
|
+
.setSmallIcon(android.R.drawable.sym_call_incoming)
|
|
1098
|
+
.setStyle(
|
|
1099
|
+
Notification.CallStyle.forIncomingCall(
|
|
1100
|
+
person,
|
|
1101
|
+
declinePendingIntent,
|
|
1102
|
+
answerPendingIntent
|
|
1103
|
+
)
|
|
1104
|
+
)
|
|
1105
|
+
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
1106
|
+
.setOngoing(true)
|
|
1107
|
+
.setAutoCancel(false)
|
|
1108
|
+
.setCategory(Notification.CATEGORY_CALL)
|
|
1109
|
+
.setPriority(Notification.PRIORITY_MAX)
|
|
1110
|
+
.setVisibility(Notification.VISIBILITY_PUBLIC)
|
|
1111
|
+
.build()
|
|
1112
|
+
} else {
|
|
1113
|
+
Notification.Builder(context, NOTIF_CHANNEL_ID)
|
|
1114
|
+
.setSmallIcon(android.R.drawable.sym_call_incoming)
|
|
1115
|
+
.setContentTitle("Incoming Call")
|
|
1116
|
+
.setContentText(callerName)
|
|
1117
|
+
.setPriority(Notification.PRIORITY_MAX)
|
|
1118
|
+
.setCategory(Notification.CATEGORY_CALL)
|
|
1119
|
+
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
1120
|
+
.addAction(android.R.drawable.sym_action_call, "Answer", answerPendingIntent)
|
|
1121
|
+
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Decline", declinePendingIntent)
|
|
1122
|
+
.setOngoing(true)
|
|
1123
|
+
.setAutoCancel(false)
|
|
1124
|
+
.setVisibility(Notification.VISIBILITY_PUBLIC)
|
|
1125
|
+
.build()
|
|
752
1126
|
}
|
|
753
1127
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1128
|
+
notificationManager.notify(NOTIF_ID, notification)
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
fun cancelIncomingCallUI() {
|
|
1132
|
+
val context = requireContext()
|
|
1133
|
+
val notificationManager =
|
|
1134
|
+
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
1135
|
+
notificationManager.cancel(NOTIF_ID)
|
|
1136
|
+
stopRingtone()
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
private fun startForegroundService() {
|
|
1140
|
+
val context = requireContext()
|
|
1141
|
+
val currentCall = activeCalls.values.find {
|
|
1142
|
+
it.state == CallState.ACTIVE ||
|
|
1143
|
+
it.state == CallState.INCOMING ||
|
|
1144
|
+
it.state == CallState.DIALING ||
|
|
1145
|
+
it.state == CallState.HELD
|
|
763
1146
|
}
|
|
764
1147
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
PowerManager.SCREEN_DIM_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
|
772
|
-
"CallEngine:WakeLock"
|
|
773
|
-
)
|
|
774
|
-
wakeLock?.acquire(10 * 60 * 1000L)
|
|
775
|
-
Log.d(TAG, "Acquired SCREEN_DIM_WAKE_LOCK")
|
|
776
|
-
}
|
|
777
|
-
} else {
|
|
778
|
-
wakeLock?.let {
|
|
779
|
-
if (it.isHeld) {
|
|
780
|
-
it.release()
|
|
781
|
-
Log.d(TAG, "Released SCREEN_DIM_WAKE_LOCK")
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
wakeLock = null
|
|
785
|
-
}
|
|
1148
|
+
val intent = Intent(context, CallForegroundService::class.java)
|
|
1149
|
+
currentCall?.let {
|
|
1150
|
+
intent.putExtra("callId", it.callId)
|
|
1151
|
+
intent.putExtra("callType", it.callType)
|
|
1152
|
+
intent.putExtra("displayName", it.displayName)
|
|
1153
|
+
intent.putExtra("state", it.state.name)
|
|
786
1154
|
}
|
|
787
1155
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1156
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1157
|
+
context.startForegroundService(intent)
|
|
1158
|
+
} else {
|
|
1159
|
+
context.startService(intent)
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
private fun stopForegroundService() {
|
|
1164
|
+
val context = requireContext()
|
|
1165
|
+
val intent = Intent(context, CallForegroundService::class.java)
|
|
1166
|
+
context.stopService(intent)
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
private fun updateForegroundNotification() {
|
|
1170
|
+
startForegroundService()
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
private fun isMainActivityInForeground(): Boolean {
|
|
1174
|
+
val context = requireContext()
|
|
1175
|
+
val activityManager =
|
|
1176
|
+
context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
|
1177
|
+
|
|
1178
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
1179
|
+
try {
|
|
1180
|
+
val tasks = activityManager.appTasks
|
|
1181
|
+
if (tasks.isNotEmpty()) {
|
|
1182
|
+
val taskInfo = tasks[0].taskInfo
|
|
1183
|
+
return taskInfo.topActivity?.className?.contains("MainActivity") == true
|
|
804
1184
|
}
|
|
1185
|
+
} catch (e: Exception) {
|
|
1186
|
+
Log.w(TAG, "Failed to get app tasks: ${e.message}")
|
|
1187
|
+
}
|
|
1188
|
+
} else {
|
|
1189
|
+
try {
|
|
1190
|
+
@Suppress("DEPRECATION")
|
|
1191
|
+
val tasks = activityManager.getRunningTasks(1)
|
|
1192
|
+
if (tasks.isNotEmpty()) {
|
|
1193
|
+
val runningTaskInfo = tasks[0]
|
|
1194
|
+
return runningTaskInfo.topActivity?.className?.contains("MainActivity") == true
|
|
1195
|
+
}
|
|
1196
|
+
} catch (e: Exception) {
|
|
1197
|
+
Log.w(TAG, "Failed to get running tasks: ${e.message}")
|
|
1198
|
+
}
|
|
805
1199
|
}
|
|
1200
|
+
return false
|
|
1201
|
+
}
|
|
806
1202
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
if (telecomManager.getPhoneAccount(phoneAccountHandle) == null) {
|
|
814
|
-
val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "PingMe Call")
|
|
815
|
-
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
|
|
816
|
-
.build()
|
|
1203
|
+
private fun bringAppToForeground() {
|
|
1204
|
+
if (isMainActivityInForeground()) {
|
|
1205
|
+
Log.d(TAG, "MainActivity is already in foreground, skipping")
|
|
1206
|
+
return
|
|
1207
|
+
}
|
|
817
1208
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1209
|
+
Log.d(TAG, "Bringing app to foreground")
|
|
1210
|
+
val context = requireContext()
|
|
1211
|
+
val packageName = context.packageName
|
|
1212
|
+
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
|
|
1213
|
+
launchIntent?.addFlags(
|
|
1214
|
+
Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
1215
|
+
Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
|
1216
|
+
Intent.FLAG_ACTIVITY_SINGLE_TOP
|
|
1217
|
+
)
|
|
1218
|
+
|
|
1219
|
+
if (isCallActive()) {
|
|
1220
|
+
launchIntent?.putExtra("BYPASS_LOCK_SCREEN", true)
|
|
825
1221
|
}
|
|
826
1222
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1223
|
+
try {
|
|
1224
|
+
context.startActivity(launchIntent)
|
|
1225
|
+
Handler(Looper.getMainLooper()).postDelayed({
|
|
1226
|
+
updateLockScreenBypass()
|
|
1227
|
+
}, 100)
|
|
1228
|
+
} catch (e: Exception) {
|
|
1229
|
+
Log.e(TAG, "Failed to bring app to foreground: ${e.message}")
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
private fun registerPhoneAccount() {
|
|
1234
|
+
val context = requireContext()
|
|
1235
|
+
val telecomManager =
|
|
1236
|
+
context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
1237
|
+
val phoneAccountHandle = getPhoneAccountHandle()
|
|
1238
|
+
|
|
1239
|
+
if (telecomManager.getPhoneAccount(phoneAccountHandle) == null) {
|
|
1240
|
+
val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "PingMe Call")
|
|
1241
|
+
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
|
|
1242
|
+
.build()
|
|
1243
|
+
|
|
1244
|
+
try {
|
|
1245
|
+
telecomManager.registerPhoneAccount(phoneAccount)
|
|
1246
|
+
Log.d(TAG, "PhoneAccount registered successfully")
|
|
1247
|
+
} catch (e: Exception) {
|
|
1248
|
+
Log.e(TAG, "Failed to register PhoneAccount: ${e.message}", e)
|
|
1249
|
+
}
|
|
833
1250
|
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
private fun getPhoneAccountHandle(): PhoneAccountHandle {
|
|
1254
|
+
val context = requireContext()
|
|
1255
|
+
return PhoneAccountHandle(
|
|
1256
|
+
ComponentName(context, MyConnectionService::class.java),
|
|
1257
|
+
PHONE_ACCOUNT_ID
|
|
1258
|
+
)
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// NEW: Improved ringtone handling to prevent double ringing
|
|
1262
|
+
private fun playRingtone() {
|
|
1263
|
+
synchronized(ringtoneStateLock) {
|
|
1264
|
+
if (isCustomRingtoneActive) {
|
|
1265
|
+
Log.d(TAG, "Custom ringtone already active, skipping")
|
|
1266
|
+
return
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
val context = requireContext()
|
|
1270
|
+
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
1271
|
+
|
|
1272
|
+
// Only set ringtone mode if not already in communication mode
|
|
1273
|
+
val currentMode = audioManager?.mode ?: AudioManager.MODE_NORMAL
|
|
1274
|
+
if (currentMode != AudioManager.MODE_IN_COMMUNICATION) {
|
|
1275
|
+
audioManager?.mode = AudioManager.MODE_RINGTONE
|
|
1276
|
+
}
|
|
834
1277
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
val
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1278
|
+
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
|
1279
|
+
vibrator?.let { v ->
|
|
1280
|
+
val pattern = longArrayOf(0L, 500L, 500L)
|
|
1281
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1282
|
+
v.vibrate(VibrationEffect.createWaveform(pattern, 0))
|
|
1283
|
+
} else {
|
|
1284
|
+
@Suppress("DEPRECATION")
|
|
1285
|
+
v.vibrate(pattern, 0)
|
|
842
1286
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
try {
|
|
1290
|
+
// Check if system ringtone is already playing for this call (API 31+)
|
|
1291
|
+
val shouldPlayCustomRingtone = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
1292
|
+
// For newer APIs, be more cautious about playing custom ringtone
|
|
1293
|
+
// if CallStyle notification is being used
|
|
1294
|
+
!supportsCallStyleNotifications()
|
|
1295
|
+
} else {
|
|
1296
|
+
true
|
|
850
1297
|
}
|
|
851
1298
|
|
|
852
|
-
if (
|
|
853
|
-
|
|
1299
|
+
if (shouldPlayCustomRingtone) {
|
|
1300
|
+
val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
1301
|
+
ringtone = RingtoneManager.getRingtone(context, uri)
|
|
1302
|
+
ringtone?.play()
|
|
1303
|
+
isCustomRingtoneActive = true
|
|
1304
|
+
Log.d(TAG, "Custom ringtone started playing")
|
|
854
1305
|
} else {
|
|
855
|
-
|
|
1306
|
+
Log.d(TAG, "Skipping custom ringtone - system should handle it")
|
|
856
1307
|
}
|
|
1308
|
+
} catch (e: Exception) {
|
|
1309
|
+
Log.e(TAG, "Failed to play ringtone", e)
|
|
1310
|
+
}
|
|
857
1311
|
}
|
|
1312
|
+
}
|
|
858
1313
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1314
|
+
fun stopRingtone() {
|
|
1315
|
+
synchronized(ringtoneStateLock) {
|
|
1316
|
+
try {
|
|
1317
|
+
ringtone?.stop()
|
|
1318
|
+
isCustomRingtoneActive = false
|
|
1319
|
+
Log.d(TAG, "Ringtone stopped")
|
|
1320
|
+
} catch (e: Exception) {
|
|
1321
|
+
Log.e(TAG, "Error stopping ringtone", e)
|
|
1322
|
+
}
|
|
1323
|
+
ringtone = null
|
|
1324
|
+
|
|
1325
|
+
vibrator?.cancel()
|
|
1326
|
+
vibrator = null
|
|
863
1327
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
private fun startRingback() {
|
|
1331
|
+
val context = requireContext()
|
|
1332
|
+
if (ringbackPlayer?.isPlaying == true) return
|
|
1333
|
+
|
|
1334
|
+
try {
|
|
1335
|
+
val ringbackUri =
|
|
1336
|
+
Uri.parse("android.resource://${context.packageName}/raw/ringback_tone")
|
|
1337
|
+
ringbackPlayer = MediaPlayer.create(context, ringbackUri)
|
|
1338
|
+
ringbackPlayer?.apply {
|
|
1339
|
+
isLooping = true
|
|
1340
|
+
start()
|
|
1341
|
+
}
|
|
1342
|
+
} catch (e: Exception) {
|
|
1343
|
+
Log.e(TAG, "Failed to play ringback tone: ${e.message}")
|
|
867
1344
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
private fun stopRingback() {
|
|
1348
|
+
try {
|
|
1349
|
+
ringbackPlayer?.stop()
|
|
1350
|
+
ringbackPlayer?.release()
|
|
1351
|
+
} catch (e: Exception) {
|
|
1352
|
+
Log.e(TAG, "Error stopping ringback: ${e.message}")
|
|
1353
|
+
} finally {
|
|
1354
|
+
ringbackPlayer = null
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
private fun cleanup() {
|
|
1359
|
+
Log.d(TAG, "Performing cleanup")
|
|
1360
|
+
stopForegroundService()
|
|
1361
|
+
keepScreenAwake(false)
|
|
1362
|
+
resetAudioMode()
|
|
1363
|
+
synchronized(ringtoneStateLock) {
|
|
1364
|
+
isCustomRingtoneActive = false
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
fun onApplicationTerminate() {
|
|
1369
|
+
Log.d(TAG, "Application terminating")
|
|
1370
|
+
activeCalls.keys.toList().forEach { callId ->
|
|
1371
|
+
telecomConnections[callId]?.let { conn ->
|
|
1372
|
+
conn.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
|
1373
|
+
conn.destroy()
|
|
1374
|
+
}
|
|
888
1375
|
}
|
|
1376
|
+
activeCalls.clear()
|
|
1377
|
+
telecomConnections.clear()
|
|
1378
|
+
callMetadata.clear()
|
|
1379
|
+
incomingCallIds.clear() // NEW: Clear duplicate tracking
|
|
1380
|
+
currentCallId = null
|
|
1381
|
+
cleanup()
|
|
1382
|
+
lockScreenBypassCallbacks.clear()
|
|
1383
|
+
eventHandler = null
|
|
1384
|
+
cachedEvents.clear()
|
|
1385
|
+
isInitialized.set(false)
|
|
1386
|
+
appContext = null
|
|
1387
|
+
}
|
|
889
1388
|
}
|