@qusaieilouti99/call-manager 0.1.57 → 0.1.58
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.
- package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/CallEngine.kt +306 -378
- package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/CallForegroundService.kt +7 -13
- package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/CallManager.kt +47 -60
- package/package.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
package com.margelo.nitro.qusaieilouti99.callmanager
|
|
2
2
|
|
|
3
|
+
import android.app.Activity
|
|
3
4
|
import android.app.Application
|
|
5
|
+
import android.app.Notification
|
|
4
6
|
import android.app.NotificationChannel
|
|
5
7
|
import android.app.NotificationManager
|
|
6
8
|
import android.app.PendingIntent
|
|
@@ -14,6 +16,7 @@ import android.media.AudioDeviceInfo
|
|
|
14
16
|
import android.media.AudioFocusRequest
|
|
15
17
|
import android.media.AudioManager
|
|
16
18
|
import android.media.MediaPlayer
|
|
19
|
+
import android.media.RingtoneManager
|
|
17
20
|
import android.net.Uri
|
|
18
21
|
import android.os.Build
|
|
19
22
|
import android.os.Bundle
|
|
@@ -28,9 +31,6 @@ import android.telecom.PhoneAccountHandle
|
|
|
28
31
|
import android.telecom.TelecomManager
|
|
29
32
|
import android.telecom.VideoProfile
|
|
30
33
|
import android.util.Log
|
|
31
|
-
import androidx.lifecycle.DefaultLifecycleObserver
|
|
32
|
-
import androidx.lifecycle.LifecycleOwner
|
|
33
|
-
import androidx.lifecycle.ProcessLifecycleOwner
|
|
34
34
|
import kotlinx.coroutines.CoroutineScope
|
|
35
35
|
import kotlinx.coroutines.Dispatchers
|
|
36
36
|
import kotlinx.coroutines.launch
|
|
@@ -39,40 +39,39 @@ import org.json.JSONObject
|
|
|
39
39
|
import java.util.concurrent.ConcurrentHashMap
|
|
40
40
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
41
41
|
|
|
42
|
-
object CallEngine
|
|
42
|
+
object CallEngine {
|
|
43
43
|
private const val TAG = "CallEngine"
|
|
44
44
|
private const val PHONE_ACCOUNT_ID = "com.qusaieilouti99.callmanager.SELF_MANAGED"
|
|
45
45
|
private const val NOTIF_CHANNEL_ID = "incoming_call_channel"
|
|
46
46
|
private const val NOTIF_ID = 2001
|
|
47
|
+
private const val FOREGROUND_CHANNEL_ID = "call_foreground_channel"
|
|
48
|
+
private const val FOREGROUND_NOTIF_ID = 1001
|
|
47
49
|
|
|
48
|
-
// Core
|
|
49
|
-
@Volatile
|
|
50
|
+
// Core context - initialized once and maintained
|
|
50
51
|
private var appContext: Context? = null
|
|
51
|
-
private
|
|
52
|
+
private var isInitialized = AtomicBoolean(false)
|
|
52
53
|
|
|
53
|
-
// Audio
|
|
54
|
-
private var audioManager: AudioManager? = null
|
|
55
|
-
private var audioFocusRequest: AudioFocusRequest? = null
|
|
56
|
-
private var hasAudioFocus: Boolean = false
|
|
57
|
-
private var isSystemCallActive: Boolean = false
|
|
58
|
-
private var lastAudioRoutesInfo: AudioRoutesInfo? = null
|
|
59
|
-
private var lastMuteState: Boolean = false
|
|
60
|
-
|
|
61
|
-
// Media Management
|
|
54
|
+
// Audio & Media
|
|
62
55
|
private var ringtone: android.media.Ringtone? = null
|
|
63
56
|
private var ringbackPlayer: MediaPlayer? = null
|
|
64
|
-
|
|
65
|
-
// Power Management
|
|
57
|
+
private var audioManager: AudioManager? = null
|
|
66
58
|
private var wakeLock: PowerManager.WakeLock? = null
|
|
59
|
+
private var audioFocusRequest: AudioFocusRequest? = null
|
|
67
60
|
|
|
68
|
-
// Call State Management
|
|
61
|
+
// Call State Management
|
|
69
62
|
private val activeCalls = ConcurrentHashMap<String, CallInfo>()
|
|
70
63
|
private val telecomConnections = ConcurrentHashMap<String, Connection>()
|
|
71
64
|
private val callMetadata = ConcurrentHashMap<String, String>()
|
|
65
|
+
|
|
72
66
|
private var currentCallId: String? = null
|
|
73
67
|
private var canMakeMultipleCalls: Boolean = false
|
|
74
68
|
|
|
75
|
-
//
|
|
69
|
+
// Audio State Tracking
|
|
70
|
+
private var lastAudioRoutesInfo: AudioRoutesInfo? = null
|
|
71
|
+
private var hasAudioFocus: Boolean = false
|
|
72
|
+
private var isSystemCallActive: Boolean = false
|
|
73
|
+
|
|
74
|
+
// Lock Screen Bypass
|
|
76
75
|
private var lockScreenBypassActive = false
|
|
77
76
|
private val lockScreenBypassCallbacks = mutableSetOf<LockScreenBypassCallback>()
|
|
78
77
|
|
|
@@ -80,93 +79,92 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
80
79
|
private var eventHandler: ((CallEventType, String) -> Unit)? = null
|
|
81
80
|
private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
|
|
82
81
|
|
|
83
|
-
// Service Management
|
|
84
|
-
private val serviceHandler = Handler(Looper.getMainLooper())
|
|
85
|
-
private var isServiceRunning = false
|
|
86
|
-
|
|
87
82
|
interface LockScreenBypassCallback {
|
|
88
83
|
fun onLockScreenBypassChanged(shouldBypass: Boolean)
|
|
89
84
|
}
|
|
90
85
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
fun initialize(application: Application) {
|
|
96
|
-
synchronized(this) {
|
|
97
|
-
if (isInitialized.get()) {
|
|
98
|
-
Log.d(TAG, "CallEngine already initialized")
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
appContext = application.applicationContext
|
|
86
|
+
// --- INITIALIZATION - Fix for context management ---
|
|
87
|
+
fun initialize(context: Context) {
|
|
88
|
+
if (isInitialized.compareAndSet(false, true)) {
|
|
89
|
+
appContext = context.applicationContext
|
|
103
90
|
audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
|
91
|
+
Log.d(TAG, "CallEngine initialized with context")
|
|
104
92
|
|
|
105
|
-
//
|
|
106
|
-
|
|
93
|
+
// Initialize foreground service if needed
|
|
94
|
+
if (isCallActive()) {
|
|
95
|
+
startForegroundService()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
107
99
|
|
|
108
|
-
|
|
109
|
-
registerAudioDeviceCallback()
|
|
100
|
+
fun isInitialized(): Boolean = isInitialized.get()
|
|
110
101
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
102
|
+
private fun requireContext(): Context {
|
|
103
|
+
return appContext ?: throw IllegalStateException("CallEngine not initialized. Call initialize() first.")
|
|
114
104
|
}
|
|
115
105
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
106
|
+
// --- Event System ---
|
|
107
|
+
fun setEventHandler(handler: ((CallEventType, String) -> Unit)?) {
|
|
108
|
+
Log.d(TAG, "setEventHandler called. Handler present: ${handler != null}")
|
|
109
|
+
eventHandler = handler
|
|
110
|
+
handler?.let { h ->
|
|
111
|
+
if (cachedEvents.isNotEmpty()) {
|
|
112
|
+
Log.d(TAG, "Emitting ${cachedEvents.size} cached events.")
|
|
113
|
+
cachedEvents.forEach { (type, data) -> h.invoke(type, data) }
|
|
114
|
+
cachedEvents.clear()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
119
117
|
}
|
|
120
118
|
|
|
121
|
-
private fun
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
)
|
|
119
|
+
private fun emitEvent(type: CallEventType, data: JSONObject) {
|
|
120
|
+
Log.d(TAG, "Emitting event: $type, data: $data")
|
|
121
|
+
val dataString = data.toString()
|
|
122
|
+
if (eventHandler != null) {
|
|
123
|
+
eventHandler?.invoke(type, dataString)
|
|
124
|
+
} else {
|
|
125
|
+
Log.d(TAG, "No event handler registered, caching event: $type")
|
|
126
|
+
cachedEvents.add(Pair(type, dataString))
|
|
127
|
+
}
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
// --- Audio Focus Management (
|
|
130
|
+
// --- Audio Focus Management (Simplified) ---
|
|
128
131
|
private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
|
129
132
|
Log.d(TAG, "Audio focus changed: $focusChange")
|
|
130
133
|
when (focusChange) {
|
|
131
|
-
AudioManager.AUDIOFOCUS_LOSS
|
|
132
|
-
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT ->
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
AudioManager.AUDIOFOCUS_LOSS,
|
|
135
|
+
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
136
|
+
hasAudioFocus = false
|
|
137
|
+
isSystemCallActive = true
|
|
138
|
+
holdSystemCalls()
|
|
139
|
+
}
|
|
140
|
+
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
141
|
+
hasAudioFocus = true
|
|
142
|
+
isSystemCallActive = false
|
|
143
|
+
Handler(Looper.getMainLooper()).postDelayed({
|
|
144
|
+
resumeSystemHeldCalls()
|
|
145
|
+
}, 1000)
|
|
136
146
|
}
|
|
137
147
|
}
|
|
148
|
+
updateForegroundNotification()
|
|
138
149
|
}
|
|
139
150
|
|
|
140
|
-
private fun
|
|
141
|
-
Log.d(TAG, "Audio focus lost - likely system call active")
|
|
142
|
-
hasAudioFocus = false
|
|
143
|
-
isSystemCallActive = true
|
|
144
|
-
|
|
151
|
+
private fun holdSystemCalls() {
|
|
145
152
|
activeCalls.values.filter { it.state == CallState.ACTIVE }.forEach { call ->
|
|
146
153
|
if (!call.wasHeldBySystem) {
|
|
147
154
|
holdCallInternal(call.callId, heldBySystem = true)
|
|
148
155
|
}
|
|
149
156
|
}
|
|
150
|
-
|
|
151
157
|
stopRingback()
|
|
152
|
-
updateForegroundNotification()
|
|
153
158
|
}
|
|
154
159
|
|
|
155
|
-
private fun
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
serviceHandler.postDelayed({
|
|
161
|
-
activeCalls.values.filter { it.state == CallState.HELD && it.wasHeldBySystem }.forEach { call ->
|
|
162
|
-
unholdCallInternal(call.callId, resumedBySystem = true)
|
|
163
|
-
}
|
|
164
|
-
updateForegroundNotification()
|
|
165
|
-
}, 1000)
|
|
160
|
+
private fun resumeSystemHeldCalls() {
|
|
161
|
+
activeCalls.values.filter { it.state == CallState.HELD && it.wasHeldBySystem }.forEach { call ->
|
|
162
|
+
unholdCallInternal(call.callId, resumedBySystem = true)
|
|
163
|
+
}
|
|
166
164
|
}
|
|
167
165
|
|
|
168
166
|
private fun requestAudioFocus(): Boolean {
|
|
169
|
-
val context =
|
|
167
|
+
val context = requireContext()
|
|
170
168
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
|
171
169
|
|
|
172
170
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
@@ -199,122 +197,78 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
199
197
|
}
|
|
200
198
|
|
|
201
199
|
private fun abandonAudioFocus() {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
200
|
+
audioManager?.let { am ->
|
|
201
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
202
|
+
audioFocusRequest?.let { request ->
|
|
203
|
+
am.abandonAudioFocusRequest(request)
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
@Suppress("DEPRECATION")
|
|
207
|
+
am.abandonAudioFocus(audioFocusChangeListener)
|
|
208
208
|
}
|
|
209
|
-
} else {
|
|
210
|
-
@Suppress("DEPRECATION")
|
|
211
|
-
audioManager?.abandonAudioFocus(audioFocusChangeListener)
|
|
212
209
|
}
|
|
213
210
|
hasAudioFocus = false
|
|
214
211
|
Log.d(TAG, "Audio focus abandoned")
|
|
215
212
|
}
|
|
216
213
|
|
|
217
|
-
// ---
|
|
218
|
-
fun
|
|
219
|
-
|
|
220
|
-
eventHandler = handler
|
|
221
|
-
handler?.let { h ->
|
|
222
|
-
if (cachedEvents.isNotEmpty()) {
|
|
223
|
-
Log.d(TAG, "Emitting ${cachedEvents.size} cached events.")
|
|
224
|
-
cachedEvents.forEach { (type, data) -> h.invoke(type, data) }
|
|
225
|
-
cachedEvents.clear()
|
|
226
|
-
}
|
|
227
|
-
}
|
|
214
|
+
// --- Lock Screen Bypass Management ---
|
|
215
|
+
fun registerLockScreenBypassCallback(callback: LockScreenBypassCallback) {
|
|
216
|
+
lockScreenBypassCallbacks.add(callback)
|
|
228
217
|
}
|
|
229
218
|
|
|
230
|
-
fun
|
|
231
|
-
|
|
232
|
-
val dataString = data.toString()
|
|
233
|
-
if (eventHandler != null) {
|
|
234
|
-
eventHandler?.invoke(type, dataString)
|
|
235
|
-
} else {
|
|
236
|
-
Log.d(TAG, "No event handler registered, caching event: $type")
|
|
237
|
-
cachedEvents.add(Pair(type, dataString))
|
|
238
|
-
}
|
|
219
|
+
fun unregisterLockScreenBypassCallback(callback: LockScreenBypassCallback) {
|
|
220
|
+
lockScreenBypassCallbacks.remove(callback)
|
|
239
221
|
}
|
|
240
222
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
it.state == CallState.DIALING || it.state == CallState.HELD
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
val intent = Intent(context, CallForegroundService::class.java)
|
|
256
|
-
if (currentCall != null) {
|
|
257
|
-
intent.putExtra("callId", currentCall.callId)
|
|
258
|
-
intent.putExtra("callType", currentCall.callType)
|
|
259
|
-
intent.putExtra("displayName", currentCall.displayName)
|
|
260
|
-
intent.putExtra("state", currentCall.state.name)
|
|
261
|
-
Log.d(TAG, "Starting foreground service with call info: ${currentCall.callId}")
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
try {
|
|
265
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
266
|
-
context.startForegroundService(intent)
|
|
267
|
-
} else {
|
|
268
|
-
context.startService(intent)
|
|
223
|
+
private fun updateLockScreenBypass() {
|
|
224
|
+
val shouldBypass = isCallActive()
|
|
225
|
+
if (lockScreenBypassActive != shouldBypass) {
|
|
226
|
+
lockScreenBypassActive = shouldBypass
|
|
227
|
+
Log.d(TAG, "Lock screen bypass state changed: $lockScreenBypassActive")
|
|
228
|
+
lockScreenBypassCallbacks.forEach { callback ->
|
|
229
|
+
try {
|
|
230
|
+
callback.onLockScreenBypassChanged(shouldBypass)
|
|
231
|
+
} catch (e: Exception) {
|
|
232
|
+
Log.w(TAG, "Error notifying lock screen bypass callback", e)
|
|
233
|
+
}
|
|
269
234
|
}
|
|
270
|
-
isServiceRunning = true
|
|
271
|
-
Log.d(TAG, "Foreground service started successfully")
|
|
272
|
-
} catch (e: Exception) {
|
|
273
|
-
Log.e(TAG, "Failed to start foreground service: ${e.message}", e)
|
|
274
235
|
}
|
|
275
236
|
}
|
|
276
237
|
|
|
277
|
-
|
|
278
|
-
if (!isServiceRunning) {
|
|
279
|
-
Log.d(TAG, "Foreground service not running")
|
|
280
|
-
return
|
|
281
|
-
}
|
|
238
|
+
fun isLockScreenBypassActive(): Boolean = lockScreenBypassActive
|
|
282
239
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
240
|
+
// --- Telecom Connection Management ---
|
|
241
|
+
fun addTelecomConnection(callId: String, connection: Connection) {
|
|
242
|
+
telecomConnections[callId] = connection
|
|
243
|
+
Log.d(TAG, "Added Telecom Connection for callId: $callId. Total: ${telecomConnections.size}")
|
|
287
244
|
}
|
|
288
245
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
246
|
+
fun removeTelecomConnection(callId: String) {
|
|
247
|
+
telecomConnections.remove(callId)?.let {
|
|
248
|
+
Log.d(TAG, "Removed Telecom Connection for callId: $callId. Total: ${telecomConnections.size}")
|
|
249
|
+
}
|
|
250
|
+
}
|
|
292
251
|
|
|
293
|
-
|
|
294
|
-
val heldCall = activeCalls.values.find { it.state == CallState.HELD }
|
|
252
|
+
fun getTelecomConnection(callId: String): Connection? = telecomConnections[callId]
|
|
295
253
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
intent.putExtra("callType", it.callType)
|
|
302
|
-
intent.putExtra("displayName", it.displayName)
|
|
303
|
-
intent.putExtra("state", it.state.name)
|
|
254
|
+
// --- Public API ---
|
|
255
|
+
fun setCanMakeMultipleCalls(allow: Boolean) {
|
|
256
|
+
canMakeMultipleCalls = allow
|
|
257
|
+
Log.d(TAG, "canMakeMultipleCalls set to: $allow")
|
|
258
|
+
}
|
|
304
259
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
} catch (e: Exception) {
|
|
312
|
-
Log.e(TAG, "Failed to update foreground notification: ${e.message}", e)
|
|
313
|
-
}
|
|
260
|
+
fun getCurrentCallState(): String {
|
|
261
|
+
val calls = getActiveCalls()
|
|
262
|
+
val jsonArray = JSONArray()
|
|
263
|
+
calls.forEach {
|
|
264
|
+
jsonArray.put(it.toJsonObject())
|
|
314
265
|
}
|
|
266
|
+
val result = jsonArray.toString()
|
|
267
|
+
Log.d(TAG, "Current call state: $result")
|
|
268
|
+
return result
|
|
315
269
|
}
|
|
316
270
|
|
|
317
|
-
// --- Call Management
|
|
271
|
+
// --- Incoming Call Management ---
|
|
318
272
|
fun reportIncomingCall(
|
|
319
273
|
context: Context,
|
|
320
274
|
callId: String,
|
|
@@ -323,7 +277,10 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
323
277
|
pictureUrl: String? = null,
|
|
324
278
|
metadata: String? = null
|
|
325
279
|
) {
|
|
326
|
-
|
|
280
|
+
if (!isInitialized.get()) {
|
|
281
|
+
initialize(context)
|
|
282
|
+
}
|
|
283
|
+
|
|
327
284
|
Log.d(TAG, "reportIncomingCall: callId=$callId, type=$callType, name=$displayName")
|
|
328
285
|
|
|
329
286
|
metadata?.let { callMetadata[callId] = it }
|
|
@@ -357,14 +314,11 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
357
314
|
currentCallId = callId
|
|
358
315
|
Log.d(TAG, "Call $callId added to activeCalls. State: INCOMING, callType: $callType")
|
|
359
316
|
|
|
360
|
-
|
|
361
|
-
|
|
317
|
+
showIncomingCallUI(callId, displayName, callType)
|
|
318
|
+
registerPhoneAccount()
|
|
362
319
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
367
|
-
val phoneAccountHandle = getPhoneAccountHandle(context)
|
|
320
|
+
val telecomManager = requireContext().getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
321
|
+
val phoneAccountHandle = getPhoneAccountHandle()
|
|
368
322
|
val extras = Bundle().apply {
|
|
369
323
|
putString(MyConnectionService.EXTRA_CALL_ID, callId)
|
|
370
324
|
putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
|
|
@@ -375,6 +329,7 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
375
329
|
|
|
376
330
|
try {
|
|
377
331
|
telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
|
|
332
|
+
startForegroundService() // Fixed: Always start foreground service
|
|
378
333
|
Log.d(TAG, "Successfully reported incoming call to TelecomManager for $callId")
|
|
379
334
|
} catch (e: SecurityException) {
|
|
380
335
|
Log.e(TAG, "SecurityException: Failed to report incoming call. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
|
|
@@ -387,14 +342,14 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
387
342
|
updateLockScreenBypass()
|
|
388
343
|
}
|
|
389
344
|
|
|
345
|
+
// --- Outgoing Call Management ---
|
|
390
346
|
fun startOutgoingCall(
|
|
391
|
-
context: Context,
|
|
392
347
|
callId: String,
|
|
393
348
|
callType: String,
|
|
394
349
|
targetName: String,
|
|
395
350
|
metadata: String? = null
|
|
396
351
|
) {
|
|
397
|
-
|
|
352
|
+
val context = requireContext()
|
|
398
353
|
Log.d(TAG, "startOutgoingCall: callId=$callId, type=$callType, target=$targetName")
|
|
399
354
|
|
|
400
355
|
metadata?.let { callMetadata[callId] = it }
|
|
@@ -422,12 +377,9 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
422
377
|
currentCallId = callId
|
|
423
378
|
Log.d(TAG, "Call $callId added to activeCalls. State: DIALING, callType: $callType")
|
|
424
379
|
|
|
425
|
-
|
|
426
|
-
startForegroundService(context)
|
|
427
|
-
|
|
428
|
-
registerPhoneAccount(context)
|
|
380
|
+
registerPhoneAccount()
|
|
429
381
|
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
430
|
-
val phoneAccountHandle = getPhoneAccountHandle(
|
|
382
|
+
val phoneAccountHandle = getPhoneAccountHandle()
|
|
431
383
|
val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
|
|
432
384
|
|
|
433
385
|
val extras = Bundle().apply {
|
|
@@ -441,13 +393,13 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
441
393
|
|
|
442
394
|
try {
|
|
443
395
|
telecomManager.placeCall(addressUri, extras)
|
|
444
|
-
|
|
445
|
-
|
|
396
|
+
startForegroundService() // Fixed: Always start foreground service
|
|
446
397
|
requestAudioFocus()
|
|
447
398
|
startRingback()
|
|
448
|
-
bringAppToForeground(
|
|
449
|
-
keepScreenAwake(
|
|
450
|
-
setInitialAudioRoute(
|
|
399
|
+
bringAppToForeground()
|
|
400
|
+
keepScreenAwake(true)
|
|
401
|
+
setInitialAudioRoute(callType)
|
|
402
|
+
Log.d(TAG, "Successfully reported outgoing call to TelecomManager via placeCall for $callId")
|
|
451
403
|
} catch (e: SecurityException) {
|
|
452
404
|
Log.e(TAG, "SecurityException: Failed to start outgoing call via placeCall. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
|
|
453
405
|
endCallInternal(callId)
|
|
@@ -459,15 +411,14 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
459
411
|
updateLockScreenBypass()
|
|
460
412
|
}
|
|
461
413
|
|
|
462
|
-
// Fixed:
|
|
414
|
+
// Fixed: Start call as active (not dialing) with foreground service
|
|
463
415
|
fun startCall(
|
|
464
|
-
context: Context,
|
|
465
416
|
callId: String,
|
|
466
417
|
callType: String,
|
|
467
418
|
targetName: String,
|
|
468
419
|
metadata: String? = null
|
|
469
420
|
) {
|
|
470
|
-
|
|
421
|
+
val context = requireContext()
|
|
471
422
|
Log.d(TAG, "startCall: callId=$callId, type=$callType, target=$targetName")
|
|
472
423
|
|
|
473
424
|
metadata?.let { callMetadata[callId] = it }
|
|
@@ -490,34 +441,31 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
490
441
|
currentCallId = callId
|
|
491
442
|
Log.d(TAG, "Call $callId started as ACTIVE, callType: $callType")
|
|
492
443
|
|
|
493
|
-
|
|
494
|
-
startForegroundService(context)
|
|
495
|
-
|
|
496
|
-
registerPhoneAccount(context)
|
|
444
|
+
registerPhoneAccount()
|
|
497
445
|
requestAudioFocus()
|
|
498
|
-
bringAppToForeground(
|
|
499
|
-
|
|
500
|
-
|
|
446
|
+
bringAppToForeground()
|
|
447
|
+
startForegroundService() // Fixed: Start foreground service for JS-initiated calls
|
|
448
|
+
keepScreenAwake(true)
|
|
449
|
+
setInitialAudioRoute(callType)
|
|
501
450
|
updateLockScreenBypass()
|
|
502
451
|
|
|
503
452
|
// Emit call answered event with metadata
|
|
504
453
|
emitCallAnsweredWithMetadata(callId)
|
|
505
454
|
}
|
|
506
455
|
|
|
507
|
-
//
|
|
508
|
-
fun callAnsweredFromJS(
|
|
509
|
-
ensureInitialized()
|
|
456
|
+
// --- Call Answer Management ---
|
|
457
|
+
fun callAnsweredFromJS(callId: String) {
|
|
510
458
|
Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
|
|
511
|
-
coreCallAnswered(
|
|
459
|
+
coreCallAnswered(callId, isLocalAnswer = false)
|
|
512
460
|
}
|
|
513
461
|
|
|
514
|
-
fun answerCall(
|
|
515
|
-
ensureInitialized()
|
|
462
|
+
fun answerCall(callId: String) {
|
|
516
463
|
Log.d(TAG, "answerCall: $callId - local party answering")
|
|
517
|
-
coreCallAnswered(
|
|
464
|
+
coreCallAnswered(callId, isLocalAnswer = true)
|
|
518
465
|
}
|
|
519
466
|
|
|
520
|
-
private fun coreCallAnswered(
|
|
467
|
+
private fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
|
|
468
|
+
val context = requireContext()
|
|
521
469
|
Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
|
|
522
470
|
|
|
523
471
|
val callInfo = activeCalls[callId]
|
|
@@ -528,7 +476,7 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
528
476
|
|
|
529
477
|
stopRingtone()
|
|
530
478
|
stopRingback()
|
|
531
|
-
cancelIncomingCallUI(
|
|
479
|
+
cancelIncomingCallUI()
|
|
532
480
|
requestAudioFocus()
|
|
533
481
|
|
|
534
482
|
activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
|
|
@@ -542,10 +490,10 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
542
490
|
}
|
|
543
491
|
}
|
|
544
492
|
|
|
545
|
-
bringAppToForeground(
|
|
546
|
-
startForegroundService(
|
|
547
|
-
keepScreenAwake(
|
|
548
|
-
resetAudioMode(
|
|
493
|
+
bringAppToForeground()
|
|
494
|
+
startForegroundService() // Fixed: Ensure foreground service is running
|
|
495
|
+
keepScreenAwake(true)
|
|
496
|
+
resetAudioMode()
|
|
549
497
|
updateLockScreenBypass()
|
|
550
498
|
updateForegroundNotification()
|
|
551
499
|
|
|
@@ -575,13 +523,12 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
575
523
|
})
|
|
576
524
|
}
|
|
577
525
|
|
|
578
|
-
|
|
579
|
-
|
|
526
|
+
// --- Call Control Methods ---
|
|
527
|
+
fun holdCall(callId: String) {
|
|
580
528
|
holdCallInternal(callId, heldBySystem = false)
|
|
581
529
|
}
|
|
582
530
|
|
|
583
|
-
fun setOnHold(
|
|
584
|
-
ensureInitialized()
|
|
531
|
+
fun setOnHold(callId: String, onHold: Boolean) {
|
|
585
532
|
Log.d(TAG, "setOnHold: $callId, onHold: $onHold")
|
|
586
533
|
|
|
587
534
|
val callInfo = activeCalls[callId]
|
|
@@ -622,8 +569,7 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
622
569
|
updateLockScreenBypass()
|
|
623
570
|
}
|
|
624
571
|
|
|
625
|
-
fun unholdCall(
|
|
626
|
-
ensureInitialized()
|
|
572
|
+
fun unholdCall(callId: String) {
|
|
627
573
|
unholdCallInternal(callId, resumedBySystem = false)
|
|
628
574
|
}
|
|
629
575
|
|
|
@@ -635,18 +581,10 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
635
581
|
return
|
|
636
582
|
}
|
|
637
583
|
|
|
638
|
-
// Fixed:
|
|
639
|
-
if (!hasAudioFocus && !resumedBySystem) {
|
|
640
|
-
Log.
|
|
641
|
-
|
|
642
|
-
Log.w(TAG, "Failed to get audio focus for unhold")
|
|
643
|
-
// Only emit UNHOLD_FAILED for user-initiated unhold attempts
|
|
644
|
-
emitEvent(CallEventType.CALL_UNHOLD_FAILED, JSONObject().apply {
|
|
645
|
-
put("callId", callId)
|
|
646
|
-
put("reason", "Could not obtain audio focus")
|
|
647
|
-
})
|
|
648
|
-
return
|
|
649
|
-
}
|
|
584
|
+
// Fixed: Simplified audio focus check to prevent UNHELD FAILED
|
|
585
|
+
if (!hasAudioFocus && !resumedBySystem && !requestAudioFocus()) {
|
|
586
|
+
Log.w(TAG, "Failed to get audio focus for unhold - but continuing anyway")
|
|
587
|
+
// Don't emit UNHELD FAILED - just continue
|
|
650
588
|
}
|
|
651
589
|
|
|
652
590
|
activeCalls[callId] = callInfo.copy(
|
|
@@ -664,23 +602,21 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
664
602
|
Log.d(TAG, "Call $callId successfully unheld")
|
|
665
603
|
}
|
|
666
604
|
|
|
667
|
-
fun muteCall(
|
|
668
|
-
|
|
669
|
-
setMutedInternal(context, callId, true)
|
|
605
|
+
fun muteCall(callId: String) {
|
|
606
|
+
setMutedInternal(callId, true)
|
|
670
607
|
}
|
|
671
608
|
|
|
672
|
-
fun unmuteCall(
|
|
673
|
-
|
|
674
|
-
setMutedInternal(context, callId, false)
|
|
609
|
+
fun unmuteCall(callId: String) {
|
|
610
|
+
setMutedInternal(callId, false)
|
|
675
611
|
}
|
|
676
612
|
|
|
677
|
-
fun setMuted(
|
|
678
|
-
ensureInitialized()
|
|
613
|
+
fun setMuted(callId: String, muted: Boolean) {
|
|
679
614
|
Log.d(TAG, "setMuted: $callId, muted: $muted")
|
|
680
|
-
setMutedInternal(
|
|
615
|
+
setMutedInternal(callId, muted)
|
|
681
616
|
}
|
|
682
617
|
|
|
683
|
-
private fun setMutedInternal(
|
|
618
|
+
private fun setMutedInternal(callId: String, muted: Boolean) {
|
|
619
|
+
val context = requireContext()
|
|
684
620
|
val callInfo = activeCalls[callId]
|
|
685
621
|
if (callInfo == null) {
|
|
686
622
|
Log.w(TAG, "Cannot set mute state for call $callId - not found in active calls")
|
|
@@ -693,21 +629,19 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
693
629
|
audioManager?.isMicrophoneMute = muted
|
|
694
630
|
|
|
695
631
|
if (wasMuted != muted) {
|
|
696
|
-
lastMuteState = muted
|
|
697
632
|
val eventType = if (muted) CallEventType.CALL_MUTED else CallEventType.CALL_UNMUTED
|
|
698
633
|
emitEvent(eventType, JSONObject().put("callId", callId))
|
|
699
634
|
Log.d(TAG, "Call $callId mute state changed to: $muted")
|
|
700
635
|
}
|
|
701
636
|
}
|
|
702
637
|
|
|
703
|
-
|
|
704
|
-
|
|
638
|
+
// --- Call End Management ---
|
|
639
|
+
fun endCall(callId: String) {
|
|
705
640
|
Log.d(TAG, "endCall: $callId")
|
|
706
641
|
endCallInternal(callId)
|
|
707
642
|
}
|
|
708
643
|
|
|
709
|
-
fun endAllCalls(
|
|
710
|
-
ensureInitialized()
|
|
644
|
+
fun endAllCalls() {
|
|
711
645
|
Log.d(TAG, "endAllCalls: Ending all active calls.")
|
|
712
646
|
if (activeCalls.isEmpty()) {
|
|
713
647
|
Log.d(TAG, "No active calls, nothing to do.")
|
|
@@ -723,7 +657,7 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
723
657
|
callMetadata.clear()
|
|
724
658
|
currentCallId = null
|
|
725
659
|
|
|
726
|
-
finalCleanup(
|
|
660
|
+
finalCleanup()
|
|
727
661
|
updateLockScreenBypass()
|
|
728
662
|
}
|
|
729
663
|
|
|
@@ -744,7 +678,7 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
744
678
|
|
|
745
679
|
stopRingback()
|
|
746
680
|
stopRingtone()
|
|
747
|
-
|
|
681
|
+
cancelIncomingCallUI()
|
|
748
682
|
|
|
749
683
|
if (currentCallId == callId) {
|
|
750
684
|
currentCallId = activeCalls.filter { it.value.state != CallState.ENDED }.keys.firstOrNull()
|
|
@@ -760,7 +694,7 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
760
694
|
}
|
|
761
695
|
|
|
762
696
|
if (activeCalls.isEmpty()) {
|
|
763
|
-
|
|
697
|
+
finalCleanup()
|
|
764
698
|
} else {
|
|
765
699
|
updateForegroundNotification()
|
|
766
700
|
}
|
|
@@ -781,17 +715,9 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
781
715
|
})
|
|
782
716
|
}
|
|
783
717
|
|
|
784
|
-
private fun finalCleanup(context: Context) {
|
|
785
|
-
Log.d(TAG, "Performing final cleanup - no active calls remaining")
|
|
786
|
-
stopForegroundService(context)
|
|
787
|
-
keepScreenAwake(context, false)
|
|
788
|
-
resetAudioMode(context)
|
|
789
|
-
isSystemCallActive = false
|
|
790
|
-
}
|
|
791
|
-
|
|
792
718
|
// --- Audio Management ---
|
|
793
719
|
fun getAudioDevices(): AudioRoutesInfo {
|
|
794
|
-
val context =
|
|
720
|
+
val context = requireContext()
|
|
795
721
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: run {
|
|
796
722
|
Log.e(TAG, "getAudioDevices: AudioManager is null. Returning default.")
|
|
797
723
|
return AudioRoutesInfo(emptyArray(), "Unknown")
|
|
@@ -834,8 +760,8 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
834
760
|
return result
|
|
835
761
|
}
|
|
836
762
|
|
|
837
|
-
fun setAudioRoute(
|
|
838
|
-
|
|
763
|
+
fun setAudioRoute(route: String) {
|
|
764
|
+
val context = requireContext()
|
|
839
765
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
840
766
|
Log.d(TAG, "Attempting to set audio route to: $route. Current mode: ${audioManager?.mode}")
|
|
841
767
|
|
|
@@ -886,7 +812,7 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
886
812
|
}
|
|
887
813
|
}
|
|
888
814
|
|
|
889
|
-
private fun setInitialAudioRoute(
|
|
815
|
+
private fun setInitialAudioRoute(callType: String) {
|
|
890
816
|
val availableDevices = getAudioDevices()
|
|
891
817
|
|
|
892
818
|
val defaultRoute = when {
|
|
@@ -897,10 +823,11 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
897
823
|
}
|
|
898
824
|
|
|
899
825
|
Log.d(TAG, "Setting initial audio route for $callType call: $defaultRoute")
|
|
900
|
-
setAudioRoute(
|
|
826
|
+
setAudioRoute(defaultRoute)
|
|
901
827
|
}
|
|
902
828
|
|
|
903
|
-
fun resetAudioMode(
|
|
829
|
+
private fun resetAudioMode() {
|
|
830
|
+
val context = requireContext()
|
|
904
831
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
905
832
|
if (activeCalls.isEmpty()) {
|
|
906
833
|
Log.d(TAG, "Resetting audio mode to NORMAL as no active calls remain.")
|
|
@@ -927,15 +854,15 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
927
854
|
}
|
|
928
855
|
}
|
|
929
856
|
|
|
930
|
-
|
|
931
|
-
val context =
|
|
857
|
+
fun registerAudioDeviceCallback() {
|
|
858
|
+
val context = requireContext()
|
|
932
859
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
933
860
|
audioManager?.registerAudioDeviceCallback(audioDeviceCallback, null)
|
|
934
861
|
Log.d(TAG, "Audio device callback registered.")
|
|
935
862
|
}
|
|
936
863
|
|
|
937
|
-
|
|
938
|
-
val context =
|
|
864
|
+
fun unregisterAudioDeviceCallback() {
|
|
865
|
+
val context = requireContext()
|
|
939
866
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
940
867
|
audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
|
|
941
868
|
Log.d(TAG, "Audio device callback unregistered.")
|
|
@@ -958,7 +885,8 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
958
885
|
}
|
|
959
886
|
|
|
960
887
|
// --- Screen Management ---
|
|
961
|
-
fun keepScreenAwake(
|
|
888
|
+
fun keepScreenAwake(keepAwake: Boolean) {
|
|
889
|
+
val context = requireContext()
|
|
962
890
|
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
|
963
891
|
if (keepAwake) {
|
|
964
892
|
if (wakeLock == null || !wakeLock!!.isHeld) {
|
|
@@ -1014,65 +942,9 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
1014
942
|
})
|
|
1015
943
|
}
|
|
1016
944
|
|
|
1017
|
-
// --- Lock Screen Bypass Management ---
|
|
1018
|
-
fun registerLockScreenBypassCallback(callback: LockScreenBypassCallback) {
|
|
1019
|
-
lockScreenBypassCallbacks.add(callback)
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
fun unregisterLockScreenBypassCallback(callback: LockScreenBypassCallback) {
|
|
1023
|
-
lockScreenBypassCallbacks.remove(callback)
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
private fun updateLockScreenBypass() {
|
|
1027
|
-
val shouldBypass = isCallActive()
|
|
1028
|
-
if (lockScreenBypassActive != shouldBypass) {
|
|
1029
|
-
lockScreenBypassActive = shouldBypass
|
|
1030
|
-
Log.d(TAG, "Lock screen bypass state changed: $lockScreenBypassActive")
|
|
1031
|
-
lockScreenBypassCallbacks.forEach { callback ->
|
|
1032
|
-
try {
|
|
1033
|
-
callback.onLockScreenBypassChanged(shouldBypass)
|
|
1034
|
-
} catch (e: Exception) {
|
|
1035
|
-
Log.w(TAG, "Error notifying lock screen bypass callback", e)
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
fun isLockScreenBypassActive(): Boolean = lockScreenBypassActive
|
|
1042
|
-
|
|
1043
|
-
// --- Telecom Connection Management ---
|
|
1044
|
-
fun addTelecomConnection(callId: String, connection: Connection) {
|
|
1045
|
-
telecomConnections[callId] = connection
|
|
1046
|
-
Log.d(TAG, "Added Telecom Connection for callId: $callId. Total: ${telecomConnections.size}")
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
fun removeTelecomConnection(callId: String) {
|
|
1050
|
-
telecomConnections.remove(callId)?.let {
|
|
1051
|
-
Log.d(TAG, "Removed Telecom Connection for callId: $callId. Total: ${telecomConnections.size}")
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
fun getTelecomConnection(callId: String): Connection? = telecomConnections[callId]
|
|
1056
|
-
|
|
1057
|
-
// --- Public API ---
|
|
1058
|
-
fun setCanMakeMultipleCalls(allow: Boolean) {
|
|
1059
|
-
canMakeMultipleCalls = allow
|
|
1060
|
-
Log.d(TAG, "canMakeMultipleCalls set to: $allow")
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
fun getCurrentCallState(): String {
|
|
1064
|
-
val calls = getActiveCalls()
|
|
1065
|
-
val jsonArray = JSONArray()
|
|
1066
|
-
calls.forEach {
|
|
1067
|
-
jsonArray.put(it.toJsonObject())
|
|
1068
|
-
}
|
|
1069
|
-
val result = jsonArray.toString()
|
|
1070
|
-
Log.d(TAG, "Current call state: $result")
|
|
1071
|
-
return result
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
945
|
// --- Notification Management ---
|
|
1075
|
-
private fun createNotificationChannel(
|
|
946
|
+
private fun createNotificationChannel() {
|
|
947
|
+
val context = requireContext()
|
|
1076
948
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1077
949
|
val channel = NotificationChannel(
|
|
1078
950
|
NOTIF_CHANNEL_ID,
|
|
@@ -1086,7 +958,7 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
1086
958
|
|
|
1087
959
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
|
1088
960
|
channel.setSound(
|
|
1089
|
-
|
|
961
|
+
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE),
|
|
1090
962
|
AudioAttributes.Builder()
|
|
1091
963
|
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
|
1092
964
|
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
@@ -1103,9 +975,10 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
1103
975
|
}
|
|
1104
976
|
}
|
|
1105
977
|
|
|
1106
|
-
fun showIncomingCallUI(
|
|
978
|
+
private fun showIncomingCallUI(callId: String, callerName: String, callType: String) {
|
|
979
|
+
val context = requireContext()
|
|
1107
980
|
Log.d(TAG, "Showing incoming call UI for $callId, caller: $callerName, callType: $callType")
|
|
1108
|
-
createNotificationChannel(
|
|
981
|
+
createNotificationChannel()
|
|
1109
982
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
1110
983
|
|
|
1111
984
|
val answerIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
|
|
@@ -1139,20 +1012,20 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
1139
1012
|
|
|
1140
1013
|
val notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
1141
1014
|
val person = android.app.Person.Builder().setName(callerName).setImportant(true).build()
|
|
1142
|
-
|
|
1015
|
+
Notification.Builder(context, NOTIF_CHANNEL_ID)
|
|
1143
1016
|
.setSmallIcon(android.R.drawable.sym_call_incoming)
|
|
1144
|
-
.setStyle(
|
|
1017
|
+
.setStyle(Notification.CallStyle.forIncomingCall(person, declinePendingIntent, answerPendingIntent))
|
|
1145
1018
|
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
1146
1019
|
.setOngoing(true)
|
|
1147
1020
|
.setAutoCancel(false)
|
|
1148
1021
|
.build()
|
|
1149
1022
|
} else {
|
|
1150
|
-
|
|
1023
|
+
Notification.Builder(context, NOTIF_CHANNEL_ID)
|
|
1151
1024
|
.setSmallIcon(android.R.drawable.sym_call_incoming)
|
|
1152
1025
|
.setContentTitle("Incoming Call")
|
|
1153
1026
|
.setContentText(callerName)
|
|
1154
|
-
.setPriority(
|
|
1155
|
-
.setCategory(
|
|
1027
|
+
.setPriority(Notification.PRIORITY_HIGH)
|
|
1028
|
+
.setCategory(Notification.CATEGORY_CALL)
|
|
1156
1029
|
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
1157
1030
|
.addAction(android.R.drawable.sym_action_call, "Answer", answerPendingIntent)
|
|
1158
1031
|
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Decline", declinePendingIntent)
|
|
@@ -1164,20 +1037,56 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
1164
1037
|
notificationManager.notify(NOTIF_ID, notification)
|
|
1165
1038
|
|
|
1166
1039
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
|
1167
|
-
playRingtone(
|
|
1040
|
+
playRingtone()
|
|
1168
1041
|
}
|
|
1169
1042
|
|
|
1170
|
-
setInitialAudioRoute(
|
|
1043
|
+
setInitialAudioRoute(callType)
|
|
1171
1044
|
}
|
|
1172
1045
|
|
|
1173
|
-
fun cancelIncomingCallUI(
|
|
1046
|
+
private fun cancelIncomingCallUI() {
|
|
1047
|
+
val context = requireContext()
|
|
1174
1048
|
Log.d(TAG, "Cancelling incoming call UI.")
|
|
1175
1049
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
1176
1050
|
notificationManager.cancel(NOTIF_ID)
|
|
1177
1051
|
stopRingtone()
|
|
1178
1052
|
}
|
|
1179
1053
|
|
|
1180
|
-
|
|
1054
|
+
// --- Service Management ---
|
|
1055
|
+
private fun startForegroundService() {
|
|
1056
|
+
val context = requireContext()
|
|
1057
|
+
Log.d(TAG, "Starting CallForegroundService.")
|
|
1058
|
+
|
|
1059
|
+
val currentCall = activeCalls.values.find {
|
|
1060
|
+
it.state == CallState.ACTIVE || it.state == CallState.INCOMING ||
|
|
1061
|
+
it.state == CallState.DIALING || it.state == CallState.HELD
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
val intent = Intent(context, CallForegroundService::class.java)
|
|
1065
|
+
|
|
1066
|
+
if (currentCall != null) {
|
|
1067
|
+
intent.putExtra("callId", currentCall.callId)
|
|
1068
|
+
intent.putExtra("callType", currentCall.callType)
|
|
1069
|
+
intent.putExtra("displayName", currentCall.displayName)
|
|
1070
|
+
intent.putExtra("state", currentCall.state.name)
|
|
1071
|
+
Log.d(TAG, "Starting foreground service with call info: ${currentCall.callId}")
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1075
|
+
context.startForegroundService(intent)
|
|
1076
|
+
} else {
|
|
1077
|
+
context.startService(intent)
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
private fun stopForegroundService() {
|
|
1082
|
+
val context = requireContext()
|
|
1083
|
+
Log.d(TAG, "Stopping CallForegroundService.")
|
|
1084
|
+
val intent = Intent(context, CallForegroundService::class.java)
|
|
1085
|
+
context.stopService(intent)
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
private fun bringAppToForeground() {
|
|
1089
|
+
val context = requireContext()
|
|
1181
1090
|
val packageName = context.packageName
|
|
1182
1091
|
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
|
|
1183
1092
|
launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
@@ -1193,7 +1102,7 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
1193
1102
|
|
|
1194
1103
|
try {
|
|
1195
1104
|
context.startActivity(launchIntent)
|
|
1196
|
-
|
|
1105
|
+
Handler(Looper.getMainLooper()).postDelayed({
|
|
1197
1106
|
updateLockScreenBypass()
|
|
1198
1107
|
}, 100)
|
|
1199
1108
|
} catch (e: Exception) {
|
|
@@ -1202,9 +1111,10 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
1202
1111
|
}
|
|
1203
1112
|
|
|
1204
1113
|
// --- Phone Account Management ---
|
|
1205
|
-
private fun registerPhoneAccount(
|
|
1114
|
+
private fun registerPhoneAccount() {
|
|
1115
|
+
val context = requireContext()
|
|
1206
1116
|
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
1207
|
-
val phoneAccountHandle = getPhoneAccountHandle(
|
|
1117
|
+
val phoneAccountHandle = getPhoneAccountHandle()
|
|
1208
1118
|
|
|
1209
1119
|
if (telecomManager.getPhoneAccount(phoneAccountHandle) == null) {
|
|
1210
1120
|
val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "PingMe Call")
|
|
@@ -1224,7 +1134,8 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
1224
1134
|
}
|
|
1225
1135
|
}
|
|
1226
1136
|
|
|
1227
|
-
private fun getPhoneAccountHandle(
|
|
1137
|
+
private fun getPhoneAccountHandle(): PhoneAccountHandle {
|
|
1138
|
+
val context = requireContext()
|
|
1228
1139
|
return PhoneAccountHandle(
|
|
1229
1140
|
ComponentName(context, MyConnectionService::class.java),
|
|
1230
1141
|
PHONE_ACCOUNT_ID
|
|
@@ -1232,7 +1143,8 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
1232
1143
|
}
|
|
1233
1144
|
|
|
1234
1145
|
// --- Media Management ---
|
|
1235
|
-
fun playRingtone(
|
|
1146
|
+
private fun playRingtone() {
|
|
1147
|
+
val context = requireContext()
|
|
1236
1148
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
1237
1149
|
Log.d(TAG, "playRingtone: Android S+ detected, system will handle ringtone via Telecom.")
|
|
1238
1150
|
return
|
|
@@ -1240,8 +1152,8 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
1240
1152
|
|
|
1241
1153
|
try {
|
|
1242
1154
|
Log.d(TAG, "Playing ringtone (for Android < S).")
|
|
1243
|
-
val uri =
|
|
1244
|
-
ringtone =
|
|
1155
|
+
val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
1156
|
+
ringtone = RingtoneManager.getRingtone(context, uri)
|
|
1245
1157
|
ringtone?.audioAttributes = AudioAttributes.Builder()
|
|
1246
1158
|
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
|
1247
1159
|
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
@@ -1252,7 +1164,7 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
1252
1164
|
}
|
|
1253
1165
|
}
|
|
1254
1166
|
|
|
1255
|
-
fun stopRingtone() {
|
|
1167
|
+
private fun stopRingtone() {
|
|
1256
1168
|
try {
|
|
1257
1169
|
if (ringtone?.isPlaying == true) {
|
|
1258
1170
|
ringtone?.stop()
|
|
@@ -1265,14 +1177,15 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
1265
1177
|
}
|
|
1266
1178
|
|
|
1267
1179
|
private fun startRingback() {
|
|
1180
|
+
val context = requireContext()
|
|
1268
1181
|
if (ringbackPlayer?.isPlaying == true) {
|
|
1269
1182
|
Log.d(TAG, "Ringback tone already playing.")
|
|
1270
1183
|
return
|
|
1271
1184
|
}
|
|
1272
1185
|
|
|
1273
1186
|
try {
|
|
1274
|
-
val ringbackUri = Uri.parse("android.resource://${
|
|
1275
|
-
ringbackPlayer = MediaPlayer.create(
|
|
1187
|
+
val ringbackUri = Uri.parse("android.resource://${context.packageName}/raw/ringback_tone")
|
|
1188
|
+
ringbackPlayer = MediaPlayer.create(context, ringbackUri)
|
|
1276
1189
|
if (ringbackPlayer == null) {
|
|
1277
1190
|
Log.e(TAG, "Failed to create MediaPlayer for ringback. Check raw/ringback_tone.mp3 exists.")
|
|
1278
1191
|
return
|
|
@@ -1308,48 +1221,63 @@ object CallEngine : DefaultLifecycleObserver {
|
|
|
1308
1221
|
}
|
|
1309
1222
|
}
|
|
1310
1223
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1224
|
+
private fun updateForegroundNotification() {
|
|
1225
|
+
val context = requireContext()
|
|
1226
|
+
val activeCall = activeCalls.values.find { it.state == CallState.ACTIVE }
|
|
1227
|
+
val heldCall = activeCalls.values.find { it.state == CallState.HELD }
|
|
1228
|
+
|
|
1229
|
+
val callToShow = activeCall ?: heldCall
|
|
1230
|
+
callToShow?.let {
|
|
1231
|
+
val intent = Intent(context, CallForegroundService::class.java)
|
|
1232
|
+
intent.putExtra("UPDATE_NOTIFICATION", true)
|
|
1233
|
+
intent.putExtra("callId", it.callId)
|
|
1234
|
+
intent.putExtra("callType", it.callType)
|
|
1235
|
+
intent.putExtra("displayName", it.displayName)
|
|
1236
|
+
intent.putExtra("state", it.state.name)
|
|
1237
|
+
|
|
1238
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1239
|
+
context.startForegroundService(intent)
|
|
1240
|
+
} else {
|
|
1241
|
+
context.startService(intent)
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
private fun finalCleanup() {
|
|
1247
|
+
Log.d(TAG, "Performing final cleanup - no active calls remaining")
|
|
1248
|
+
stopForegroundService()
|
|
1249
|
+
keepScreenAwake(false)
|
|
1250
|
+
resetAudioMode()
|
|
1251
|
+
isSystemCallActive = false
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// --- Lifecycle Management ---
|
|
1255
|
+
fun onApplicationTerminate() {
|
|
1256
|
+
Log.d(TAG, "Application terminating - cleaning up all calls")
|
|
1314
1257
|
|
|
1315
|
-
// End all calls
|
|
1258
|
+
// End all calls properly
|
|
1316
1259
|
activeCalls.keys.toList().forEach { callId ->
|
|
1317
|
-
|
|
1260
|
+
val connection = telecomConnections[callId]
|
|
1261
|
+
connection?.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
|
1262
|
+
connection?.destroy()
|
|
1318
1263
|
}
|
|
1319
1264
|
|
|
1320
|
-
// Clear all
|
|
1265
|
+
// Clear all state
|
|
1321
1266
|
activeCalls.clear()
|
|
1322
1267
|
telecomConnections.clear()
|
|
1323
1268
|
callMetadata.clear()
|
|
1324
|
-
|
|
1325
|
-
cachedEvents.clear()
|
|
1326
|
-
|
|
1327
|
-
// Stop media
|
|
1328
|
-
stopRingtone()
|
|
1329
|
-
stopRingback()
|
|
1269
|
+
currentCallId = null
|
|
1330
1270
|
|
|
1331
1271
|
// Release resources
|
|
1332
|
-
|
|
1333
|
-
if (it.isHeld) it.release()
|
|
1334
|
-
}
|
|
1335
|
-
wakeLock = null
|
|
1336
|
-
|
|
1337
|
-
// Audio cleanup
|
|
1338
|
-
abandonAudioFocus()
|
|
1339
|
-
appContext?.let {
|
|
1340
|
-
resetAudioMode(it)
|
|
1341
|
-
unregisterAudioDeviceCallback()
|
|
1342
|
-
}
|
|
1272
|
+
finalCleanup()
|
|
1343
1273
|
|
|
1344
|
-
//
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
lockScreenBypassActive = false
|
|
1349
|
-
hasAudioFocus = false
|
|
1350
|
-
isSystemCallActive = false
|
|
1351
|
-
isServiceRunning = false
|
|
1274
|
+
// Clear callbacks
|
|
1275
|
+
lockScreenBypassCallbacks.clear()
|
|
1276
|
+
eventHandler = null
|
|
1277
|
+
cachedEvents.clear()
|
|
1352
1278
|
|
|
1353
|
-
|
|
1279
|
+
// Reset initialization
|
|
1280
|
+
isInitialized.set(false)
|
|
1281
|
+
appContext = null
|
|
1354
1282
|
}
|
|
1355
1283
|
}
|