srctree

Robin Linden parent 7cb54967 269fbd1b
Add support for outgoing audio calls

inlinesplit
atox/src/main/AndroidManifest.xml added: 503, removed: 36, total 467
@@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
 
<uses-feature android:name="android.hardware.camera" android:required="false"/>
 
 
atox/src/main/kotlin/di/ViewModelModule.kt added: 503, removed: 36, total 467
@@ -7,6 +7,7 @@ import dagger.Module
import dagger.multibindings.IntoMap
import kotlin.reflect.KClass
import ltd.evilcorp.atox.ui.addcontact.AddContactViewModel
import ltd.evilcorp.atox.ui.call.CallViewModel
import ltd.evilcorp.atox.ui.chat.ChatViewModel
import ltd.evilcorp.atox.ui.contact_profile.ContactProfileViewModel
import ltd.evilcorp.atox.ui.contactlist.ContactListViewModel
@@ -33,6 +34,11 @@ abstract class ViewModelModule {
@ViewModelKey(AddContactViewModel::class)
abstract fun bindAddContactViewModel(vm: AddContactViewModel): ViewModel
 
@Binds
@IntoMap
@ViewModelKey(CallViewModel::class)
abstract fun bindCallViewModel(vm: CallViewModel): ViewModel
 
@Binds
@IntoMap
@ViewModelKey(ChatViewModel::class)
 
atox/src/main/kotlin/tox/EventListenerCallbacks.kt added: 503, removed: 36, total 467
@@ -2,6 +2,7 @@ package ltd.evilcorp.atox.tox
 
import android.content.Context
import android.util.Log
import im.tox.tox4j.av.enums.ToxavFriendCallState
import im.tox.tox4j.core.enums.ToxFileControl
import java.net.URLConnection
import java.util.Date
@@ -26,6 +27,7 @@ import ltd.evilcorp.core.vo.FileTransfer
import ltd.evilcorp.core.vo.FriendRequest
import ltd.evilcorp.core.vo.Message
import ltd.evilcorp.core.vo.Sender
import ltd.evilcorp.domain.av.AudioPlayer
import ltd.evilcorp.domain.feature.ChatManager
import ltd.evilcorp.domain.feature.FileTransferManager
import ltd.evilcorp.domain.tox.PublicKey
@@ -57,6 +59,7 @@ class EventListenerCallbacks @Inject constructor(
private val settings: Settings,
) : CoroutineScope by GlobalScope {
private var contacts: List<Contact> = listOf()
private var audioPlayer: AudioPlayer? = null
 
init {
launch {
@@ -166,6 +169,10 @@ class EventListenerCallbacks @Inject constructor(
 
callStateHandler = { pk, callState ->
Log.e(TAG, "callState ${pk.take(8)} $callState")
if (callState.contains(ToxavFriendCallState.FINISHED)) {
audioPlayer?.stop()
audioPlayer = null
}
}
 
videoBitRateHandler = { pk, bitRate ->
@@ -192,5 +199,13 @@ class EventListenerCallbacks @Inject constructor(
audioBitRateHandler = { pk, bitRate ->
Log.e(TAG, "audioBitRate ${pk.take(8)} $bitRate")
}
 
audioReceiveFrameHandler = { _, pcm, channels, samplingRate ->
if (audioPlayer == null) {
audioPlayer = AudioPlayer(samplingRate, channels)
audioPlayer?.start()
}
audioPlayer?.buffer(pcm)
}
}
}
 
atox/src/main/kotlin/ui/NotificationHelper.kt added: 503, removed: 36, total 467
@@ -35,6 +35,7 @@ import ltd.evilcorp.domain.tox.PublicKey
 
private const val MESSAGE = "aTox messages"
private const val FRIEND_REQUEST = "aTox friend requests"
private const val CALL = "aTox call"
 
@Singleton
class NotificationHelper @Inject constructor(
@@ -65,7 +66,13 @@ class NotificationHelper @Inject constructor(
NotificationManager.IMPORTANCE_HIGH
)
 
notifier.createNotificationChannels(listOf(messageChannel, friendRequestChannel))
val callChannel = NotificationChannel(
CALL,
context.getString(R.string.calls),
NotificationManager.IMPORTANCE_HIGH
)
 
notifier.createNotificationChannels(listOf(messageChannel, friendRequestChannel, callChannel))
}
 
fun dismissNotifications(publicKey: PublicKey) = notifier.cancel(publicKey.string().hashCode())
@@ -182,4 +189,31 @@ class NotificationHelper @Inject constructor(
 
notifier.notify(friendRequest.publicKey.hashCode(), notificationBuilder.build())
}
 
fun dismissCallNotification(contact: Contact) =
notifier.cancel(contact.publicKey.hashCode() + CALL.hashCode())
 
fun showCallNotification(contact: Contact) {
val notificationBuilder = NotificationCompat.Builder(context, FRIEND_REQUEST)
.setSmallIcon(android.R.drawable.ic_menu_call)
.setContentTitle(context.getString(R.string.ongoing_call))
.setContentText(context.getString(R.string.in_call_with, contact.name))
.setUsesChronometer(true)
.setWhen(System.currentTimeMillis())
.setContentIntent(
NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.callFragment)
.setArguments(bundleOf(CONTACT_PUBLIC_KEY to contact.publicKey))
.createPendingIntent()
)
.setOngoing(true)
.setSilent(true)
 
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
notificationBuilder.setCategory(Notification.CATEGORY_CALL)
}
 
notifier.notify(contact.publicKey.hashCode() + CALL.hashCode(), notificationBuilder.build())
}
}
 
filename was Deleted added: 503, removed: 36, total 467
@@ -0,0 +1,71 @@
package ltd.evilcorp.atox.ui.call
 
import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import ltd.evilcorp.atox.databinding.FragmentCallBinding
import ltd.evilcorp.atox.requireStringArg
import ltd.evilcorp.atox.ui.BaseFragment
import ltd.evilcorp.atox.ui.chat.CONTACT_PUBLIC_KEY
import ltd.evilcorp.atox.ui.setAvatarFromContact
import ltd.evilcorp.atox.vmFactory
import ltd.evilcorp.domain.tox.PublicKey
 
private val PERMISSIONS = arrayOf(Manifest.permission.RECORD_AUDIO)
private const val REQUEST_RECORD_AUDIO_PERMISSION = 8888
 
class CallFragment : BaseFragment<FragmentCallBinding>(FragmentCallBinding::inflate) {
private val vm: CallViewModel by viewModels { vmFactory }
 
override fun onViewCreated(view: View, savedInstanceState: Bundle?) = binding.run {
ViewCompat.setOnApplyWindowInsetsListener(view) { _, compat ->
val insets = compat.getInsets(WindowInsetsCompat.Type.systemBars())
controlContainer.updatePadding(bottom = insets.bottom)
compat
}
 
vm.setActiveContact(PublicKey(requireStringArg(CONTACT_PUBLIC_KEY)))
vm.contact.observe(viewLifecycleOwner) {
setAvatarFromContact(callBackground, it)
}
 
endCall.setOnClickListener {
vm.endCall()
findNavController().popBackStack()
}
 
if (vm.inCall()) {
return
}
 
if (ContextCompat.checkSelfPermission(requireContext(), PERMISSIONS[0]) == PackageManager.PERMISSION_GRANTED) {
vm.startCall()
return
}
 
requestPermissions(PERMISSIONS, REQUEST_RECORD_AUDIO_PERMISSION)
}
 
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
 
val granted = if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION) {
grantResults[0] == PackageManager.PERMISSION_GRANTED
} else {
false
}
 
if (!granted) {
findNavController().popBackStack()
} else {
vm.startCall()
}
}
}
 
