@qusaieilouti99/call-manager 0.1.112 → 0.1.113
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.
|
@@ -28,7 +28,6 @@ import android.widget.LinearLayout
|
|
|
28
28
|
import android.widget.TextView
|
|
29
29
|
import java.net.HttpURLConnection
|
|
30
30
|
import java.net.URL
|
|
31
|
-
import android.view.KeyEvent
|
|
32
31
|
|
|
33
32
|
class CallActivity : Activity(), CallEngine.CallEndListener {
|
|
34
33
|
|
|
@@ -49,17 +48,6 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
|
|
|
49
48
|
finishCallActivity()
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
|
53
|
-
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
|
|
54
|
-
keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
|
|
55
|
-
keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) {
|
|
56
|
-
// silence the ring immediately
|
|
57
|
-
CallEngine.stopRingtone()
|
|
58
|
-
return true
|
|
59
|
-
}
|
|
60
|
-
return super.onKeyDown(keyCode, event)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
51
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
64
52
|
super.onCreate(savedInstanceState)
|
|
65
53
|
val isSamsungBypass = intent.getBooleanExtra(
|
|
@@ -42,6 +42,8 @@ import android.os.Vibrator
|
|
|
42
42
|
import android.os.VibrationEffect
|
|
43
43
|
import android.content.BroadcastReceiver
|
|
44
44
|
import android.content.IntentFilter
|
|
45
|
+
import android.database.ContentObserver
|
|
46
|
+
import android.provider.Settings
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
49
|
* Core call‐management engine. Manages self-managed telecom calls,
|
|
@@ -53,6 +55,11 @@ object CallEngine {
|
|
|
53
55
|
private const val NOTIF_CHANNEL_ID = "incoming_call_channel"
|
|
54
56
|
private const val NOTIF_ID = 2001
|
|
55
57
|
|
|
58
|
+
// Audio routing timing constants
|
|
59
|
+
private const val AUDIO_ROUTE_DELAY_AFTER_ANSWER = 500L
|
|
60
|
+
private const val AUDIO_ROUTE_RETRY_DELAY = 200L
|
|
61
|
+
private const val MAX_AUDIO_ROUTE_RETRIES = 3
|
|
62
|
+
|
|
56
63
|
// --- NEW: Call-end listener API ---
|
|
57
64
|
/**
|
|
58
65
|
* Implement this in your UI (CallActivity). Will be called
|
|
@@ -82,17 +89,24 @@ object CallEngine {
|
|
|
82
89
|
private val isInitialized = AtomicBoolean(false)
|
|
83
90
|
private val initializationLock = Any()
|
|
84
91
|
|
|
85
|
-
//
|
|
92
|
+
// Audio & Media Management
|
|
86
93
|
private var ringtone: android.media.Ringtone? = null
|
|
87
94
|
private var ringbackPlayer: MediaPlayer? = null
|
|
88
95
|
|
|
89
|
-
//
|
|
96
|
+
// Volume monitoring
|
|
90
97
|
private var vibrator: Vibrator? = null
|
|
91
|
-
private var
|
|
98
|
+
private var volumeContentObserver: ContentObserver? = null
|
|
99
|
+
private var lastRingVolumeLevel = -1
|
|
100
|
+
private var isMonitoringVolume = false
|
|
92
101
|
|
|
93
102
|
private var audioManager: AudioManager? = null
|
|
94
103
|
private var wakeLock: PowerManager.WakeLock? = null
|
|
95
104
|
|
|
105
|
+
// Audio routing state
|
|
106
|
+
private var pendingAudioRoute: String? = null
|
|
107
|
+
private var audioRouteRetryCount = 0
|
|
108
|
+
private val audioRouteHandler = Handler(Looper.getMainLooper())
|
|
109
|
+
|
|
96
110
|
// Call State Management
|
|
97
111
|
private val activeCalls = ConcurrentHashMap<String, CallInfo>()
|
|
98
112
|
private val telecomConnections = ConcurrentHashMap<String, Connection>()
|
|
@@ -122,6 +136,7 @@ object CallEngine {
|
|
|
122
136
|
if (isInitialized.compareAndSet(false, true)) {
|
|
123
137
|
appContext = context.applicationContext
|
|
124
138
|
audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
|
139
|
+
setupVolumeMonitoring()
|
|
125
140
|
Log.d(TAG, "CallEngine initialized successfully")
|
|
126
141
|
|
|
127
142
|
if (isCallActive()) {
|
|
@@ -139,6 +154,64 @@ object CallEngine {
|
|
|
139
154
|
)
|
|
140
155
|
}
|
|
141
156
|
|
|
157
|
+
// --- VOLUME MONITORING FOR RINGTONE SILENCING ---
|
|
158
|
+
private fun setupVolumeMonitoring() {
|
|
159
|
+
val context = requireContext()
|
|
160
|
+
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
161
|
+
|
|
162
|
+
// Get initial ring volume
|
|
163
|
+
lastRingVolumeLevel = audioManager?.getStreamVolume(AudioManager.STREAM_RING) ?: 0
|
|
164
|
+
|
|
165
|
+
// Create content observer for ring volume changes
|
|
166
|
+
volumeContentObserver = object : ContentObserver(mainHandler) {
|
|
167
|
+
override fun onChange(selfChange: Boolean) {
|
|
168
|
+
handleVolumeChange()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private fun startVolumeMonitoring() {
|
|
174
|
+
if (!isMonitoringVolume) {
|
|
175
|
+
isMonitoringVolume = true
|
|
176
|
+
val context = requireContext()
|
|
177
|
+
|
|
178
|
+
// Monitor ring volume changes
|
|
179
|
+
context.contentResolver.registerContentObserver(
|
|
180
|
+
Settings.System.getUriFor(Settings.System.VOLUME_RING),
|
|
181
|
+
false,
|
|
182
|
+
volumeContentObserver!!
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
Log.d(TAG, "Started volume monitoring for ringtone silencing")
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private fun stopVolumeMonitoring() {
|
|
190
|
+
if (isMonitoringVolume) {
|
|
191
|
+
isMonitoringVolume = false
|
|
192
|
+
val context = requireContext()
|
|
193
|
+
volumeContentObserver?.let {
|
|
194
|
+
context.contentResolver.unregisterContentObserver(it)
|
|
195
|
+
}
|
|
196
|
+
Log.d(TAG, "Stopped volume monitoring")
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private fun handleVolumeChange() {
|
|
201
|
+
if (!isMonitoringVolume) return
|
|
202
|
+
|
|
203
|
+
val currentRingVolume = audioManager?.getStreamVolume(AudioManager.STREAM_RING) ?: 0
|
|
204
|
+
|
|
205
|
+
// Check if ring volume changed
|
|
206
|
+
if (currentRingVolume != lastRingVolumeLevel) {
|
|
207
|
+
Log.d(TAG, "Ring volume changed from $lastRingVolumeLevel to $currentRingVolume - silencing ringtone")
|
|
208
|
+
lastRingVolumeLevel = currentRingVolume
|
|
209
|
+
|
|
210
|
+
// Stop ringtone when volume is changed
|
|
211
|
+
stopRingtone()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
142
215
|
// --- Event System ---
|
|
143
216
|
fun setEventHandler(handler: ((CallEventType, String) -> Unit)?) {
|
|
144
217
|
Log.d(TAG, "setEventHandler called. Handler present: ${handler != null}")
|
|
@@ -393,7 +466,12 @@ object CallEngine {
|
|
|
393
466
|
bringAppToForeground()
|
|
394
467
|
startForegroundService()
|
|
395
468
|
keepScreenAwake(true)
|
|
396
|
-
|
|
469
|
+
|
|
470
|
+
// Delay initial audio route to avoid conflicts
|
|
471
|
+
mainHandler.postDelayed({
|
|
472
|
+
setInitialAudioRoute(callType)
|
|
473
|
+
}, AUDIO_ROUTE_DELAY_AFTER_ANSWER)
|
|
474
|
+
|
|
397
475
|
updateLockScreenBypass()
|
|
398
476
|
|
|
399
477
|
emitOutgoingCallAnsweredWithMetadata(callId)
|
|
@@ -418,10 +496,9 @@ object CallEngine {
|
|
|
418
496
|
return
|
|
419
497
|
}
|
|
420
498
|
|
|
421
|
-
|
|
422
499
|
activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
|
|
423
500
|
currentCallId = callId
|
|
424
|
-
Log.d(TAG, "Call $callId set to ACTIVE state
|
|
501
|
+
Log.d(TAG, "Call $callId set to ACTIVE state")
|
|
425
502
|
|
|
426
503
|
stopRingtone()
|
|
427
504
|
stopRingback()
|
|
@@ -440,8 +517,10 @@ object CallEngine {
|
|
|
440
517
|
keepScreenAwake(true)
|
|
441
518
|
updateLockScreenBypass()
|
|
442
519
|
|
|
443
|
-
//
|
|
444
|
-
|
|
520
|
+
// FIXED: Delay audio routing to avoid conflicts with system
|
|
521
|
+
mainHandler.postDelayed({
|
|
522
|
+
setInitialAudioRoute(callInfo.callType)
|
|
523
|
+
}, AUDIO_ROUTE_DELAY_AFTER_ANSWER)
|
|
445
524
|
|
|
446
525
|
if (isLocalAnswer) {
|
|
447
526
|
emitCallAnsweredWithMetadata(callId)
|
|
@@ -728,43 +807,59 @@ object CallEngine {
|
|
|
728
807
|
return AudioRoutesInfo(devices.toTypedArray(), current)
|
|
729
808
|
}
|
|
730
809
|
|
|
810
|
+
// FIXED: Improved audio route handling with retries and proper timing
|
|
731
811
|
fun setAudioRoute(route: String) {
|
|
732
|
-
|
|
812
|
+
Log.d(TAG, "setAudioRoute called: $route")
|
|
813
|
+
|
|
814
|
+
// Clear any pending route changes
|
|
815
|
+
audioRouteHandler.removeCallbacksAndMessages(null)
|
|
816
|
+
audioRouteRetryCount = 0
|
|
817
|
+
|
|
818
|
+
// Store the pending route for retry logic
|
|
819
|
+
pendingAudioRoute = route
|
|
820
|
+
|
|
821
|
+
// Execute immediately, then set up retry mechanism
|
|
822
|
+
performAudioRouteChange(route)
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
private fun performAudioRouteChange(route: String) {
|
|
733
826
|
val ctx = requireContext()
|
|
734
827
|
if (audioManager == null) {
|
|
735
828
|
audioManager = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
736
829
|
}
|
|
737
830
|
val am = audioManager!!
|
|
738
|
-
|
|
831
|
+
|
|
832
|
+
// Ensure proper audio mode first
|
|
833
|
+
if (am.mode != AudioManager.MODE_IN_COMMUNICATION) {
|
|
834
|
+
setAudioMode()
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
Log.d(TAG, "Performing audio route change to: $route")
|
|
739
838
|
|
|
740
839
|
when (route) {
|
|
741
840
|
"Speaker" -> {
|
|
742
|
-
//
|
|
841
|
+
// Disable Bluetooth SCO first if active
|
|
743
842
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
|
|
744
843
|
am.stopBluetoothSco()
|
|
745
844
|
am.isBluetoothScoOn = false
|
|
746
845
|
}
|
|
747
|
-
// route to speaker
|
|
748
846
|
am.isSpeakerphoneOn = true
|
|
749
847
|
Log.d(TAG, "Audio routed to SPEAKER")
|
|
750
848
|
}
|
|
751
849
|
|
|
752
850
|
"Earpiece" -> {
|
|
753
|
-
//
|
|
851
|
+
// Disable Bluetooth SCO if active
|
|
754
852
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
|
|
755
853
|
am.stopBluetoothSco()
|
|
756
854
|
am.isBluetoothScoOn = false
|
|
757
855
|
}
|
|
758
|
-
// route to earpiece
|
|
759
856
|
am.isSpeakerphoneOn = false
|
|
760
857
|
Log.d(TAG, "Audio routed to EARPIECE")
|
|
761
858
|
}
|
|
762
859
|
|
|
763
860
|
"Bluetooth" -> {
|
|
764
|
-
// disable speaker
|
|
765
861
|
am.isSpeakerphoneOn = false
|
|
766
862
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
767
|
-
// start SCO for Bluetooth
|
|
768
863
|
am.startBluetoothSco()
|
|
769
864
|
am.isBluetoothScoOn = true
|
|
770
865
|
Log.d(TAG, "Audio routed to BLUETOOTH (SCO started)")
|
|
@@ -774,7 +869,6 @@ object CallEngine {
|
|
|
774
869
|
}
|
|
775
870
|
|
|
776
871
|
"Headset" -> {
|
|
777
|
-
// wired headset is automatic when plugged in, just turn off speaker/SCO
|
|
778
872
|
am.isSpeakerphoneOn = false
|
|
779
873
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
|
|
780
874
|
am.stopBluetoothSco()
|
|
@@ -785,13 +879,41 @@ object CallEngine {
|
|
|
785
879
|
|
|
786
880
|
else -> {
|
|
787
881
|
Log.w(TAG, "Unknown audio route: $route")
|
|
882
|
+
return
|
|
788
883
|
}
|
|
789
884
|
}
|
|
790
885
|
|
|
791
|
-
//
|
|
886
|
+
// Verify the route was applied and retry if necessary
|
|
887
|
+
audioRouteHandler.postDelayed({
|
|
888
|
+
verifyAudioRoute(route)
|
|
889
|
+
}, AUDIO_ROUTE_RETRY_DELAY)
|
|
890
|
+
|
|
792
891
|
emitAudioRouteChanged()
|
|
793
892
|
}
|
|
794
893
|
|
|
894
|
+
private fun verifyAudioRoute(expectedRoute: String) {
|
|
895
|
+
val currentRoute = getCurrentAudioRoute()
|
|
896
|
+
|
|
897
|
+
if (currentRoute != expectedRoute && audioRouteRetryCount < MAX_AUDIO_ROUTE_RETRIES) {
|
|
898
|
+
audioRouteRetryCount++
|
|
899
|
+
Log.d(TAG, "Audio route verification failed. Expected: $expectedRoute, Current: $currentRoute. Retry: $audioRouteRetryCount")
|
|
900
|
+
|
|
901
|
+
// Retry the audio route change
|
|
902
|
+
audioRouteHandler.postDelayed({
|
|
903
|
+
performAudioRouteChange(expectedRoute)
|
|
904
|
+
}, AUDIO_ROUTE_RETRY_DELAY)
|
|
905
|
+
} else {
|
|
906
|
+
if (currentRoute == expectedRoute) {
|
|
907
|
+
Log.d(TAG, "Audio route successfully verified: $currentRoute")
|
|
908
|
+
} else {
|
|
909
|
+
Log.w(TAG, "Audio route change failed after $MAX_AUDIO_ROUTE_RETRIES retries. Expected: $expectedRoute, Current: $currentRoute")
|
|
910
|
+
}
|
|
911
|
+
// Clear pending route
|
|
912
|
+
pendingAudioRoute = null
|
|
913
|
+
audioRouteRetryCount = 0
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
795
917
|
private fun getCurrentAudioRoute(): String {
|
|
796
918
|
return when {
|
|
797
919
|
audioManager?.isBluetoothScoOn == true -> "Bluetooth"
|
|
@@ -920,7 +1042,6 @@ object CallEngine {
|
|
|
920
1042
|
}
|
|
921
1043
|
|
|
922
1044
|
// --- Notification Management ---
|
|
923
|
-
// **SAMSUNG FIX**: Enhanced notification channel creation
|
|
924
1045
|
private fun createNotificationChannel() {
|
|
925
1046
|
val context = requireContext()
|
|
926
1047
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
@@ -934,7 +1055,6 @@ object CallEngine {
|
|
|
934
1055
|
channel.lightColor = Color.GREEN
|
|
935
1056
|
channel.enableVibration(true)
|
|
936
1057
|
|
|
937
|
-
// **SAMSUNG FIX**: Set bypass DND and lock screen visibility
|
|
938
1058
|
channel.setBypassDnd(true)
|
|
939
1059
|
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
940
1060
|
|
|
@@ -960,7 +1080,6 @@ object CallEngine {
|
|
|
960
1080
|
val context = requireContext()
|
|
961
1081
|
Log.d(TAG, "Showing incoming call UI for $callId")
|
|
962
1082
|
|
|
963
|
-
// **SIMPLE LOGIC**: If locked -> overlay, if unlocked -> notification
|
|
964
1083
|
if (isDeviceLocked(context)) {
|
|
965
1084
|
Log.d(TAG, "Device is locked - using overlay approach")
|
|
966
1085
|
showCallActivityOverlay(context, callId, callerName, callType, callerPicUrl)
|
|
@@ -972,9 +1091,6 @@ object CallEngine {
|
|
|
972
1091
|
playRingtone()
|
|
973
1092
|
}
|
|
974
1093
|
|
|
975
|
-
/**
|
|
976
|
-
* Simple check if device is locked
|
|
977
|
-
*/
|
|
978
1094
|
private fun isDeviceLocked(context: Context): Boolean {
|
|
979
1095
|
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
|
980
1096
|
val isLocked = keyguardManager.isKeyguardLocked
|
|
@@ -982,9 +1098,6 @@ object CallEngine {
|
|
|
982
1098
|
return isLocked
|
|
983
1099
|
}
|
|
984
1100
|
|
|
985
|
-
/**
|
|
986
|
-
* Shows the call activity directly as overlay (when device is locked)
|
|
987
|
-
*/
|
|
988
1101
|
private fun showCallActivityOverlay(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
|
|
989
1102
|
val overlayIntent = Intent(context, CallActivity::class.java).apply {
|
|
990
1103
|
addFlags(
|
|
@@ -1000,11 +1113,10 @@ object CallEngine {
|
|
|
1000
1113
|
putExtra("callerAvatar", callerPicUrl)
|
|
1001
1114
|
}
|
|
1002
1115
|
|
|
1003
|
-
putExtra("LOCK_SCREEN_MODE", true)
|
|
1116
|
+
putExtra("LOCK_SCREEN_MODE", true)
|
|
1004
1117
|
}
|
|
1005
1118
|
|
|
1006
1119
|
try {
|
|
1007
|
-
// Wake the screen when device is locked
|
|
1008
1120
|
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
|
1009
1121
|
val wakeLock = powerManager.newWakeLock(
|
|
1010
1122
|
PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
|
@@ -1012,22 +1124,15 @@ object CallEngine {
|
|
|
1012
1124
|
)
|
|
1013
1125
|
wakeLock.acquire(5000)
|
|
1014
1126
|
|
|
1015
|
-
// Launch the activity overlay
|
|
1016
1127
|
context.startActivity(overlayIntent)
|
|
1017
1128
|
Log.d(TAG, "Successfully launched CallActivity overlay for locked device")
|
|
1018
1129
|
|
|
1019
|
-
// **NO NOTIFICATION** when using overlay
|
|
1020
|
-
|
|
1021
1130
|
} catch (e: Exception) {
|
|
1022
1131
|
Log.e(TAG, "Overlay failed, falling back to notification: ${e.message}")
|
|
1023
|
-
// If overlay fails, fall back to notification
|
|
1024
1132
|
showStandardNotification(context, callId, callerName, callType, callerPicUrl)
|
|
1025
1133
|
}
|
|
1026
1134
|
}
|
|
1027
1135
|
|
|
1028
|
-
/**
|
|
1029
|
-
* Shows standard notification with full-screen intent (when device is unlocked)
|
|
1030
|
-
*/
|
|
1031
1136
|
private fun showStandardNotification(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
|
|
1032
1137
|
createNotificationChannel()
|
|
1033
1138
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
@@ -1040,7 +1145,6 @@ object CallEngine {
|
|
|
1040
1145
|
if (callerPicUrl != null) {
|
|
1041
1146
|
putExtra("callerAvatar", callerPicUrl)
|
|
1042
1147
|
}
|
|
1043
|
-
// No lock screen flag - standard notification mode
|
|
1044
1148
|
}
|
|
1045
1149
|
|
|
1046
1150
|
val fullScreenPendingIntent = PendingIntent.getActivity(
|
|
@@ -1239,17 +1343,23 @@ object CallEngine {
|
|
|
1239
1343
|
}
|
|
1240
1344
|
|
|
1241
1345
|
// --- Media Management ---
|
|
1346
|
+
// FIXED: Improved ringtone management with proper volume monitoring
|
|
1242
1347
|
private fun playRingtone() {
|
|
1243
1348
|
val context = requireContext()
|
|
1244
1349
|
|
|
1245
1350
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
1246
|
-
|
|
1351
|
+
|
|
1352
|
+
// Set up proper audio mode for ringtone
|
|
1247
1353
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
1248
1354
|
audioManager?.mode = AudioManager.MODE_RINGTONE
|
|
1249
1355
|
}
|
|
1250
1356
|
audioManager?.isSpeakerphoneOn = true
|
|
1251
1357
|
|
|
1252
|
-
//
|
|
1358
|
+
// Start volume monitoring for silencing
|
|
1359
|
+
startVolumeMonitoring()
|
|
1360
|
+
lastRingVolumeLevel = audioManager?.getStreamVolume(AudioManager.STREAM_RING) ?: 0
|
|
1361
|
+
|
|
1362
|
+
// Start repeating vibration
|
|
1253
1363
|
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
|
1254
1364
|
vibrator?.let { v ->
|
|
1255
1365
|
val pattern = longArrayOf(0L, 500L, 500L)
|
|
@@ -1261,33 +1371,15 @@ object CallEngine {
|
|
|
1261
1371
|
}
|
|
1262
1372
|
}
|
|
1263
1373
|
|
|
1264
|
-
//
|
|
1374
|
+
// Play the ringtone
|
|
1265
1375
|
try {
|
|
1266
1376
|
val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
1267
1377
|
ringtone = RingtoneManager.getRingtone(context, uri)
|
|
1268
1378
|
ringtone?.play()
|
|
1379
|
+
Log.d(TAG, "Ringtone started playing")
|
|
1269
1380
|
} catch (e: Exception) {
|
|
1270
1381
|
Log.e(TAG, "Failed to play ringtone", e)
|
|
1271
1382
|
}
|
|
1272
|
-
|
|
1273
|
-
// 3) register a receiver to catch volume‐key presses on the RING stream
|
|
1274
|
-
if (volumeReceiver == null) {
|
|
1275
|
-
val filter = IntentFilter("android.media.VOLUME_CHANGED_ACTION")
|
|
1276
|
-
volumeReceiver = object : BroadcastReceiver() {
|
|
1277
|
-
override fun onReceive(ctx: Context, intent: Intent) {
|
|
1278
|
-
if (intent.action == "android.media.VOLUME_CHANGED_ACTION") {
|
|
1279
|
-
val streamType = intent.getIntExtra(
|
|
1280
|
-
"android.media.EXTRA_VOLUME_STREAM_TYPE", -1
|
|
1281
|
-
)
|
|
1282
|
-
if (streamType == AudioManager.STREAM_RING) {
|
|
1283
|
-
Log.d(TAG, "Volume key pressed on RING → silencing ringtone/vibrate")
|
|
1284
|
-
stopRingtone()
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
context.registerReceiver(volumeReceiver, filter)
|
|
1290
|
-
}
|
|
1291
1383
|
}
|
|
1292
1384
|
|
|
1293
1385
|
fun stopRingtone() {
|
|
@@ -1299,15 +1391,12 @@ object CallEngine {
|
|
|
1299
1391
|
}
|
|
1300
1392
|
ringtone = null
|
|
1301
1393
|
|
|
1302
|
-
//
|
|
1394
|
+
// Cancel vibration
|
|
1303
1395
|
vibrator?.cancel()
|
|
1304
1396
|
vibrator = null
|
|
1305
1397
|
|
|
1306
|
-
//
|
|
1307
|
-
|
|
1308
|
-
requireContext().unregisterReceiver(it)
|
|
1309
|
-
volumeReceiver = null
|
|
1310
|
-
}
|
|
1398
|
+
// Stop volume monitoring
|
|
1399
|
+
stopVolumeMonitoring()
|
|
1311
1400
|
}
|
|
1312
1401
|
|
|
1313
1402
|
private fun startRingback() {
|
|
@@ -1344,6 +1433,10 @@ object CallEngine {
|
|
|
1344
1433
|
stopForegroundService()
|
|
1345
1434
|
keepScreenAwake(false)
|
|
1346
1435
|
resetAudioMode()
|
|
1436
|
+
stopVolumeMonitoring()
|
|
1437
|
+
audioRouteHandler.removeCallbacksAndMessages(null)
|
|
1438
|
+
pendingAudioRoute = null
|
|
1439
|
+
audioRouteRetryCount = 0
|
|
1347
1440
|
}
|
|
1348
1441
|
|
|
1349
1442
|
// --- Lifecycle Management ---
|