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