@qusaieilouti99/call-manager 0.1.142 → 0.1.144
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,6 +1,7 @@
|
|
|
1
1
|
package com.margelo.nitro.qusaieilouti99.callmanager
|
|
2
2
|
|
|
3
3
|
import android.app.ActivityManager
|
|
4
|
+
import android.app.KeyguardManager
|
|
4
5
|
import android.app.Notification
|
|
5
6
|
import android.app.NotificationChannel
|
|
6
7
|
import android.app.NotificationManager
|
|
@@ -8,8 +9,6 @@ import android.app.PendingIntent
|
|
|
8
9
|
import android.content.ComponentName
|
|
9
10
|
import android.content.Context
|
|
10
11
|
import android.content.Intent
|
|
11
|
-
import android.graphics.Color
|
|
12
|
-
import android.media.AudioAttributes
|
|
13
12
|
import android.media.AudioDeviceCallback
|
|
14
13
|
import android.media.AudioDeviceInfo
|
|
15
14
|
import android.media.AudioManager
|
|
@@ -21,6 +20,8 @@ import android.os.Bundle
|
|
|
21
20
|
import android.os.Handler
|
|
22
21
|
import android.os.Looper
|
|
23
22
|
import android.os.PowerManager
|
|
23
|
+
import android.os.VibrationEffect
|
|
24
|
+
import android.os.Vibrator
|
|
24
25
|
import android.telecom.Connection
|
|
25
26
|
import android.telecom.DisconnectCause
|
|
26
27
|
import android.telecom.PhoneAccount
|
|
@@ -32,1357 +33,797 @@ import org.json.JSONObject
|
|
|
32
33
|
import java.util.concurrent.ConcurrentHashMap
|
|
33
34
|
import java.util.concurrent.CopyOnWriteArrayList
|
|
34
35
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
35
|
-
import android.app.KeyguardManager
|
|
36
|
-
import android.os.Vibrator
|
|
37
|
-
import android.os.VibrationEffect
|
|
38
36
|
|
|
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
|
-
*/
|
|
46
37
|
object CallEngine {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
}
|
|
110
|
-
}
|
|
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
|
-
}
|
|
135
|
-
}
|
|
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))
|
|
146
|
-
}
|
|
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
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
fun isLockScreenBypassActive(): Boolean = lockScreenBypassActive
|
|
38
|
+
private const val TAG = "CallEngine"
|
|
39
|
+
private const val PHONE_ACCOUNT_ID = "com.qusaieilouti99.callmanager.SELF_MANAGED"
|
|
40
|
+
private const val NOTIF_CHANNEL_ID = "incoming_call_channel"
|
|
41
|
+
private const val NOTIF_ID = 2001
|
|
209
42
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
fun removeTelecomConnection(callId: String) {
|
|
216
|
-
telecomConnections.remove(callId)
|
|
217
|
-
Log.d(TAG, "Removed Telecom Connection for callId: $callId")
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
fun getTelecomConnection(callId: String): Connection? = telecomConnections[callId]
|
|
43
|
+
interface CallEndListener {
|
|
44
|
+
fun onCallEnded(callId: String)
|
|
45
|
+
}
|
|
221
46
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
Log.d(TAG, "canMakeMultipleCalls set to: $allow")
|
|
225
|
-
}
|
|
47
|
+
private val callEndListeners = CopyOnWriteArrayList<CallEndListener>()
|
|
48
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
226
49
|
|
|
227
|
-
|
|
228
|
-
|
|
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)
|
|
50
|
+
fun registerCallEndListener(l: CallEndListener) {
|
|
51
|
+
callEndListeners.add(l)
|
|
246
52
|
}
|
|
247
53
|
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
54
|
+
fun unregisterCallEndListener(l: CallEndListener) {
|
|
55
|
+
callEndListeners.remove(l)
|
|
254
56
|
}
|
|
255
|
-
incomingCallIds[callId] = currentTime
|
|
256
57
|
|
|
257
|
-
|
|
258
|
-
val
|
|
259
|
-
|
|
58
|
+
@Volatile private var appContext: Context? = null
|
|
59
|
+
private val isInitialized = AtomicBoolean(false)
|
|
60
|
+
private val initializationLock = Any()
|
|
260
61
|
|
|
261
|
-
|
|
262
|
-
|
|
62
|
+
private var ringtone: android.media.Ringtone? = null
|
|
63
|
+
private var ringbackPlayer: MediaPlayer? = null
|
|
64
|
+
private var vibrator: Vibrator? = null
|
|
65
|
+
private var audioManager: AudioManager? = null
|
|
66
|
+
private var wakeLock: PowerManager.WakeLock? = null
|
|
263
67
|
|
|
264
|
-
val
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
return
|
|
269
|
-
}
|
|
68
|
+
private val activeCalls = ConcurrentHashMap<String, CallInfo>()
|
|
69
|
+
private val telecomConnections = ConcurrentHashMap<String, Connection>()
|
|
70
|
+
private val callMetadata = ConcurrentHashMap<String, String>()
|
|
71
|
+
private val incomingCallIds = ConcurrentHashMap<String, Long>()
|
|
270
72
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
73
|
+
private var currentCallId: String? = null
|
|
74
|
+
private var canMakeMultipleCalls: Boolean = false
|
|
75
|
+
private var eventHandler: ((CallEventType, String) -> Unit)? = null
|
|
76
|
+
private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
|
|
77
|
+
|
|
78
|
+
// --- Modern Audio Management State ---
|
|
79
|
+
private var userSelectedAudioRoute: String? = null
|
|
80
|
+
private val audioStateLock = Any()
|
|
279
81
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
82
|
+
fun initialize(context: Context) {
|
|
83
|
+
synchronized(initializationLock) {
|
|
84
|
+
if (isInitialized.compareAndSet(false, true)) {
|
|
85
|
+
appContext = context.applicationContext
|
|
86
|
+
audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
|
87
|
+
registerAudioDeviceCallback() // Register callback on init
|
|
88
|
+
Log.d(TAG, "CallEngine initialized successfully")
|
|
89
|
+
}
|
|
285
90
|
}
|
|
286
|
-
}
|
|
287
91
|
}
|
|
288
92
|
|
|
289
|
-
|
|
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) }
|
|
306
|
-
}
|
|
93
|
+
fun isInitialized(): Boolean = isInitialized.get()
|
|
307
94
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
} catch (e: Exception) {
|
|
313
|
-
Log.e(TAG, "Failed to report incoming call: ${e.message}", e)
|
|
314
|
-
endCallInternal(callId)
|
|
95
|
+
private fun requireContext(): Context {
|
|
96
|
+
return appContext ?: throw IllegalStateException(
|
|
97
|
+
"CallEngine not initialized. Call initialize() in Application.onCreate()"
|
|
98
|
+
)
|
|
315
99
|
}
|
|
316
100
|
|
|
317
|
-
|
|
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
|
|
337
|
-
}
|
|
101
|
+
fun getContext(): Context? = appContext
|
|
338
102
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
103
|
+
fun setEventHandler(handler: ((CallEventType, String) -> Unit)?) {
|
|
104
|
+
Log.d(TAG, "setEventHandler called. Handler present: ${handler != null}")
|
|
105
|
+
eventHandler = handler
|
|
106
|
+
handler?.let { h ->
|
|
107
|
+
if (cachedEvents.isNotEmpty()) {
|
|
108
|
+
Log.d(TAG, "Emitting ${cachedEvents.size} cached events.")
|
|
109
|
+
cachedEvents.forEach { (type, data) -> h.invoke(type, data) }
|
|
110
|
+
cachedEvents.clear()
|
|
111
|
+
}
|
|
344
112
|
}
|
|
345
|
-
}
|
|
346
113
|
}
|
|
347
114
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
|
|
115
|
+
fun emitEvent(type: CallEventType, data: JSONObject) {
|
|
116
|
+
Log.d(TAG, "Emitting event: $type")
|
|
117
|
+
val dataString = data.toString()
|
|
118
|
+
if (eventHandler != null) {
|
|
119
|
+
eventHandler?.invoke(type, dataString)
|
|
120
|
+
} else {
|
|
121
|
+
Log.d(TAG, "No event handler, caching event: $type")
|
|
122
|
+
cachedEvents.add(Pair(type, dataString))
|
|
123
|
+
}
|
|
124
|
+
}
|
|
359
125
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
metadata?.let { putString("metadata", it) }
|
|
126
|
+
private fun supportsCallStyleNotifications(): Boolean {
|
|
127
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return false
|
|
128
|
+
val manufacturer = Build.MANUFACTURER.lowercase()
|
|
129
|
+
val supportedManufacturers = setOf("google", "samsung")
|
|
130
|
+
return supportedManufacturers.contains(manufacturer)
|
|
366
131
|
}
|
|
367
132
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, isVideoCall)
|
|
133
|
+
fun silenceIncomingCall() {
|
|
134
|
+
Log.d(TAG, "Silencing incoming call ringtone via Connection.onSilence()")
|
|
135
|
+
stopRingtone()
|
|
372
136
|
}
|
|
373
137
|
|
|
374
|
-
|
|
375
|
-
|
|
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)
|
|
138
|
+
fun addTelecomConnection(callId: String, connection: Connection) {
|
|
139
|
+
telecomConnections[callId] = connection
|
|
384
140
|
}
|
|
385
141
|
|
|
386
|
-
|
|
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
|
|
142
|
+
fun removeTelecomConnection(callId: String) {
|
|
143
|
+
telecomConnections.remove(callId)
|
|
401
144
|
}
|
|
402
145
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
146
|
+
fun getTelecomConnection(callId: String): Connection? = telecomConnections[callId]
|
|
147
|
+
|
|
148
|
+
fun reportIncomingCall(
|
|
149
|
+
context: Context,
|
|
150
|
+
callId: String,
|
|
151
|
+
callType: String,
|
|
152
|
+
displayName: String,
|
|
153
|
+
pictureUrl: String? = null,
|
|
154
|
+
metadata: String? = null
|
|
155
|
+
) {
|
|
156
|
+
if (!isInitialized.get()) {
|
|
157
|
+
initialize(context)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
val currentTime = System.currentTimeMillis()
|
|
161
|
+
if (incomingCallIds.containsKey(callId) || activeCalls.containsKey(callId)) {
|
|
162
|
+
Log.w(TAG, "Ignoring duplicate incoming call report for callId: $callId")
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
incomingCallIds[callId] = currentTime
|
|
166
|
+
|
|
167
|
+
Log.d(TAG, "reportIncomingCall: callId=$callId, type=$callType, name=$displayName")
|
|
168
|
+
metadata?.let { callMetadata[callId] = it }
|
|
169
|
+
|
|
170
|
+
activeCalls[callId] =
|
|
171
|
+
CallInfo(callId, callType, displayName, pictureUrl, CallState.INCOMING)
|
|
172
|
+
currentCallId = callId
|
|
173
|
+
|
|
174
|
+
showIncomingCallUI(callId, displayName, callType, pictureUrl)
|
|
175
|
+
registerPhoneAccount()
|
|
176
|
+
|
|
177
|
+
val telecomManager =
|
|
178
|
+
requireContext().getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
179
|
+
val phoneAccountHandle = getPhoneAccountHandle()
|
|
180
|
+
val extras = Bundle().apply {
|
|
181
|
+
putString(MyConnectionService.EXTRA_CALL_ID, callId)
|
|
182
|
+
putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
|
|
183
|
+
putString(MyConnectionService.EXTRA_DISPLAY_NAME, displayName)
|
|
184
|
+
putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, callType == "Video")
|
|
185
|
+
pictureUrl?.let { putString(MyConnectionService.EXTRA_PICTURE_URL, it) }
|
|
407
186
|
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
187
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
|
188
|
+
try {
|
|
189
|
+
telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
|
|
190
|
+
startForegroundService()
|
|
191
|
+
} catch (e: Exception) {
|
|
192
|
+
Log.e(TAG, "Failed to report incoming call: ${e.message}", e)
|
|
193
|
+
endCallInternal(callId)
|
|
194
|
+
}
|
|
447
195
|
}
|
|
448
196
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
197
|
+
fun startOutgoingCall(
|
|
198
|
+
callId: String,
|
|
199
|
+
callType: String,
|
|
200
|
+
targetName: String,
|
|
201
|
+
metadata: String? = null
|
|
202
|
+
) {
|
|
203
|
+
val context = requireContext()
|
|
204
|
+
Log.d(TAG, "startOutgoingCall: callId=$callId, type=$callType, target=$targetName")
|
|
205
|
+
metadata?.let { callMetadata[callId] = it }
|
|
206
|
+
|
|
207
|
+
activeCalls[callId] = CallInfo(callId, callType, targetName, null, CallState.DIALING)
|
|
208
|
+
currentCallId = callId
|
|
209
|
+
Log.d(TAG, "Call $callId added to activeCalls. State: DIALING")
|
|
210
|
+
|
|
211
|
+
registerPhoneAccount()
|
|
212
|
+
|
|
213
|
+
val telecomManager =
|
|
214
|
+
context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
215
|
+
val phoneAccountHandle = getPhoneAccountHandle()
|
|
216
|
+
val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
|
|
217
|
+
|
|
218
|
+
val outgoingExtras = Bundle().apply {
|
|
219
|
+
putString(MyConnectionService.EXTRA_CALL_ID, callId)
|
|
220
|
+
putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
|
|
221
|
+
putString(MyConnectionService.EXTRA_DISPLAY_NAME, targetName)
|
|
222
|
+
putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, callType == "Video")
|
|
223
|
+
metadata?.let { putString("metadata", it) }
|
|
224
|
+
}
|
|
452
225
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
226
|
+
val extras = Bundle().apply {
|
|
227
|
+
putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
|
|
228
|
+
putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
|
|
229
|
+
putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, callType == "Video")
|
|
230
|
+
}
|
|
456
231
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
232
|
+
try {
|
|
233
|
+
telecomManager.placeCall(addressUri, extras)
|
|
234
|
+
startForegroundService()
|
|
235
|
+
startRingback()
|
|
236
|
+
bringAppToForeground()
|
|
237
|
+
keepScreenAwake(true)
|
|
238
|
+
Log.d(TAG, "Successfully reported outgoing call to TelecomManager")
|
|
239
|
+
} catch (e: Exception) {
|
|
240
|
+
Log.e(TAG, "Failed to start outgoing call: ${e.message}", e)
|
|
241
|
+
endCallInternal(callId)
|
|
461
242
|
}
|
|
462
|
-
}
|
|
463
243
|
}
|
|
464
244
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
245
|
+
fun startCall(
|
|
246
|
+
callId: String,
|
|
247
|
+
callType: String,
|
|
248
|
+
targetName: String,
|
|
249
|
+
metadata: String? = null
|
|
250
|
+
) {
|
|
251
|
+
Log.d(TAG, "startCall: callId=$callId, type=$callType, target=$targetName")
|
|
252
|
+
metadata?.let { callMetadata[callId] = it }
|
|
253
|
+
|
|
254
|
+
if (activeCalls.containsKey(callId)) {
|
|
255
|
+
Log.w(TAG, "Call $callId already exists, cannot start again")
|
|
256
|
+
return
|
|
257
|
+
}
|
|
469
258
|
|
|
470
|
-
|
|
259
|
+
activeCalls[callId] =
|
|
260
|
+
CallInfo(callId, callType, targetName, null, CallState.ACTIVE)
|
|
261
|
+
currentCallId = callId
|
|
262
|
+
Log.d(TAG, "Call $callId started as ACTIVE")
|
|
471
263
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
264
|
+
registerPhoneAccount()
|
|
265
|
+
bringAppToForeground()
|
|
266
|
+
startForegroundService()
|
|
267
|
+
keepScreenAwake(true)
|
|
475
268
|
|
|
476
|
-
|
|
477
|
-
|
|
269
|
+
synchronized(audioStateLock) {
|
|
270
|
+
userSelectedAudioRoute = null
|
|
271
|
+
}
|
|
478
272
|
mainHandler.postDelayed({
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (isLocalAnswer) {
|
|
489
|
-
emitCallAnsweredWithMetadata(callId)
|
|
490
|
-
} else {
|
|
491
|
-
emitOutgoingCallAnsweredWithMetadata(callId)
|
|
273
|
+
updateAndApplyAudioRoute()
|
|
274
|
+
}, 300)
|
|
275
|
+
|
|
276
|
+
emitEvent(CallEventType.OUTGOING_CALL_ANSWERED, JSONObject().apply {
|
|
277
|
+
put("callId", callId)
|
|
278
|
+
put("callType", callType)
|
|
279
|
+
put("displayName", targetName)
|
|
280
|
+
})
|
|
492
281
|
}
|
|
493
282
|
|
|
494
|
-
|
|
495
|
-
|
|
283
|
+
fun answerCall(callId: String) {
|
|
284
|
+
Log.d(TAG, "answerCall: $callId - local party answering")
|
|
285
|
+
coreCallAnswered(callId, isLocalAnswer = true)
|
|
286
|
+
}
|
|
496
287
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
288
|
+
private fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
|
|
289
|
+
val callInfo = activeCalls[callId] ?: return
|
|
290
|
+
activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
|
|
291
|
+
currentCallId = callId
|
|
500
292
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
293
|
+
stopRingtone()
|
|
294
|
+
stopRingback()
|
|
295
|
+
cancelIncomingCallUI()
|
|
296
|
+
|
|
297
|
+
bringAppToForeground()
|
|
298
|
+
startForegroundService()
|
|
299
|
+
keepScreenAwake(true)
|
|
300
|
+
|
|
301
|
+
// Set initial audio route using the new robust system
|
|
302
|
+
synchronized(audioStateLock) {
|
|
303
|
+
userSelectedAudioRoute = null // Reset any previous user selection
|
|
511
304
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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)
|
|
305
|
+
// Give the audio system a moment to initialize after the call becomes active
|
|
306
|
+
mainHandler.postDelayed({
|
|
307
|
+
updateAndApplyAudioRoute()
|
|
308
|
+
}, 300)
|
|
309
|
+
|
|
310
|
+
if (isLocalAnswer) {
|
|
311
|
+
emitCallAnsweredWithMetadata(callId)
|
|
530
312
|
}
|
|
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
|
|
545
313
|
}
|
|
546
314
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
|
315
|
+
private fun emitCallAnsweredWithMetadata(callId: String) {
|
|
316
|
+
val callInfo = activeCalls[callId] ?: return
|
|
317
|
+
emitEvent(CallEventType.CALL_ANSWERED, JSONObject().apply {
|
|
318
|
+
put("callId", callId)
|
|
319
|
+
put("callType", callInfo.callType)
|
|
320
|
+
put("displayName", callInfo.displayName)
|
|
321
|
+
callInfo.pictureUrl?.let { put("pictureUrl", it) }
|
|
322
|
+
})
|
|
560
323
|
}
|
|
561
324
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
|
325
|
+
fun holdCall(callId: String) {
|
|
326
|
+
val callInfo = activeCalls[callId]
|
|
327
|
+
if (callInfo?.state != CallState.ACTIVE) {
|
|
328
|
+
Log.w(TAG, "Cannot hold call $callId - not in active state")
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
activeCalls[callId] = callInfo.copy(state = CallState.HELD)
|
|
332
|
+
telecomConnections[callId]?.setOnHold()
|
|
333
|
+
updateForegroundNotification()
|
|
334
|
+
emitEvent(CallEventType.CALL_HELD, JSONObject().put("callId", callId))
|
|
583
335
|
}
|
|
584
336
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
|
337
|
+
fun unholdCall(callId: String) {
|
|
338
|
+
val callInfo = activeCalls[callId]
|
|
339
|
+
if (callInfo?.state != CallState.HELD) {
|
|
340
|
+
Log.w(TAG, "Cannot unhold call $callId - not in held state")
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
|
|
344
|
+
telecomConnections[callId]?.setActive()
|
|
345
|
+
updateForegroundNotification()
|
|
346
|
+
emitEvent(CallEventType.CALL_UNHELD, JSONObject().put("callId", callId))
|
|
613
347
|
}
|
|
614
348
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
349
|
+
fun setMuted(callId: String, muted: Boolean) {
|
|
350
|
+
val callInfo = activeCalls[callId]
|
|
351
|
+
if (callInfo == null) {
|
|
352
|
+
Log.w(TAG, "Cannot set mute state for call $callId - not found")
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
val am = audioManager ?: return
|
|
356
|
+
am.isMicrophoneMute = muted
|
|
357
|
+
val eventType = if (muted) CallEventType.CALL_MUTED else CallEventType.CALL_UNMUTED
|
|
358
|
+
emitEvent(eventType, JSONObject().put("callId", callId))
|
|
625
359
|
}
|
|
626
|
-
}
|
|
627
360
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
}
|
|
361
|
+
fun endCall(callId: String) {
|
|
362
|
+
endCallInternal(callId)
|
|
363
|
+
}
|
|
632
364
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
365
|
+
fun endAllCalls() {
|
|
366
|
+
Log.d(TAG, "endAllCalls: Ending all active calls")
|
|
367
|
+
if (activeCalls.isEmpty()) return
|
|
636
368
|
|
|
637
|
-
|
|
638
|
-
|
|
369
|
+
activeCalls.keys.toList().forEach { callId ->
|
|
370
|
+
endCallInternal(callId)
|
|
371
|
+
}
|
|
639
372
|
}
|
|
640
373
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
currentCallId = null
|
|
374
|
+
private fun endCallInternal(callId: String) {
|
|
375
|
+
val callInfo = activeCalls.remove(callId) ?: return
|
|
376
|
+
incomingCallIds.remove(callId)
|
|
377
|
+
callMetadata.remove(callId)
|
|
646
378
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
379
|
+
stopRingtone()
|
|
380
|
+
stopRingback()
|
|
381
|
+
cancelIncomingCallUI()
|
|
650
382
|
|
|
651
|
-
|
|
652
|
-
|
|
383
|
+
telecomConnections.remove(callId)?.let {
|
|
384
|
+
it.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
|
385
|
+
it.destroy()
|
|
386
|
+
}
|
|
653
387
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
388
|
+
if (activeCalls.isEmpty()) {
|
|
389
|
+
cleanup()
|
|
390
|
+
} else {
|
|
391
|
+
updateForegroundNotification()
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
callEndListeners.forEach { it.onCallEnded(callId) }
|
|
395
|
+
emitEvent(CallEventType.CALL_ENDED, JSONObject().put("callId", callId))
|
|
657
396
|
}
|
|
658
397
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
398
|
+
/**
|
|
399
|
+
* Sets the user's desired audio route. This will be respected until a
|
|
400
|
+
* higher-priority device connects or this route becomes unavailable.
|
|
401
|
+
*/
|
|
402
|
+
fun setAudioRoute(route: String) {
|
|
403
|
+
synchronized(audioStateLock) {
|
|
404
|
+
Log.d(TAG, "User requested audio route: $route")
|
|
405
|
+
userSelectedAudioRoute = route
|
|
406
|
+
}
|
|
407
|
+
updateAndApplyAudioRoute()
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Central function to determine and apply the correct audio route.
|
|
412
|
+
* This is the single source of truth for audio routing decisions.
|
|
413
|
+
*/
|
|
414
|
+
private fun updateAndApplyAudioRoute() {
|
|
415
|
+
val call = activeCalls[currentCallId]
|
|
416
|
+
if (call == null || call.state != CallState.ACTIVE) {
|
|
417
|
+
Log.d(TAG, "Skipping audio route update: No active call.")
|
|
418
|
+
return
|
|
419
|
+
}
|
|
662
420
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
421
|
+
synchronized(audioStateLock) {
|
|
422
|
+
val am = audioManager ?: return
|
|
423
|
+
val availableDevices = getAvailableAudioDevices()
|
|
424
|
+
|
|
425
|
+
// Determine the target route based on priority
|
|
426
|
+
val targetRoute = if (userSelectedAudioRoute != null && availableDevices.contains(userSelectedAudioRoute)) {
|
|
427
|
+
userSelectedAudioRoute!! // Respect user's choice if available
|
|
428
|
+
} else {
|
|
429
|
+
// Auto-select based on priority
|
|
430
|
+
when {
|
|
431
|
+
availableDevices.contains("Bluetooth") -> "Bluetooth"
|
|
432
|
+
availableDevices.contains("Headset") -> "Headset"
|
|
433
|
+
call.callType == "Video" -> "Speaker"
|
|
434
|
+
else -> "Earpiece"
|
|
435
|
+
}
|
|
436
|
+
}
|
|
666
437
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
438
|
+
Log.d(TAG, "Updating audio route. Available: $availableDevices, User Pref: $userSelectedAudioRoute, Target: $targetRoute")
|
|
439
|
+
|
|
440
|
+
// Apply the target route
|
|
441
|
+
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
442
|
+
|
|
443
|
+
when (targetRoute) {
|
|
444
|
+
"Bluetooth" -> {
|
|
445
|
+
am.isSpeakerphoneOn = false
|
|
446
|
+
am.startBluetoothSco()
|
|
447
|
+
am.isBluetoothScoOn = true
|
|
448
|
+
}
|
|
449
|
+
"Headset" -> {
|
|
450
|
+
am.isSpeakerphoneOn = false
|
|
451
|
+
am.stopBluetoothSco()
|
|
452
|
+
am.isBluetoothScoOn = false
|
|
453
|
+
}
|
|
454
|
+
"Speaker" -> {
|
|
455
|
+
am.isSpeakerphoneOn = true
|
|
456
|
+
am.stopBluetoothSco()
|
|
457
|
+
am.isBluetoothScoOn = false
|
|
458
|
+
}
|
|
459
|
+
"Earpiece" -> {
|
|
460
|
+
am.isSpeakerphoneOn = false
|
|
461
|
+
am.stopBluetoothSco()
|
|
462
|
+
am.isBluetoothScoOn = false
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// After applying, emit the result
|
|
467
|
+
mainHandler.postDelayed({ emitAudioRouteChanged() }, 100)
|
|
468
|
+
}
|
|
670
469
|
}
|
|
671
470
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
471
|
+
private fun getAvailableAudioDevices(): Set<String> {
|
|
472
|
+
val am = audioManager ?: return emptySet()
|
|
473
|
+
val devices = mutableSetOf<String>()
|
|
474
|
+
devices.add("Earpiece") // Always available
|
|
475
|
+
devices.add("Speaker") // Always available
|
|
676
476
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
477
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
478
|
+
val audioDevices = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
|
479
|
+
audioDevices.forEach {
|
|
480
|
+
when (it.type) {
|
|
481
|
+
AudioDeviceInfo.TYPE_BLUETOOTH_SCO, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> devices.add("Bluetooth")
|
|
482
|
+
AudioDeviceInfo.TYPE_WIRED_HEADSET, AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> devices.add("Headset")
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
} else {
|
|
486
|
+
@Suppress("DEPRECATION")
|
|
487
|
+
if (am.isBluetoothScoAvailableOffCall) devices.add("Bluetooth")
|
|
488
|
+
@Suppress("DEPRECATION")
|
|
489
|
+
if (am.isWiredHeadsetOn) devices.add("Headset")
|
|
490
|
+
}
|
|
491
|
+
return devices
|
|
682
492
|
}
|
|
683
493
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
494
|
+
fun getAudioDevices(): AudioRoutesInfo {
|
|
495
|
+
val available = getAvailableAudioDevices().map { StringHolder(it) }.toTypedArray()
|
|
496
|
+
val current = getCurrentAudioRoute()
|
|
497
|
+
return AudioRoutesInfo(available, current)
|
|
688
498
|
}
|
|
689
499
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
500
|
+
private fun getCurrentAudioRoute(): String {
|
|
501
|
+
val am = audioManager ?: return "Unknown"
|
|
502
|
+
return when {
|
|
503
|
+
am.isBluetoothScoOn -> "Bluetooth"
|
|
504
|
+
am.isWiredHeadsetOn -> "Headset"
|
|
505
|
+
am.isSpeakerphoneOn -> "Speaker"
|
|
506
|
+
else -> "Earpiece"
|
|
507
|
+
}
|
|
694
508
|
}
|
|
695
509
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
listener.onCallEnded(callId)
|
|
702
|
-
} catch (_: Throwable) {
|
|
703
|
-
// swallow
|
|
510
|
+
private fun emitAudioRouteChanged() {
|
|
511
|
+
val info = getAudioDevices()
|
|
512
|
+
val payload = JSONObject().apply {
|
|
513
|
+
put("devices", JSONArray(info.devices.map { it.value }))
|
|
514
|
+
put("currentRoute", info.currentRoute)
|
|
704
515
|
}
|
|
705
|
-
|
|
516
|
+
emitEvent(CallEventType.AUDIO_ROUTE_CHANGED, payload)
|
|
517
|
+
Log.d(TAG, "Emitted AUDIO_ROUTE_CHANGED: Current='${info.currentRoute}', Available=${info.devices.map { it.value }}")
|
|
706
518
|
}
|
|
707
519
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
AudioDeviceInfo.TYPE_WIRED_HEADSET -> devices.add("Headset")
|
|
520
|
+
private val audioDeviceCallback = object : AudioDeviceCallback() {
|
|
521
|
+
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
|
|
522
|
+
Log.d(TAG, "Audio devices added. Triggering audio route update.")
|
|
523
|
+
// A new device was added, reset user preference to allow auto-switch to higher priority device
|
|
524
|
+
synchronized(audioStateLock) {
|
|
525
|
+
userSelectedAudioRoute = null
|
|
526
|
+
}
|
|
527
|
+
updateAndApplyAudioRoute()
|
|
528
|
+
}
|
|
529
|
+
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
|
|
530
|
+
Log.d(TAG, "Audio devices removed. Triggering audio route update.")
|
|
531
|
+
// A device was removed, if it was the user's selection, clear it
|
|
532
|
+
synchronized(audioStateLock) {
|
|
533
|
+
val removedDeviceTypes = removedDevices.map {
|
|
534
|
+
when(it.type) {
|
|
535
|
+
AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "Bluetooth"
|
|
536
|
+
AudioDeviceInfo.TYPE_WIRED_HEADSET -> "Headset"
|
|
537
|
+
else -> ""
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (removedDeviceTypes.contains(userSelectedAudioRoute)) {
|
|
541
|
+
userSelectedAudioRoute = null
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
updateAndApplyAudioRoute()
|
|
734
545
|
}
|
|
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
546
|
}
|
|
743
547
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
}
|
|
752
|
-
|
|
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
|
|
548
|
+
private fun registerAudioDeviceCallback() {
|
|
549
|
+
audioManager?.registerAudioDeviceCallback(audioDeviceCallback, mainHandler)
|
|
759
550
|
}
|
|
760
|
-
val am = audioManager!!
|
|
761
551
|
|
|
762
|
-
|
|
763
|
-
|
|
552
|
+
private fun cleanup() {
|
|
553
|
+
Log.d(TAG, "Performing cleanup")
|
|
554
|
+
stopForegroundService()
|
|
555
|
+
keepScreenAwake(false)
|
|
556
|
+
// Reset audio state
|
|
557
|
+
synchronized(audioStateLock) {
|
|
558
|
+
userSelectedAudioRoute = null
|
|
559
|
+
}
|
|
560
|
+
audioManager?.mode = AudioManager.MODE_NORMAL
|
|
561
|
+
audioManager?.isSpeakerphoneOn = false
|
|
562
|
+
audioManager?.stopBluetoothSco()
|
|
764
563
|
}
|
|
765
564
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
783
|
-
"Bluetooth" -> {
|
|
784
|
-
am.isSpeakerphoneOn = false
|
|
785
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
786
|
-
am.startBluetoothSco()
|
|
787
|
-
am.isBluetoothScoOn = true
|
|
788
|
-
Log.d(TAG, "Audio routed to BLUETOOTH")
|
|
789
|
-
} else {
|
|
790
|
-
Log.w(TAG, "Bluetooth SCO not supported on this OS version")
|
|
791
|
-
}
|
|
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
|
|
565
|
+
private fun createNotificationChannel() {
|
|
566
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
567
|
+
val context = requireContext()
|
|
568
|
+
val channel = NotificationChannel(
|
|
569
|
+
NOTIF_CHANNEL_ID,
|
|
570
|
+
"Incoming Call Channel",
|
|
571
|
+
NotificationManager.IMPORTANCE_HIGH
|
|
572
|
+
).apply {
|
|
573
|
+
description = "Notifications for incoming calls"
|
|
574
|
+
setBypassDnd(true)
|
|
575
|
+
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
576
|
+
// **CRITICAL FIX**: Set sound to null. We will manage the ringtone manually.
|
|
577
|
+
setSound(null, null)
|
|
578
|
+
}
|
|
579
|
+
val manager = context.getSystemService(NotificationManager::class.java)
|
|
580
|
+
manager.createNotificationChannel(channel)
|
|
798
581
|
}
|
|
799
|
-
Log.d(TAG, "Audio routed to HEADSET")
|
|
800
|
-
}
|
|
801
|
-
else -> {
|
|
802
|
-
Log.w(TAG, "Unknown audio route: $route")
|
|
803
|
-
return
|
|
804
|
-
}
|
|
805
|
-
}
|
|
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"
|
|
815
|
-
}
|
|
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"
|
|
829
582
|
}
|
|
830
583
|
|
|
831
|
-
|
|
584
|
+
private fun showIncomingCallUI(callId: String, callerName: String, callType: String, callerPicUrl: String?) {
|
|
585
|
+
val context = requireContext()
|
|
586
|
+
Log.d(TAG, "Showing incoming call UI for $callId")
|
|
587
|
+
createNotificationChannel() // Ensure channel is created with correct settings
|
|
832
588
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
setAudioMode()
|
|
589
|
+
showCallActivityOverlay(context, callId, callerName, callType, callerPicUrl)
|
|
590
|
+
showStandardNotification(context, callId, callerName, callType, callerPicUrl)
|
|
836
591
|
|
|
837
|
-
|
|
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")
|
|
848
|
-
}
|
|
849
|
-
}, 300L)
|
|
850
|
-
}, 200L)
|
|
851
|
-
} else {
|
|
852
|
-
setAudioRoute(defaultRoute)
|
|
853
|
-
}
|
|
854
|
-
} else {
|
|
855
|
-
setAudioRoute(defaultRoute)
|
|
856
|
-
}
|
|
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")
|
|
871
|
-
}
|
|
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)
|
|
881
|
-
}
|
|
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()
|
|
592
|
+
playRingtone()
|
|
890
593
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
}
|
|
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"
|
|
929
|
-
)
|
|
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")
|
|
594
|
+
|
|
595
|
+
private fun showCallActivityOverlay(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
|
|
596
|
+
val overlayIntent = Intent(context, CallActivity::class.java).apply {
|
|
597
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
|
|
598
|
+
putExtra("callId", callId)
|
|
599
|
+
putExtra("callerName", callerName)
|
|
600
|
+
putExtra("callType", callType)
|
|
601
|
+
callerPicUrl?.let { putExtra("callerAvatar", it) }
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
context.startActivity(overlayIntent)
|
|
605
|
+
} catch (e: Exception) {
|
|
606
|
+
Log.e(TAG, "Failed to launch CallActivity overlay: ${e.message}")
|
|
938
607
|
}
|
|
939
|
-
}
|
|
940
|
-
wakeLock = null
|
|
941
|
-
}
|
|
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
|
|
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
608
|
}
|
|
1000
|
-
}
|
|
1001
609
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
610
|
+
private fun showStandardNotification(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
|
|
611
|
+
val fullScreenIntent = Intent(context, CallActivity::class.java).apply {
|
|
612
|
+
putExtra("callId", callId)
|
|
613
|
+
}
|
|
614
|
+
val fullScreenPendingIntent = PendingIntent.getActivity(
|
|
615
|
+
context, callId.hashCode(), fullScreenIntent,
|
|
616
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
617
|
+
)
|
|
1005
618
|
|
|
1006
|
-
|
|
1007
|
-
|
|
619
|
+
val builder = Notification.Builder(context, NOTIF_CHANNEL_ID)
|
|
620
|
+
.setSmallIcon(android.R.drawable.sym_call_incoming)
|
|
621
|
+
.setContentTitle("Incoming Call")
|
|
622
|
+
.setContentText(callerName)
|
|
623
|
+
.setPriority(Notification.PRIORITY_MAX)
|
|
624
|
+
.setCategory(Notification.CATEGORY_CALL)
|
|
625
|
+
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
626
|
+
.setOngoing(true)
|
|
627
|
+
|
|
628
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && supportsCallStyleNotifications()) {
|
|
629
|
+
val person = android.app.Person.Builder().setName(callerName).build()
|
|
630
|
+
val answerIntent = Intent(context, CallNotificationActionReceiver::class.java).apply { action = "com.qusaieilouti99.callmanager.ANSWER_CALL"; putExtra("callId", callId) }
|
|
631
|
+
val declineIntent = Intent(context, CallNotificationActionReceiver::class.java).apply { action = "com.qusaieilouti99.callmanager.DECLINE_CALL"; putExtra("callId", callId) }
|
|
632
|
+
val answerPI = PendingIntent.getBroadcast(context, callId.hashCode() + 1, answerIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
633
|
+
val declinePI = PendingIntent.getBroadcast(context, callId.hashCode() + 2, declineIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
634
|
+
builder.setStyle(Notification.CallStyle.forIncomingCall(person, declinePI, answerPI))
|
|
635
|
+
}
|
|
1008
636
|
|
|
1009
|
-
|
|
1010
|
-
|
|
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)
|
|
637
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
638
|
+
notificationManager.notify(NOTIF_ID, builder.build())
|
|
1015
639
|
}
|
|
1016
640
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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)
|
|
641
|
+
fun cancelIncomingCallUI() {
|
|
642
|
+
val context = requireContext()
|
|
643
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
644
|
+
notificationManager.cancel(NOTIF_ID)
|
|
645
|
+
stopRingtone()
|
|
1039
646
|
}
|
|
1040
647
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
}
|
|
648
|
+
private fun playRingtone() {
|
|
649
|
+
if (ringtone?.isPlaying == true) return
|
|
650
|
+
val context = requireContext()
|
|
651
|
+
audioManager?.mode = AudioManager.MODE_RINGTONE
|
|
1067
652
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
653
|
+
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
|
654
|
+
val pattern = longArrayOf(0, 1000, 500)
|
|
655
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
656
|
+
vibrator?.vibrate(VibrationEffect.createWaveform(pattern, 0))
|
|
657
|
+
} else {
|
|
658
|
+
@Suppress("DEPRECATION")
|
|
659
|
+
vibrator?.vibrate(pattern, 0)
|
|
660
|
+
}
|
|
1072
661
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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()
|
|
662
|
+
try {
|
|
663
|
+
val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
664
|
+
ringtone = RingtoneManager.getRingtone(context, ringtoneUri)
|
|
665
|
+
ringtone?.play()
|
|
666
|
+
Log.d(TAG, "Manual ringtone started.")
|
|
667
|
+
} catch (e: Exception) {
|
|
668
|
+
Log.e(TAG, "Failed to play ringtone", e)
|
|
669
|
+
}
|
|
1126
670
|
}
|
|
1127
671
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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
|
|
672
|
+
fun stopRingtone() {
|
|
673
|
+
vibrator?.cancel()
|
|
674
|
+
vibrator = null
|
|
675
|
+
ringtone?.stop()
|
|
676
|
+
ringtone = null
|
|
677
|
+
Log.d(TAG, "Manual ringtone stopped.")
|
|
1146
678
|
}
|
|
1147
679
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
intent.putExtra("callType", it.callType)
|
|
1152
|
-
intent.putExtra("displayName", it.displayName)
|
|
1153
|
-
intent.putExtra("state", it.state.name)
|
|
1154
|
-
}
|
|
680
|
+
private fun startRingback() {
|
|
681
|
+
val context = requireContext()
|
|
682
|
+
if (ringbackPlayer?.isPlaying == true) return
|
|
1155
683
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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
|
|
684
|
+
try {
|
|
685
|
+
val ringbackUri =
|
|
686
|
+
Uri.parse("android.resource://${context.packageName}/raw/ringback_tone")
|
|
687
|
+
ringbackPlayer = MediaPlayer.create(context, ringbackUri)
|
|
688
|
+
ringbackPlayer?.apply {
|
|
689
|
+
isLooping = true
|
|
690
|
+
start()
|
|
691
|
+
}
|
|
692
|
+
} catch (e: Exception) {
|
|
693
|
+
Log.e(TAG, "Failed to play ringback tone: ${e.message}")
|
|
1184
694
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private fun stopRingback() {
|
|
698
|
+
try {
|
|
699
|
+
ringbackPlayer?.stop()
|
|
700
|
+
ringbackPlayer?.release()
|
|
701
|
+
} catch (e: Exception) {
|
|
702
|
+
Log.e(TAG, "Error stopping ringback: ${e.message}")
|
|
703
|
+
} finally {
|
|
704
|
+
ringbackPlayer = null
|
|
1195
705
|
}
|
|
1196
|
-
} catch (e: Exception) {
|
|
1197
|
-
Log.w(TAG, "Failed to get running tasks: ${e.message}")
|
|
1198
|
-
}
|
|
1199
706
|
}
|
|
1200
|
-
return false
|
|
1201
|
-
}
|
|
1202
707
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
708
|
+
fun keepScreenAwake(keepAwake: Boolean) {
|
|
709
|
+
val context = requireContext()
|
|
710
|
+
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
|
711
|
+
if (keepAwake) {
|
|
712
|
+
if (wakeLock == null || wakeLock!!.isHeld.not()) {
|
|
713
|
+
wakeLock = powerManager.newWakeLock(
|
|
714
|
+
PowerManager.SCREEN_DIM_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
|
715
|
+
"CallEngine:WakeLock"
|
|
716
|
+
)
|
|
717
|
+
wakeLock?.acquire(10 * 60 * 1000L)
|
|
718
|
+
Log.d(TAG, "Acquired SCREEN_DIM_WAKE_LOCK")
|
|
719
|
+
}
|
|
720
|
+
} else {
|
|
721
|
+
wakeLock?.let {
|
|
722
|
+
if (it.isHeld) {
|
|
723
|
+
it.release()
|
|
724
|
+
Log.d(TAG, "Released SCREEN_DIM_WAKE_LOCK")
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
wakeLock = null
|
|
728
|
+
}
|
|
1207
729
|
}
|
|
1208
730
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
731
|
+
private fun bringAppToForeground() {
|
|
732
|
+
val context = requireContext()
|
|
733
|
+
val packageName = context.packageName
|
|
734
|
+
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
|
|
735
|
+
launchIntent?.addFlags(
|
|
736
|
+
Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
737
|
+
Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
|
738
|
+
Intent.FLAG_ACTIVITY_SINGLE_TOP
|
|
739
|
+
)
|
|
740
|
+
try {
|
|
741
|
+
context.startActivity(launchIntent)
|
|
742
|
+
} catch (e: Exception) {
|
|
743
|
+
Log.e(TAG, "Failed to bring app to foreground: ${e.message}")
|
|
744
|
+
}
|
|
1221
745
|
}
|
|
1222
746
|
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
747
|
+
private fun registerPhoneAccount() {
|
|
748
|
+
val context = requireContext()
|
|
749
|
+
val telecomManager =
|
|
750
|
+
context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
751
|
+
val phoneAccountHandle = getPhoneAccountHandle()
|
|
752
|
+
|
|
753
|
+
if (telecomManager.getPhoneAccount(phoneAccountHandle) == null) {
|
|
754
|
+
val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "PingMe Call")
|
|
755
|
+
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
|
|
756
|
+
.build()
|
|
757
|
+
|
|
758
|
+
try {
|
|
759
|
+
telecomManager.registerPhoneAccount(phoneAccount)
|
|
760
|
+
Log.d(TAG, "PhoneAccount registered successfully")
|
|
761
|
+
} catch (e: Exception) {
|
|
762
|
+
Log.e(TAG, "Failed to register PhoneAccount: ${e.message}", e)
|
|
763
|
+
}
|
|
764
|
+
}
|
|
1230
765
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
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
|
-
}
|
|
766
|
+
|
|
767
|
+
private fun getPhoneAccountHandle(): PhoneAccountHandle {
|
|
768
|
+
val context = requireContext()
|
|
769
|
+
return PhoneAccountHandle(
|
|
770
|
+
ComponentName(context, MyConnectionService::class.java),
|
|
771
|
+
PHONE_ACCOUNT_ID
|
|
772
|
+
)
|
|
1250
773
|
}
|
|
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
|
-
}
|
|
1277
774
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
val
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
v.vibrate(pattern, 0)
|
|
775
|
+
private fun startForegroundService() {
|
|
776
|
+
val context = requireContext()
|
|
777
|
+
val currentCall = activeCalls.values.find {
|
|
778
|
+
it.state == CallState.ACTIVE ||
|
|
779
|
+
it.state == CallState.INCOMING ||
|
|
780
|
+
it.state == CallState.DIALING ||
|
|
781
|
+
it.state == CallState.HELD
|
|
1286
782
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
!supportsCallStyleNotifications()
|
|
1295
|
-
} else {
|
|
1296
|
-
true
|
|
783
|
+
|
|
784
|
+
val intent = Intent(context, CallForegroundService::class.java)
|
|
785
|
+
currentCall?.let {
|
|
786
|
+
intent.putExtra("callId", it.callId)
|
|
787
|
+
intent.putExtra("callType", it.callType)
|
|
788
|
+
intent.putExtra("displayName", it.displayName)
|
|
789
|
+
intent.putExtra("state", it.state.name)
|
|
1297
790
|
}
|
|
1298
791
|
|
|
1299
|
-
if (
|
|
1300
|
-
|
|
1301
|
-
ringtone = RingtoneManager.getRingtone(context, uri)
|
|
1302
|
-
ringtone?.play()
|
|
1303
|
-
isCustomRingtoneActive = true
|
|
1304
|
-
Log.d(TAG, "Custom ringtone started playing")
|
|
792
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
793
|
+
context.startForegroundService(intent)
|
|
1305
794
|
} else {
|
|
1306
|
-
|
|
795
|
+
context.startService(intent)
|
|
1307
796
|
}
|
|
1308
|
-
} catch (e: Exception) {
|
|
1309
|
-
Log.e(TAG, "Failed to play ringtone", e)
|
|
1310
|
-
}
|
|
1311
797
|
}
|
|
1312
|
-
}
|
|
1313
798
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
|
1327
|
-
}
|
|
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}")
|
|
1344
|
-
}
|
|
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
|
|
799
|
+
private fun stopForegroundService() {
|
|
800
|
+
val context = requireContext()
|
|
801
|
+
val intent = Intent(context, CallForegroundService::class.java)
|
|
802
|
+
context.stopService(intent)
|
|
1355
803
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
Log.d(TAG, "Performing cleanup")
|
|
1360
|
-
stopForegroundService()
|
|
1361
|
-
keepScreenAwake(false)
|
|
1362
|
-
resetAudioMode()
|
|
1363
|
-
synchronized(ringtoneStateLock) {
|
|
1364
|
-
isCustomRingtoneActive = false
|
|
804
|
+
|
|
805
|
+
private fun updateForegroundNotification() {
|
|
806
|
+
startForegroundService()
|
|
1365
807
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
808
|
+
|
|
809
|
+
fun onApplicationTerminate() {
|
|
810
|
+
Log.d(TAG, "Application terminating")
|
|
811
|
+
activeCalls.keys.toList().forEach { callId ->
|
|
812
|
+
telecomConnections[callId]?.let { conn ->
|
|
813
|
+
conn.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
|
814
|
+
conn.destroy()
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
activeCalls.clear()
|
|
818
|
+
telecomConnections.clear()
|
|
819
|
+
callMetadata.clear()
|
|
820
|
+
incomingCallIds.clear()
|
|
821
|
+
currentCallId = null
|
|
822
|
+
cleanup()
|
|
823
|
+
audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
|
|
824
|
+
eventHandler = null
|
|
825
|
+
cachedEvents.clear()
|
|
826
|
+
isInitialized.set(false)
|
|
827
|
+
appContext = null
|
|
1375
828
|
}
|
|
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
|
-
}
|
|
1388
829
|
}
|