@qusaieilouti99/call-manager 0.1.111 → 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,10 +517,10 @@ object CallEngine {
|
|
|
440
517
|
keepScreenAwake(true)
|
|
441
518
|
updateLockScreenBypass()
|
|
442
519
|
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
520
|
+
// FIXED: Delay audio routing to avoid conflicts with system
|
|
521
|
+
mainHandler.postDelayed({
|
|
522
|
+
setInitialAudioRoute(callInfo.callType)
|
|
523
|
+
}, AUDIO_ROUTE_DELAY_AFTER_ANSWER)
|
|
447
524
|
|
|
448
525
|
if (isLocalAnswer) {
|
|
449
526
|
emitCallAnsweredWithMetadata(callId)
|
|
@@ -730,42 +807,59 @@ object CallEngine {
|
|
|
730
807
|
return AudioRoutesInfo(devices.toTypedArray(), current)
|
|
731
808
|
}
|
|
732
809
|
|
|
810
|
+
// FIXED: Improved audio route handling with retries and proper timing
|
|
733
811
|
fun setAudioRoute(route: String) {
|
|
734
|
-
|
|
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) {
|
|
735
826
|
val ctx = requireContext()
|
|
736
827
|
if (audioManager == null) {
|
|
737
828
|
audioManager = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
738
829
|
}
|
|
739
830
|
val am = audioManager!!
|
|
740
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")
|
|
838
|
+
|
|
741
839
|
when (route) {
|
|
742
840
|
"Speaker" -> {
|
|
743
|
-
//
|
|
841
|
+
// Disable Bluetooth SCO first if active
|
|
744
842
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
|
|
745
843
|
am.stopBluetoothSco()
|
|
746
844
|
am.isBluetoothScoOn = false
|
|
747
845
|
}
|
|
748
|
-
// route to speaker
|
|
749
846
|
am.isSpeakerphoneOn = true
|
|
750
847
|
Log.d(TAG, "Audio routed to SPEAKER")
|
|
751
848
|
}
|
|
752
849
|
|
|
753
850
|
"Earpiece" -> {
|
|
754
|
-
//
|
|
851
|
+
// Disable Bluetooth SCO if active
|
|
755
852
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
|
|
756
853
|
am.stopBluetoothSco()
|
|
757
854
|
am.isBluetoothScoOn = false
|
|
758
855
|
}
|
|
759
|
-
// route to earpiece
|
|
760
856
|
am.isSpeakerphoneOn = false
|
|
761
857
|
Log.d(TAG, "Audio routed to EARPIECE")
|
|
762
858
|
}
|
|
763
859
|
|
|
764
860
|
"Bluetooth" -> {
|
|
765
|
-
// disable speaker
|
|
766
861
|
am.isSpeakerphoneOn = false
|
|
767
862
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
768
|
-
// start SCO for Bluetooth
|
|
769
863
|
am.startBluetoothSco()
|
|
770
864
|
am.isBluetoothScoOn = true
|
|
771
865
|
Log.d(TAG, "Audio routed to BLUETOOTH (SCO started)")
|
|
@@ -775,7 +869,6 @@ object CallEngine {
|
|
|
775
869
|
}
|
|
776
870
|
|
|
777
871
|
"Headset" -> {
|
|
778
|
-
// wired headset is automatic when plugged in, just turn off speaker/SCO
|
|
779
872
|
am.isSpeakerphoneOn = false
|
|
780
873
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
|
|
781
874
|
am.stopBluetoothSco()
|
|
@@ -786,13 +879,41 @@ object CallEngine {
|
|
|
786
879
|
|
|
787
880
|
else -> {
|
|
788
881
|
Log.w(TAG, "Unknown audio route: $route")
|
|
882
|
+
return
|
|
789
883
|
}
|
|
790
884
|
}
|
|
791
885
|
|
|
792
|
-
//
|
|
886
|
+
// Verify the route was applied and retry if necessary
|
|
887
|
+
audioRouteHandler.postDelayed({
|
|
888
|
+
verifyAudioRoute(route)
|
|
889
|
+
}, AUDIO_ROUTE_RETRY_DELAY)
|
|
890
|
+
|
|
793
891
|
emitAudioRouteChanged()
|
|
794
892
|
}
|
|
795
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
|
+
|
|
796
917
|
private fun getCurrentAudioRoute(): String {
|
|
797
918
|
return when {
|
|
798
919
|
audioManager?.isBluetoothScoOn == true -> "Bluetooth"
|
|
@@ -921,7 +1042,6 @@ object CallEngine {
|
|
|
921
1042
|
}
|
|
922
1043
|
|
|
923
1044
|
// --- Notification Management ---
|
|
924
|
-
// **SAMSUNG FIX**: Enhanced notification channel creation
|
|
925
1045
|
private fun createNotificationChannel() {
|
|
926
1046
|
val context = requireContext()
|
|
927
1047
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
@@ -935,7 +1055,6 @@ object CallEngine {
|
|
|
935
1055
|
channel.lightColor = Color.GREEN
|
|
936
1056
|
channel.enableVibration(true)
|
|
937
1057
|
|
|
938
|
-
// **SAMSUNG FIX**: Set bypass DND and lock screen visibility
|
|
939
1058
|
channel.setBypassDnd(true)
|
|
940
1059
|
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
941
1060
|
|
|
@@ -961,7 +1080,6 @@ object CallEngine {
|
|
|
961
1080
|
val context = requireContext()
|
|
962
1081
|
Log.d(TAG, "Showing incoming call UI for $callId")
|
|
963
1082
|
|
|
964
|
-
// **SIMPLE LOGIC**: If locked -> overlay, if unlocked -> notification
|
|
965
1083
|
if (isDeviceLocked(context)) {
|
|
966
1084
|
Log.d(TAG, "Device is locked - using overlay approach")
|
|
967
1085
|
showCallActivityOverlay(context, callId, callerName, callType, callerPicUrl)
|
|
@@ -973,9 +1091,6 @@ object CallEngine {
|
|
|
973
1091
|
playRingtone()
|
|
974
1092
|
}
|
|
975
1093
|
|
|
976
|
-
/**
|
|
977
|
-
* Simple check if device is locked
|
|
978
|
-
*/
|
|
979
1094
|
private fun isDeviceLocked(context: Context): Boolean {
|
|
980
1095
|
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
|
981
1096
|
val isLocked = keyguardManager.isKeyguardLocked
|
|
@@ -983,9 +1098,6 @@ object CallEngine {
|
|
|
983
1098
|
return isLocked
|
|
984
1099
|
}
|
|
985
1100
|
|
|
986
|
-
/**
|
|
987
|
-
* Shows the call activity directly as overlay (when device is locked)
|
|
988
|
-
*/
|
|
989
1101
|
private fun showCallActivityOverlay(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
|
|
990
1102
|
val overlayIntent = Intent(context, CallActivity::class.java).apply {
|
|
991
1103
|
addFlags(
|
|
@@ -1001,11 +1113,10 @@ object CallEngine {
|
|
|
1001
1113
|
putExtra("callerAvatar", callerPicUrl)
|
|
1002
1114
|
}
|
|
1003
1115
|
|
|
1004
|
-
putExtra("LOCK_SCREEN_MODE", true)
|
|
1116
|
+
putExtra("LOCK_SCREEN_MODE", true)
|
|
1005
1117
|
}
|
|
1006
1118
|
|
|
1007
1119
|
try {
|
|
1008
|
-
// Wake the screen when device is locked
|
|
1009
1120
|
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
|
1010
1121
|
val wakeLock = powerManager.newWakeLock(
|
|
1011
1122
|
PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
|
@@ -1013,22 +1124,15 @@ object CallEngine {
|
|
|
1013
1124
|
)
|
|
1014
1125
|
wakeLock.acquire(5000)
|
|
1015
1126
|
|
|
1016
|
-
// Launch the activity overlay
|
|
1017
1127
|
context.startActivity(overlayIntent)
|
|
1018
1128
|
Log.d(TAG, "Successfully launched CallActivity overlay for locked device")
|
|
1019
1129
|
|
|
1020
|
-
// **NO NOTIFICATION** when using overlay
|
|
1021
|
-
|
|
1022
1130
|
} catch (e: Exception) {
|
|
1023
1131
|
Log.e(TAG, "Overlay failed, falling back to notification: ${e.message}")
|
|
1024
|
-
// If overlay fails, fall back to notification
|
|
1025
1132
|
showStandardNotification(context, callId, callerName, callType, callerPicUrl)
|
|
1026
1133
|
}
|
|
1027
1134
|
}
|
|
1028
1135
|
|
|
1029
|
-
/**
|
|
1030
|
-
* Shows standard notification with full-screen intent (when device is unlocked)
|
|
1031
|
-
*/
|
|
1032
1136
|
private fun showStandardNotification(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
|
|
1033
1137
|
createNotificationChannel()
|
|
1034
1138
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
@@ -1041,7 +1145,6 @@ object CallEngine {
|
|
|
1041
1145
|
if (callerPicUrl != null) {
|
|
1042
1146
|
putExtra("callerAvatar", callerPicUrl)
|
|
1043
1147
|
}
|
|
1044
|
-
// No lock screen flag - standard notification mode
|
|
1045
1148
|
}
|
|
1046
1149
|
|
|
1047
1150
|
val fullScreenPendingIntent = PendingIntent.getActivity(
|
|
@@ -1240,17 +1343,23 @@ object CallEngine {
|
|
|
1240
1343
|
}
|
|
1241
1344
|
|
|
1242
1345
|
// --- Media Management ---
|
|
1346
|
+
// FIXED: Improved ringtone management with proper volume monitoring
|
|
1243
1347
|
private fun playRingtone() {
|
|
1244
1348
|
val context = requireContext()
|
|
1245
1349
|
|
|
1246
1350
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
1247
|
-
|
|
1351
|
+
|
|
1352
|
+
// Set up proper audio mode for ringtone
|
|
1248
1353
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
1249
1354
|
audioManager?.mode = AudioManager.MODE_RINGTONE
|
|
1250
1355
|
}
|
|
1251
1356
|
audioManager?.isSpeakerphoneOn = true
|
|
1252
1357
|
|
|
1253
|
-
//
|
|
1358
|
+
// Start volume monitoring for silencing
|
|
1359
|
+
startVolumeMonitoring()
|
|
1360
|
+
lastRingVolumeLevel = audioManager?.getStreamVolume(AudioManager.STREAM_RING) ?: 0
|
|
1361
|
+
|
|
1362
|
+
// Start repeating vibration
|
|
1254
1363
|
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
|
1255
1364
|
vibrator?.let { v ->
|
|
1256
1365
|
val pattern = longArrayOf(0L, 500L, 500L)
|
|
@@ -1262,33 +1371,15 @@ object CallEngine {
|
|
|
1262
1371
|
}
|
|
1263
1372
|
}
|
|
1264
1373
|
|
|
1265
|
-
//
|
|
1374
|
+
// Play the ringtone
|
|
1266
1375
|
try {
|
|
1267
1376
|
val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
1268
1377
|
ringtone = RingtoneManager.getRingtone(context, uri)
|
|
1269
1378
|
ringtone?.play()
|
|
1379
|
+
Log.d(TAG, "Ringtone started playing")
|
|
1270
1380
|
} catch (e: Exception) {
|
|
1271
1381
|
Log.e(TAG, "Failed to play ringtone", e)
|
|
1272
1382
|
}
|
|
1273
|
-
|
|
1274
|
-
// 3) register a receiver to catch volume‐key presses on the RING stream
|
|
1275
|
-
if (volumeReceiver == null) {
|
|
1276
|
-
val filter = IntentFilter("android.media.VOLUME_CHANGED_ACTION")
|
|
1277
|
-
volumeReceiver = object : BroadcastReceiver() {
|
|
1278
|
-
override fun onReceive(ctx: Context, intent: Intent) {
|
|
1279
|
-
if (intent.action == "android.media.VOLUME_CHANGED_ACTION") {
|
|
1280
|
-
val streamType = intent.getIntExtra(
|
|
1281
|
-
"android.media.EXTRA_VOLUME_STREAM_TYPE", -1
|
|
1282
|
-
)
|
|
1283
|
-
if (streamType == AudioManager.STREAM_RING) {
|
|
1284
|
-
Log.d(TAG, "Volume key pressed on RING → silencing ringtone/vibrate")
|
|
1285
|
-
stopRingtone()
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
context.registerReceiver(volumeReceiver, filter)
|
|
1291
|
-
}
|
|
1292
1383
|
}
|
|
1293
1384
|
|
|
1294
1385
|
fun stopRingtone() {
|
|
@@ -1300,15 +1391,12 @@ object CallEngine {
|
|
|
1300
1391
|
}
|
|
1301
1392
|
ringtone = null
|
|
1302
1393
|
|
|
1303
|
-
//
|
|
1394
|
+
// Cancel vibration
|
|
1304
1395
|
vibrator?.cancel()
|
|
1305
1396
|
vibrator = null
|
|
1306
1397
|
|
|
1307
|
-
//
|
|
1308
|
-
|
|
1309
|
-
requireContext().unregisterReceiver(it)
|
|
1310
|
-
volumeReceiver = null
|
|
1311
|
-
}
|
|
1398
|
+
// Stop volume monitoring
|
|
1399
|
+
stopVolumeMonitoring()
|
|
1312
1400
|
}
|
|
1313
1401
|
|
|
1314
1402
|
private fun startRingback() {
|
|
@@ -1345,6 +1433,10 @@ object CallEngine {
|
|
|
1345
1433
|
stopForegroundService()
|
|
1346
1434
|
keepScreenAwake(false)
|
|
1347
1435
|
resetAudioMode()
|
|
1436
|
+
stopVolumeMonitoring()
|
|
1437
|
+
audioRouteHandler.removeCallbacksAndMessages(null)
|
|
1438
|
+
pendingAudioRoute = null
|
|
1439
|
+
audioRouteRetryCount = 0
|
|
1348
1440
|
}
|
|
1349
1441
|
|
|
1350
1442
|
// --- Lifecycle Management ---
|