srctree

Robin Linden parent 30e192c8 e65cdca4
Allow disabling the outgoing audio in calls

This also adds support for (incoming-only) audio calls with nomicrophone permissions needed.

inlinesplit
atox/src/main/kotlin/ActionReceiver.kt added: 105, removed: 45, total 60
@@ -1,9 +1,13 @@
package ltd.evilcorp.atox
 
import android.Manifest
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.widget.Toast
import androidx.core.app.RemoteInput
import androidx.core.content.ContextCompat
import javax.inject.Inject
import ltd.evilcorp.atox.ui.NotificationHelper
import ltd.evilcorp.core.vo.Contact
@@ -40,11 +44,20 @@ class ActionReceiver : BroadcastReceiver() {
}
}
 
intent.getStringExtra(KEY_CALL)?.let { callChoice ->
intent.getStringExtra(KEY_CALL)?.also { callChoice ->
val pk = intent.getStringExtra(KEY_CONTACT_PK) ?: return
if (callChoice == "accept") {
callManager.answerCall(PublicKey(pk))
notificationHelper.showOngoingCallNotification(Contact(pk, tox.getName()))
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.RECORD_AUDIO
) == PackageManager.PERMISSION_GRANTED
) {
callManager.startSendingAudio()
} else {
Toast.makeText(context, R.string.call_mic_permission_needed, Toast.LENGTH_LONG).show()
}
} else if (callChoice == "reject") {
callManager.endCall(PublicKey(pk))
notificationHelper.dismissCallNotification(Contact(pk))
 
atox/src/main/kotlin/ui/call/CallFragment.kt added: 105, removed: 45, total 60
@@ -4,6 +4,7 @@ import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
@@ -12,6 +13,7 @@ import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
import androidx.lifecycle.asLiveData
import androidx.navigation.fragment.findNavController
import ltd.evilcorp.atox.R
import ltd.evilcorp.atox.databinding.FragmentCallBinding
import ltd.evilcorp.atox.requireStringArg
import ltd.evilcorp.atox.ui.BaseFragment
@@ -30,9 +32,9 @@ class CallFragment : BaseFragment<FragmentCallBinding>(FragmentCallBinding::infl
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
startCall()
vm.startSendingAudio()
} else {
findNavController().popBackStack()
Toast.makeText(requireContext(), getString(R.string.call_mic_permission_needed), Toast.LENGTH_LONG).show()
}
}
 
@@ -53,6 +55,30 @@ class CallFragment : BaseFragment<FragmentCallBinding>(FragmentCallBinding::infl
findNavController().popBackStack()
}
 
vm.sendingAudio.asLiveData().observe(viewLifecycleOwner) { sending ->
if (sending) {
microphoneControl.setImageResource(R.drawable.ic_mic)
} else {
microphoneControl.setImageResource(R.drawable.ic_mic_off)
}
}
 
microphoneControl.setOnClickListener {
if (vm.sendingAudio.value) {
vm.stopSendingAudio()
} else {
if (ContextCompat.checkSelfPermission(
requireContext(),
PERMISSION
) == PackageManager.PERMISSION_GRANTED
) {
vm.startSendingAudio()
} else {
requestPermissionLauncher.launch(PERMISSION)
}
}
}
 