filename was Deleted added: 503, removed: 36, total 467
@@ -0,0 +1,47 @@
package ltd.evilcorp.atox.ui.call
 
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import ltd.evilcorp.atox.ui.NotificationHelper
import ltd.evilcorp.core.vo.Contact
import ltd.evilcorp.domain.feature.CallManager
import ltd.evilcorp.domain.feature.ContactManager
import ltd.evilcorp.domain.tox.PublicKey
 
class CallViewModel @Inject constructor(
private val callManager: CallManager,
private val notificationHelper: NotificationHelper,
private val contactManager: ContactManager,
) : ViewModel(), CoroutineScope by GlobalScope {
private var publicKey = PublicKey("")
 
val contact: LiveData<Contact> by lazy {
contactManager.get(publicKey).asLiveData()
}
 
fun setActiveContact(pk: PublicKey) {
publicKey = pk
}
 
fun startCall(): Boolean {
if (callManager.startCall(publicKey)) {
launch { notificationHelper.showCallNotification(contactManager.get(publicKey).first()) }
return true
}
 
return false
}
 
fun endCall() = launch {
callManager.endCall(publicKey)
notificationHelper.dismissCallNotification(contactManager.get(publicKey).first())
}
 
fun inCall() = callManager.isInCall()
}
 
atox/src/main/kotlin/ui/chat/ChatFragment.kt added: 503, removed: 36, total 467
@@ -73,6 +73,7 @@ class ChatFragment : BaseFragment<FragmentChatBinding>(FragmentChatBinding::infl
toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
 
toolbar.inflateMenu(R.menu.chat_options_menu)
toolbar.menu.findItem(R.id.call).isEnabled = !viewModel.inCall()
toolbar.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.clear_history -> {
@@ -86,6 +87,13 @@ class ChatFragment : BaseFragment<FragmentChatBinding>(FragmentChatBinding::infl
.setNegativeButton(R.string.cancel, null).show()
true
}
R.id.call -> {
findNavController().navigate(
R.id.action_chatFragment_to_callFragment,
bundleOf(CONTACT_PUBLIC_KEY to contactPubKey)
)
true
}
else -> super.onOptionsItemSelected(item)
}
}
 
