|
1 | 1 | package com.webtrit.callkeep.services.services.connection |
2 | 2 |
|
3 | 3 | import android.content.Context |
| 4 | +import android.media.AudioAttributes |
| 5 | +import android.media.AudioFocusRequest |
4 | 6 | import android.os.Build |
5 | 7 | import android.os.Handler |
6 | 8 | import android.os.Looper |
@@ -44,6 +46,19 @@ class PhoneConnection internal constructor( |
44 | 46 | private var isMute = false |
45 | 47 | private var isHasSpeaker = false |
46 | 48 |
|
| 49 | + /** |
| 50 | + * True when this connection explicitly set MODE_IN_COMMUNICATION because the OEM Telecom |
| 51 | + * (e.g. MIUI) skipped audio-focus setup for outgoing calls. Used to know whether we need |
| 52 | + * to reset the audio mode on disconnect. |
| 53 | + */ |
| 54 | + private var audioModeForced = false |
| 55 | + |
| 56 | + /** |
| 57 | + * Audio focus request held alongside [audioModeForced], for the same OEM Telecom |
| 58 | + * workaround — released together with the forced mode reset on disconnect. |
| 59 | + */ |
| 60 | + private var forcedAudioFocusRequest: AudioFocusRequest? = null |
| 61 | + |
47 | 62 | /** |
48 | 63 | * Tracks whether the speaker was manually disabled by the user. |
49 | 64 | * Prevents automatic re-enabling during video calls. |
@@ -202,6 +217,23 @@ class PhoneConnection internal constructor( |
202 | 217 |
|
203 | 218 | dispatcher(eventForDisconnectCause(disconnectCause), metadata) |
204 | 219 | onDisconnectCallback.invoke(this) |
| 220 | + |
| 221 | + // If we forced MODE_IN_COMMUNICATION in onActiveConnection(), restore NORMAL mode now |
| 222 | + // that this call is gone. Only reset if no other active/holding calls remain so we |
| 223 | + // don't disrupt a concurrent call that also needs the VoIP audio path. |
| 224 | + if (audioModeForced && !PhoneConnectionService.connectionManager.hasActiveOrHoldingConnection()) { |
| 225 | + val sysAm = context.getSystemService(android.content.Context.AUDIO_SERVICE) as android.media.AudioManager |
| 226 | + sysAm.mode = android.media.AudioManager.MODE_NORMAL |
| 227 | + logger.d("onDisconnect: reset audio mode to MODE_NORMAL (last active call ended)") |
| 228 | + |
| 229 | + forcedAudioFocusRequest?.let { |
| 230 | + sysAm.abandonAudioFocusRequest(it) |
| 231 | + logger.d("onDisconnect: abandoned call audio focus (OEM workaround)") |
| 232 | + } |
| 233 | + forcedAudioFocusRequest = null |
| 234 | + } |
| 235 | + audioModeForced = false |
| 236 | + |
205 | 237 | destroy() |
206 | 238 | } |
207 | 239 |
|
@@ -634,6 +666,45 @@ class PhoneConnection internal constructor( |
634 | 666 |
|
635 | 667 | enforceVideoSpeakerLogic() |
636 | 668 |
|
| 669 | + // On OEM Telecom implementations (e.g. MIUI Android 9-12) that skip audio-focus setup |
| 670 | + // for outgoing self-managed calls, callAudioState is never delivered and |
| 671 | + // AudioManager.setMode(MODE_IN_COMMUNICATION) is never called. Without that mode, |
| 672 | + // WebRTC's AudioRecord(VOICE_COMMUNICATION) and AudioTrack(STREAM_VOICE_CALL) cannot |
| 673 | + // access the voice call path on these devices — both mic capture and speaker playback |
| 674 | + // stall completely. Explicitly set the mode here so WebRTC audio works. |
| 675 | + // On AOSP, callAudioState is non-null (Telecom already set the mode), so the guard |
| 676 | + // keeps this change inert on standard Android. |
| 677 | + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE && callAudioState == null) { |
| 678 | + val sysAm = context.getSystemService(android.content.Context.AUDIO_SERVICE) as android.media.AudioManager |
| 679 | + if (sysAm.mode != android.media.AudioManager.MODE_IN_COMMUNICATION) { |
| 680 | + sysAm.mode = android.media.AudioManager.MODE_IN_COMMUNICATION |
| 681 | + audioModeForced = true |
| 682 | + logger.d("onActiveConnection: forced MODE_IN_COMMUNICATION (callAudioState null — OEM workaround)") |
| 683 | + } |
| 684 | + |
| 685 | + // Telecom normally requests call audio focus for us when a self-managed connection |
| 686 | + // becomes active (CallAudioModeStateMachine); the same OEM bug that skips setMode() |
| 687 | + // also skips this. Without it, another app's audio focus can keep suppressing |
| 688 | + // WebRTC's stream even after MODE_IN_COMMUNICATION is set. requestAudioFocusForCall() |
| 689 | + // is a hidden @SystemApi we can't call, so mirror the intent with the public API. |
| 690 | + if (forcedAudioFocusRequest == null) { |
| 691 | + val attributes = AudioAttributes.Builder() |
| 692 | + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) |
| 693 | + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) |
| 694 | + .build() |
| 695 | + val request = AudioFocusRequest.Builder(android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) |
| 696 | + .setAudioAttributes(attributes) |
| 697 | + .build() |
| 698 | + val result = sysAm.requestAudioFocus(request) |
| 699 | + if (result == android.media.AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { |
| 700 | + forcedAudioFocusRequest = request |
| 701 | + logger.d("onActiveConnection: acquired call audio focus (OEM workaround)") |
| 702 | + } else { |
| 703 | + logger.w("onActiveConnection: audio focus request denied (result=$result)") |
| 704 | + } |
| 705 | + } |
| 706 | + } |
| 707 | + |
637 | 708 | // Proactively emit audio device state for OEM Telecom implementations that do not |
638 | 709 | // call setCallAudioState for outgoing calls (e.g. MIUI Android 12). On AOSP, |
639 | 710 | // CallAudioRouteStateMachine.resendSystemAudioState() fires during call setup and |
|
0 commit comments