if (vm.inCall.value is CallState.InCall) {
vm.inCall.asLiveData().observe(viewLifecycleOwner) { inCall ->
if (inCall == CallState.NotInCall) {
@@ -62,12 +88,11 @@ class CallFragment : BaseFragment<FragmentCallBinding>(FragmentCallBinding::infl
return
}
 
if (ContextCompat.checkSelfPermission(requireContext(), PERMISSION) == PackageManager.PERMISSION_GRANTED) {
startCall()
return
}
startCall()
 
requestPermissionLauncher.launch(PERMISSION)
if (ContextCompat.checkSelfPermission(requireContext(), PERMISSION) == PackageManager.PERMISSION_GRANTED) {
vm.startSendingAudio()
}
}
 
private fun startCall() {
 
atox/src/main/kotlin/ui/call/CallViewModel.kt added: 105, removed: 45, total 60
@@ -29,13 +29,9 @@ class CallViewModel @Inject constructor(
publicKey = pk
}
 
fun startCall(): Boolean {
if (callManager.startCall(publicKey)) {
launch { notificationHelper.showOngoingCallNotification(contactManager.get(publicKey).first()) }
return true
}
 
return false
fun startCall() {
callManager.startCall(publicKey)
launch { notificationHelper.showOngoingCallNotification(contactManager.get(publicKey).first()) }
}
 
fun endCall() = launch {
@@ -43,5 +39,9 @@ class CallViewModel @Inject constructor(
notificationHelper.dismissCallNotification(contactManager.get(publicKey).first())
}
 
fun startSendingAudio() = callManager.startSendingAudio()
fun stopSendingAudio() = callManager.stopSendingAudio()
 
val inCall = callManager.inCall
val sendingAudio = callManager.sendingAudio
}
 
atox/src/main/res/drawable/ic_mic.xml added: 105, removed: 45, total 60
@@ -1,5 +1,10 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
</vector>
 
filename was Deleted added: 105, removed: 45, total 60
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M19,11h-1.7c0,0.74 -0.16,1.43 -0.43,2.05l1.23,1.23c0.56,-0.98 0.9,-2.09 0.9,-3.28zM14.98,11.17c0,-0.06 0.02,-0.11 0.02,-0.17L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v0.18l5.98,5.99zM4.27,3L3,4.27l6.01,6.01L9.01,11c0,1.66 1.33,3 2.99,3 0.22,0 0.44,-0.03 0.65,-0.08l1.66,1.66c-0.71,0.33 -1.5,0.52 -2.31,0.52 -2.76,0 -5.3,-2.1 -5.3,-5.1L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c0.91,-0.13 1.77,-0.45 2.54,-0.9L19.73,21 21,19.73 4.27,3z"/>
</vector>
 
atox/src/main/res/layout/fragment_call.xml added: 105, removed: 45, total 60
@@ -28,8 +28,7 @@
android:background="@null"
android:contentDescription="@string/microphone_control"
android:scaleType="fitCenter"
android:src="@drawable/ic_mic"
android:visibility="invisible" />
android:src="@drawable/ic_mic"/>
 
<com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/end_call"
android:layout_width="wrap_content"
 
atox/src/main/res/values/strings.xml added: 105, removed: 45, total 60
@@ -152,4 +152,5 @@
<string name="contact_default_name">Unknown</string>
<string name="incoming_call">Incoming call</string>
<string name="incoming_call_from">Incoming call from %1$s</string>
<string name="call_mic_permission_needed">Microphone permission required to enable sending audio</string>
</resources>
No newline at end of file
 
domain/src/main/kotlin/feature/CallManager.kt added: 105, removed: 45, total 60
@@ -28,28 +28,17 @@ class CallManager @Inject constructor(
private val _inCall = MutableStateFlow<CallState>(CallState.NotInCall)
val inCall: StateFlow<CallState> get() = _inCall
 
fun startCall(publicKey: PublicKey): Boolean {
val recorder = AudioCapture(48_000, 1)
if (!recorder.isOk()) {
return false
}
private val _sendingAudio = MutableStateFlow(false)
val sendingAudio: StateFlow<Boolean> get() = _sendingAudio
 
fun startCall(publicKey: PublicKey) {
tox.startCall(publicKey)
_inCall.value = CallState.InCall(publicKey)
startAudioSender(recorder, publicKey)
return true
}
 
fun answerCall(publicKey: PublicKey): Boolean {
val recorder = AudioCapture(48_000, 1)
if (!recorder.isOk()) {
return false
}
 
fun answerCall(publicKey: PublicKey) {
tox.answerCall(publicKey)
_inCall.value = CallState.InCall(publicKey)
startAudioSender(recorder, publicKey)
return true
}
 
fun endCall(publicKey: PublicKey) {
@@ -63,10 +52,27 @@ class CallManager @Inject constructor(
}
}
 
fun startSendingAudio(): Boolean {
val to = (inCall.value as CallState.InCall?)?.publicKey ?: return false
 
val recorder = AudioCapture(48_000, 1)
if (!recorder.isOk()) {
return false
}
 
startAudioSender(recorder, to)
return true
}
 
fun stopSendingAudio() {
_sendingAudio.value = false
}
 
private fun startAudioSender(recorder: AudioCapture, to: PublicKey) {
launch {
recorder.start()
while (inCall.value is CallState.InCall) {
_sendingAudio.value = true
while (inCall.value is CallState.InCall && sendingAudio.value) {
val start = System.currentTimeMillis()
val audioFrame = recorder.read()
try {
@@ -81,6 +87,7 @@ class CallManager @Inject constructor(
}
recorder.stop()
recorder.release()
_sendingAudio.value = false
}
}
}