atox/src/main/kotlin/ui/chat/ChatViewModel.kt added: 503, removed: 36, total 467
@@ -24,6 +24,7 @@ import ltd.evilcorp.core.vo.Contact
import ltd.evilcorp.core.vo.FileTransfer
import ltd.evilcorp.core.vo.Message
import ltd.evilcorp.core.vo.MessageType
import ltd.evilcorp.domain.feature.CallManager
import ltd.evilcorp.domain.feature.ChatManager
import ltd.evilcorp.domain.feature.ContactManager
import ltd.evilcorp.domain.feature.FileTransferManager
@@ -32,6 +33,7 @@ import ltd.evilcorp.domain.tox.PublicKey
private const val TAG = "ChatViewModel"
 
class ChatViewModel @Inject constructor(
private val callManager: CallManager,
private val chatManager: ChatManager,
private val contactManager: ContactManager,
private val fileTransferManager: FileTransferManager,
@@ -119,4 +121,6 @@ class ChatViewModel @Inject constructor(
 
fun setDraft(draft: String) = contactManager.setDraft(publicKey, draft)
fun clearDraft() = setDraft("")
 
fun inCall() = callManager.isInCall()
}
 
filename was Deleted added: 503, removed: 36, total 467
@@ -0,0 +1,5 @@
<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,9c-1.6,0 -3.15,0.25 -4.6,0.72v3.1c0,0.39 -0.23,0.74 -0.56,0.9 -0.98,0.49 -1.87,1.12 -2.66,1.85 -0.18,0.18 -0.43,0.28 -0.7,0.28 -0.28,0 -0.53,-0.11 -0.71,-0.29L0.29,13.08c-0.18,-0.17 -0.29,-0.42 -0.29,-0.7 0,-0.28 0.11,-0.53 0.29,-0.71C3.34,8.78 7.46,7 12,7s8.66,1.78 11.71,4.67c0.18,0.18 0.29,0.43 0.29,0.71 0,0.28 -0.11,0.53 -0.29,0.71l-2.48,2.48c-0.18,0.18 -0.43,0.29 -0.71,0.29 -0.27,0 -0.52,-0.11 -0.7,-0.28 -0.79,-0.74 -1.69,-1.36 -2.67,-1.85 -0.33,-0.16 -0.56,-0.5 -0.56,-0.9v-3.1C15.15,9.25 13.6,9 12,9z"/>
</vector>
 
filename was Deleted added: 503, removed: 36, total 467
@@ -0,0 +1,5 @@
<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>
 
filename was Deleted added: 503, removed: 36, total 467
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView android:id="@+id/call_background"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:adjustViewBounds="true"
android:importantForAccessibility="no"
android:scaleType="centerCrop" />
 
<RelativeLayout android:id="@+id/control_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@color/transparentBar"
android:clipToPadding="false"
android:padding="16dp">
 
<ImageButton android:id="@+id/microphone_control"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_marginStart="16dp"
android:background="@null"
android:contentDescription="@string/microphone_control"
android:scaleType="fitCenter"
android:src="@drawable/ic_mic"
android:visibility="invisible" />
 
<com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/end_call"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_alignParentTop="true"
android:contentDescription="@string/end_call"
android:src="@drawable/ic_call_end"
app:backgroundTint="@android:color/holo_red_dark" />
</RelativeLayout>
 
<com.google.android.material.imageview.ShapeableImageView android:id="@+id/user_avatar"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:src="@mipmap/launcher_icon"
android:visibility="invisible"
app:shapeAppearanceOverlay="@style/CircleImageView" />
</RelativeLayout>
 
atox/src/main/res/menu/chat_options_menu.xml added: 503, removed: 36, total 467
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/call" android:title="@string/call"/>
<item android:id="@+id/clear_history" android:title="@string/clear_history"/>
</menu>
 
atox/src/main/res/navigation/nav_graph.xml added: 503, removed: 36, total 467
@@ -9,6 +9,11 @@
android:label="ProfileFragment"
tools:layout="@layout/fragment_profile"/>
 
<fragment android:id="@+id/callFragment"
android:name="ltd.evilcorp.atox.ui.call.CallFragment"
android:label="CallFragment"
tools:layout="@layout/fragment_call"/>
 
<fragment android:id="@+id/contactListFragment"
android:name="ltd.evilcorp.atox.ui.contactlist.ContactListFragment"
android:label="ContactListFragment"
@@ -73,6 +78,13 @@
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in"
app:popExitAnim="@anim/slide_out_right"/>
 
<action android:id="@+id/action_chatFragment_to_callFragment"
app:destination="@id/callFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in"
app:popExitAnim="@anim/slide_out_right"/>
</fragment>
 
<fragment android:id="@+id/addContactFragment"
 
atox/src/main/res/values/strings.xml added: 503, removed: 36, total 467
@@ -136,4 +136,11 @@
<string name="error_no_nodes_loaded">Unable to load bootstrap nodes, please switch to built-in nodes or import nodes again</string>
<string name="receive_share_share_to">Share to…</string>
<string name="contact_list_delete_contact_confirm">Are you sure you want to delete \"%1$s\" from your contacts?</string>
 
<string name="call">Call</string>
<string name="microphone_control">Microphone control</string>
<string name="end_call">End call</string>
<string name="calls">Calls</string>
<string name="ongoing_call">Ongoing call</string>
<string name="in_call_with">In call with %1$s</string>
</resources>
No newline at end of file
 
domain/build.gradle.kts added: 503, removed: 36, total 467
@@ -48,6 +48,9 @@ android {
}
lintOptions {
disable("InvalidPackage") // tox4j is still not really allowed on Android. :/
// The macOS domain:lint task fails due to not guarding AudioRecord with permission checks in this module.
// This doesn't fail locally, and use of the audio code is guarded in the UI in the aTox module.
isAbortOnError = false
}
sourceSets["main"].java.srcDir("src/main/kotlin")
sourceSets["test"].java.srcDir("src/test/kotlin")
 
filename was Deleted added: 503, removed: 36, total 467
@@ -0,0 +1,46 @@
package ltd.evilcorp.domain.av
 
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
 
private fun intToChannel(channels: Int) = when (channels) {
1 -> AudioFormat.CHANNEL_IN_MONO
else -> AudioFormat.CHANNEL_IN_STEREO
}
 
private fun findAudioRecord(sampleRate: Int, channels: Int): AudioRecord? {
val audioFormat = AudioFormat.ENCODING_PCM_16BIT
val channelConfig = intToChannel(channels)
 
val bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
if (bufferSize == AudioRecord.ERROR_BAD_VALUE) {
return null
}
 
val recorder = AudioRecord(
MediaRecorder.AudioSource.VOICE_COMMUNICATION,
sampleRate,
channelConfig,
audioFormat,
bufferSize
)
 
if (recorder.state != AudioRecord.STATE_INITIALIZED) {
return null
}
 
return recorder
}
 
class AudioCapture(private val sampleRate: Int, private val channels: Int) {
private val audioRecord = findAudioRecord(sampleRate, channels)
fun isOk() = audioRecord != null
fun start() = audioRecord?.startRecording()
fun stop() = audioRecord?.stop()
fun read(): ShortArray {
val bytes = ShortArray((sampleRate * channels * 0.1).toInt()) // E.g. 16-bit, 48kHz, 1 channel, 100ms
audioRecord?.read(bytes, 0, bytes.size)
return bytes
}
}
 
filename was Deleted added: 503, removed: 36, total 467
@@ -0,0 +1,74 @@
package ltd.evilcorp.domain.av
 
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioTrack
import android.os.Build
import java.util.Queue
import java.util.concurrent.ConcurrentLinkedQueue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
 
private fun intToChannel(channels: Int) = when (channels) {
1 -> AudioFormat.CHANNEL_OUT_MONO
else -> AudioFormat.CHANNEL_OUT_STEREO
}
 
class AudioPlayer(private val sampleRate: Int, channels: Int) : CoroutineScope by GlobalScope {
private val minBufferSize =
AudioTrack.getMinBufferSize(sampleRate, intToChannel(channels), AudioFormat.ENCODING_PCM_16BIT)
private val audioTrack = if (Build.VERSION.SDK_INT < 23) {
// TODO(robinlinden): Verify that this works on old devices.
AudioTrack(
AudioManager.STREAM_VOICE_CALL,
sampleRate,
intToChannel(channels),
AudioFormat.ENCODING_PCM_16BIT,
minBufferSize,
AudioTrack.MODE_STREAM
)
} else {
AudioTrack.Builder()
.setAudioFormat(
AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(sampleRate)
.setChannelMask(intToChannel(channels))
.build()
)
.setBufferSizeInBytes(minBufferSize)
.build()
}
private val audioQueue: Queue<ShortArray> = ConcurrentLinkedQueue()
 
private var active = false
 
fun buffer(data: ShortArray) = audioQueue.add(data)
 
fun start() {
active = true
launch {
audioTrack.play()
while (active) {
val sleepTime = playAudioFrame()
delay(sleepTime.toLong())
}
audioTrack.pause()
audioTrack.flush()
}
}
 
fun stop() {
active = false
}
 
private fun playAudioFrame(): Int = if (audioQueue.isEmpty()) {
0
} else {
val data = audioQueue.remove()
audioTrack.write(data, 0, data.size)
data.size * 1000 / sampleRate
}
}
 
filename was Deleted added: 503, removed: 36, total 467
@@ -0,0 +1,64 @@
package ltd.evilcorp.domain.feature
 
import android.util.Log
import im.tox.tox4j.av.exceptions.ToxavCallControlException
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import ltd.evilcorp.domain.av.AudioCapture
import ltd.evilcorp.domain.tox.PublicKey
import ltd.evilcorp.domain.tox.Tox
 
private const val TAG = "CallManager"
 
@Singleton
class CallManager @Inject constructor(
private val tox: Tox,
) : CoroutineScope by GlobalScope {
private var inCall = false
 
fun isInCall() = inCall
 
fun startCall(publicKey: PublicKey): Boolean {
tox.startCall(publicKey)
inCall = true
 
val recorder = AudioCapture(48_000, 1)
if (!recorder.isOk()) {
return false
}
 
launch {
recorder.start()
while (inCall) {
val start = System.currentTimeMillis()
val audioFrame = recorder.read()
try {
tox.sendAudio(publicKey, audioFrame, 1, 48_000)
} catch (e: Exception) {
Log.e(TAG, e.toString())
}
val elapsed = System.currentTimeMillis() - start
if (elapsed < 20) {
delay(20 - elapsed)
}
}
recorder.stop()
}
return true
}
 
fun endCall(publicKey: PublicKey) {
inCall = false
try {
tox.endCall(publicKey)
} catch (e: ToxavCallControlException) {
if (e.code() != ToxavCallControlException.Code.FRIEND_NOT_IN_CALL) {
throw e
}
}
}
}
 
domain/src/main/kotlin/tox/Tox.kt added: 503, removed: 36, total 467
@@ -38,6 +38,7 @@ class Tox @Inject constructor(
var isBootstrapNeeded = true
 
private var running = false
private var toxAvRunning = false
 
private lateinit var tox: ToxWrapper
 
@@ -55,9 +56,18 @@ class Tox @Inject constructor(
}
}
 
fun iterateForeverAv() = launch {
toxAvRunning = true
while (running) {
tox.iterateAv()
delay(tox.iterationIntervalAv())
}
toxAvRunning = false
}
 
fun iterateForever() = launch {
running = true
while (running) {
while (running || toxAvRunning) {
if (isBootstrapNeeded) {
try {
bootstrap()
@@ -76,6 +86,7 @@ class Tox @Inject constructor(
save()
loadContacts()
iterateForever()
iterateForeverAv()
}
 
fun stop() = launch {
@@ -157,7 +168,8 @@ class Tox @Inject constructor(
fun getStatus() = async { tox.getStatus() }
 
// ToxAv, probably move these.
fun endCall(pk: PublicKey) = launch {
tox.endCall(pk)
}
fun startCall(pk: PublicKey) = tox.startCall(pk)
fun endCall(pk: PublicKey) = tox.endCall(pk)
fun sendAudio(pk: PublicKey, pcm: ShortArray, channels: Int, samplingRate: Int) =
tox.sendAudio(pk, pcm, channels, samplingRate)
}
 
domain/src/main/kotlin/tox/ToxWrapper.kt added: 503, removed: 36, total 467
@@ -8,7 +8,6 @@ import im.tox.tox4j.core.exceptions.ToxFileSendChunkException
import im.tox.tox4j.core.exceptions.ToxFriendAddException
import im.tox.tox4j.impl.jni.ToxAvImpl
import im.tox.tox4j.impl.jni.ToxCoreImpl
import kotlin.math.min
import kotlin.random.Random
import ltd.evilcorp.core.vo.FileKind
import ltd.evilcorp.core.vo.MessageType
@@ -47,13 +46,10 @@ class ToxWrapper(
tox.close()
}
 
fun iterate() {
tox.iterate(eventListener, Unit)
av.iterate(avEventListener, Unit)
}
 
fun iterationInterval(): Long =
min(tox.iterationInterval(), av.iterationInterval()).toLong()
fun iterate(): Unit = tox.iterate(eventListener, Unit)
fun iterateAv(): Unit = av.iterate(avEventListener, Unit)
fun iterationInterval(): Long = tox.iterationInterval().toLong()
fun iterationIntervalAv(): Long = av.iterationInterval().toLong()
 
fun getName(): String = String(tox.name)
fun setName(name: String) {
@@ -143,7 +139,8 @@ class ToxWrapper(
private fun contactByKey(publicKey: PublicKey): Int = tox.friendByPublicKey(publicKey.bytes())
 
// ToxAv, probably move these.
fun endCall(pk: PublicKey) {
av.callControl(contactByKey(pk), ToxavCallControl.CANCEL)
}
fun startCall(pk: PublicKey) = av.call(contactByKey(pk), 128, 0)
fun endCall(pk: PublicKey) = av.callControl(contactByKey(pk), ToxavCallControl.CANCEL)
fun sendAudio(pk: PublicKey, pcm: ShortArray, channels: Int, samplingRate: Int) =
av.audioSendFrame(contactByKey(pk), pcm, pcm.size, channels, samplingRate)
}