diff --git a/atox/src/main/kotlin/ui/call/CallFragment.kt b/atox/src/main/kotlin/ui/call/CallFragment.kt index 7cd5c0d69..3c5b04a06 100644 --- a/atox/src/main/kotlin/ui/call/CallFragment.kt +++ b/atox/src/main/kotlin/ui/call/CallFragment.kt @@ -5,10 +5,19 @@ package ltd.evilcorp.atox.ui.call import android.Manifest +import android.media.AudioDeviceInfo.TYPE_BLE_HEADSET +import android.media.AudioDeviceInfo.TYPE_BLE_SPEAKER +import android.media.AudioDeviceInfo.TYPE_BLUETOOTH_SCO +import android.media.AudioDeviceInfo.TYPE_WIRED_HEADPHONES +import android.media.AudioDeviceInfo.TYPE_WIRED_HEADSET +import android.media.AudioManager +import android.os.Build import android.os.Bundle import android.view.View import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat.getSystemService import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding @@ -28,6 +37,20 @@ import ltd.evilcorp.domain.tox.PublicKey private const val PERMISSION = Manifest.permission.RECORD_AUDIO class CallFragment : BaseFragment(FragmentCallBinding::inflate) { + + companion object { + private var hasCalled = false + + @RequiresApi(Build.VERSION_CODES.S) + private val audioOutputDevices = arrayOf( + TYPE_WIRED_HEADPHONES, + TYPE_WIRED_HEADSET, + TYPE_BLE_HEADSET, + TYPE_BLE_SPEAKER, + TYPE_BLUETOOTH_SCO, + ) + } + private val vm: CallViewModel by viewModels { vmFactory } private val requestPermissionLauncher = registerForActivityResult( @@ -40,6 +63,7 @@ class CallFragment : BaseFragment(FragmentCallBinding::infl } } + @RequiresApi(Build.VERSION_CODES.S) override fun onViewCreated(view: View, savedInstanceState: Bundle?) = binding.run { ViewCompat.setOnApplyWindowInsetsListener(view) { _, compat -> val insets = compat.getInsets(WindowInsetsCompat.Type.systemBars()) @@ -77,6 +101,7 @@ class CallFragment : BaseFragment(FragmentCallBinding::infl } } + updateSpeakerphoneOnDetectHeadphones() updateSpeakerphoneIcon() speakerphone.setOnClickListener { vm.toggleSpeakerphone() @@ -108,12 +133,28 @@ class CallFragment : BaseFragment(FragmentCallBinding::infl binding.speakerphone.setImageResource(icon) } + @RequiresApi(Build.VERSION_CODES.S) + private fun updateSpeakerphoneOnDetectHeadphones() { + if (headphonesArePlugged()) { + vm.disableSpeakerphone() + } + } + private fun startCall() { vm.startCall() vm.inCall.asLiveData().observe(viewLifecycleOwner) { inCall -> if (inCall == CallState.NotInCall) { findNavController().popBackStack() + hasCalled = false } } } + + @RequiresApi(Build.VERSION_CODES.S) + private fun headphonesArePlugged(): Boolean { + val audioManager = context?.let { getSystemService(it, AudioManager::class.java) } ?: return false + return audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS).any { info -> + audioOutputDevices.contains(info.type) + } + } } diff --git a/atox/src/main/kotlin/ui/call/CallViewModel.kt b/atox/src/main/kotlin/ui/call/CallViewModel.kt index 5d310ccf2..35507328c 100644 --- a/atox/src/main/kotlin/ui/call/CallViewModel.kt +++ b/atox/src/main/kotlin/ui/call/CallViewModel.kt @@ -49,8 +49,7 @@ class CallViewModel @Inject constructor( fun startSendingAudio() = callManager.startSendingAudio() fun stopSendingAudio() = callManager.stopSendingAudio() - fun toggleSpeakerphone() { - speakerphoneOn = !speakerphoneOn + fun applyProximityScreenSetting() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (speakerphoneOn) { proximityScreenOff.release() @@ -60,6 +59,16 @@ class CallViewModel @Inject constructor( } } + fun disableSpeakerphone() { + speakerphoneOn = false + applyProximityScreenSetting() + } + + fun toggleSpeakerphone() { + speakerphoneOn = !speakerphoneOn + applyProximityScreenSetting() + } + val inCall = callManager.inCall val sendingAudio = callManager.sendingAudio diff --git a/domain/src/main/kotlin/av/HeadsetPlugReceiver.kt b/domain/src/main/kotlin/av/HeadsetPlugReceiver.kt new file mode 100644 index 000000000..9451f386c --- /dev/null +++ b/domain/src/main/kotlin/av/HeadsetPlugReceiver.kt @@ -0,0 +1,46 @@ +package ltd.evilcorp.domain.av + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.media.AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED +import android.media.AudioManager.EXTRA_SCO_AUDIO_STATE +import android.media.AudioManager.SCO_AUDIO_STATE_CONNECTED +import android.util.Log +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class HeadsetPlugReceiver : BroadcastReceiver() { + + private companion object { + private const val logTag = "HeadsetPlugReceiver" + private const val headsetState = "state" + } + + private val _isPlugged = MutableStateFlow(false) + val isPlugged: StateFlow = _isPlugged.asStateFlow() + + private fun checkStateOff(intent: Intent) { + Log.d(logTag, "[HeadsetPlugReceiver.checkStateOff] intent: $intent") + } + + private fun sendEvent(intent: Intent) { + val isPlugged = when (intent.action) { + Intent.ACTION_HEADSET_PLUG -> intent.getIntExtra(headsetState, 0) == 1 + ACTION_SCO_AUDIO_STATE_UPDATED -> intent.getIntExtra(EXTRA_SCO_AUDIO_STATE, 0) == SCO_AUDIO_STATE_CONNECTED + else -> false + } + Log.d(logTag, "[HeadsetPlugReceiver.sendEvent] isPlugged: $isPlugged") + _isPlugged.value = isPlugged + } + + override fun onReceive(context: Context, intent: Intent?) { + val action = intent?.action ?: return + Log.d(logTag, "[HeadsetPlugReceiver.onReceive] action: $action") + when (action) { + Intent.ACTION_HEADSET_PLUG, ACTION_SCO_AUDIO_STATE_UPDATED -> sendEvent(intent) + else -> checkStateOff(intent) + } + } +}