srctree

Robin Linden parent 89e4b786 26b3c3d0
Add support for incoming calls

inlinesplit
atox/src/main/kotlin/ReplyReceiver.kt added: 144, removed: 35, total 109
@@ -7,14 +7,19 @@ import androidx.core.app.RemoteInput
import javax.inject.Inject
import ltd.evilcorp.atox.ui.NotificationHelper
import ltd.evilcorp.core.vo.Contact
import ltd.evilcorp.domain.feature.CallManager
import ltd.evilcorp.domain.feature.ChatManager
import ltd.evilcorp.domain.tox.PublicKey
import ltd.evilcorp.domain.tox.Tox
 
const val KEY_TEXT_REPLY = "text_reply"
const val KEY_CALL = "accept_or_reject_call"
const val KEY_CONTACT_PK = "contact_pk"
 
class ReplyReceiver : BroadcastReceiver() {
@Inject
lateinit var callManager: CallManager
 
@Inject
lateinit var chatManager: ChatManager
 
@@ -26,10 +31,24 @@ class ReplyReceiver : BroadcastReceiver() {
 
override fun onReceive(context: Context, intent: Intent) {
(context.applicationContext as App).component.inject(this)
val results = RemoteInput.getResultsFromIntent(intent) ?: return
val input = results.getCharSequence(KEY_TEXT_REPLY)?.toString() ?: return
val pk = intent.getStringExtra(KEY_CONTACT_PK) ?: return
chatManager.sendMessage(PublicKey(pk), input)
notificationHelper.showMessageNotification(Contact(pk, tox.getName()), input, outgoing = true)
 
RemoteInput.getResultsFromIntent(intent)?.let { results ->
results.getCharSequence(KEY_TEXT_REPLY)?.toString()?.let { input ->
val pk = intent.getStringExtra(KEY_CONTACT_PK) ?: return
chatManager.sendMessage(PublicKey(pk), input)
notificationHelper.showMessageNotification(Contact(pk, tox.getName()), input, outgoing = true)
}
}
 
intent.getStringExtra(KEY_CALL)?.let { callChoice ->
val pk = intent.getStringExtra(KEY_CONTACT_PK) ?: return
if (callChoice == "accept") {
callManager.answerCall(PublicKey(pk))
notificationHelper.showOngoingCallNotification(Contact(pk, tox.getName()))
} else if (callChoice == "reject") {
callManager.endCall(PublicKey(pk))
notificationHelper.dismissCallNotification(Contact(pk))
}
}
}
}
 
atox/src/main/kotlin/tox/EventListenerCallbacks.kt added: 144, removed: 35, total 109
@@ -166,7 +166,7 @@ class EventListenerCallbacks @Inject constructor(
fun setUp(listener: ToxAvEventListener) = with(listener) {
callHandler = { pk, audioEnabled, videoEnabled ->
Log.e(TAG, "call ${pk.take(8)} $audioEnabled $videoEnabled")
tox.endCall(PublicKey(pk))
notificationHelper.showPendingCallNotification(contactByPublicKey(pk))
}
 
callStateHandler = { pk, callState ->
 
atox/src/main/kotlin/ui/NotificationHelper.kt added: 144, removed: 35, total 109
@@ -24,6 +24,7 @@ import com.squareup.picasso.Picasso
import com.squareup.picasso.Transformation
import javax.inject.Inject
import javax.inject.Singleton
import ltd.evilcorp.atox.KEY_CALL
import ltd.evilcorp.atox.KEY_CONTACT_PK
import ltd.evilcorp.atox.KEY_TEXT_REPLY
import ltd.evilcorp.atox.R
@@ -177,7 +178,7 @@ class NotificationHelper @Inject constructor(
fun dismissCallNotification(contact: Contact) =
notifier.cancel(contact.publicKey.hashCode() + CALL.hashCode())
 
fun showCallNotification(contact: Contact) {
fun showOngoingCallNotification(contact: Contact) {
val notificationBuilder = NotificationCompat.Builder(context, CALL)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setSmallIcon(android.R.drawable.ic_menu_call)
@@ -202,4 +203,54 @@ class NotificationHelper @Inject constructor(
 
notifier.notify(contact.publicKey.hashCode() + CALL.hashCode(), notificationBuilder.build())
}
 
fun showPendingCallNotification(c: Contact) {
val notificationBuilder = NotificationCompat.Builder(context, CALL)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setSmallIcon(android.R.drawable.ic_menu_call)
.setContentTitle(context.getString(R.string.incoming_call))
.setContentText(context.getString(R.string.incoming_call_from, c.name))
.setContentIntent(
NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.chatFragment)
.setArguments(bundleOf(CONTACT_PUBLIC_KEY to c.publicKey))
.createPendingIntent()
)
.addAction(
NotificationCompat.Action
.Builder(
IconCompat.createWithResource(context, R.drawable.ic_call),
context.getString(R.string.accept),
PendingIntent.getBroadcast(
context,
"${c.publicKey}_accept_call".hashCode(),
Intent(context, ReplyReceiver::class.java)
.putExtra(KEY_CONTACT_PK, c.publicKey)
.putExtra(KEY_CALL, "accept"),
PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_CALL)
.build()
)
.addAction(
NotificationCompat.Action
.Builder(
IconCompat.createWithResource(context, R.drawable.ic_not_interested),
context.getString(R.string.reject),
PendingIntent.getBroadcast(
context,
"${c.publicKey}_reject_call".hashCode(),
Intent(context, ReplyReceiver::class.java)
.putExtra(KEY_CONTACT_PK, c.publicKey)
.putExtra(KEY_CALL, "reject"),
PendingIntent.FLAG_UPDATE_CURRENT
)
)
.build()
)
 
notifier.notify(c.publicKey.hashCode() + CALL.hashCode(), notificationBuilder.build())
}
}
 
atox/src/main/kotlin/ui/call/CallViewModel.kt added: 144, removed: 35, total 109
@@ -31,7 +31,7 @@ class CallViewModel @Inject constructor(
 
fun startCall(): Boolean {
if (callManager.startCall(publicKey)) {
launch { notificationHelper.showCallNotification(contactManager.get(publicKey).first()) }
launch { notificationHelper.showOngoingCallNotification(contactManager.get(publicKey).first()) }
return true
}
 
 
filename was Deleted added: 144, removed: 35, total 109
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M20.01,15.38c-1.23,0 -2.42,-0.2 -3.53,-0.56 -0.35,-0.12 -0.74,-0.03 -1.01,0.24l-1.57,1.97c-2.83,-1.35 -5.48,-3.9 -6.89,-6.83l1.95,-1.66c0.27,-0.28 0.35,-0.67 0.24,-1.02 -0.37,-1.11 -0.56,-2.3 -0.56,-3.53 0,-0.54 -0.45,-0.99 -0.99,-0.99H4.19C3.65,3 3,3.24 3,3.99 3,13.28 10.73,21 20.01,21c0.71,0 0.99,-0.63 0.99,-1.18v-3.45c0,-0.54 -0.45,-0.99 -0.99,-0.99z"/>
</vector>
 
filename was Deleted added: 144, removed: 35, total 109
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8 0,-1.85 0.63,-3.55 1.69,-4.9L16.9,18.31C15.55,19.37 13.85,20 12,20zM18.31,16.9L7.1,5.69C8.45,4.63 10.15,4 12,4c4.42,0 8,3.58 8,8 0,1.85 -0.63,3.55 -1.69,4.9z"/>
</vector>
 
atox/src/main/res/values/strings.xml added: 144, removed: 35, total 109
@@ -150,4 +150,6 @@
<string name="saved">Saved</string>
<string name="delete_message">Delete message</string>
<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>
</resources>
No newline at end of file
 
domain/src/main/kotlin/feature/CallManager.kt added: 144, removed: 35, total 109
@@ -36,25 +36,19 @@ class CallManager @Inject constructor(
 
tox.startCall(publicKey)
_inCall.value = CallState.InCall(publicKey)
startAudioSender(recorder, publicKey)
return true
}
 
launch {
recorder.start()
while (inCall.value is CallState.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()
recorder.release()
fun answerCall(publicKey: PublicKey): Boolean {
val recorder = AudioCapture(48_000, 1)
if (!recorder.isOk()) {
return false
}
 
tox.answerCall(publicKey)
_inCall.value = CallState.InCall(publicKey)
startAudioSender(recorder, publicKey)
return true
}
 
@@ -68,4 +62,25 @@ class CallManager @Inject constructor(
}
}
}
 
private fun startAudioSender(recorder: AudioCapture, to: PublicKey) {
launch {
recorder.start()
while (inCall.value is CallState.InCall) {
val start = System.currentTimeMillis()
val audioFrame = recorder.read()
try {
tox.sendAudio(to, 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()
recorder.release()
}
}
}
 
domain/src/main/kotlin/tox/Tox.kt added: 144, removed: 35, total 109
@@ -164,6 +164,7 @@ class Tox @Inject constructor(
 
// ToxAv, probably move these.
fun startCall(pk: PublicKey) = tox.startCall(pk)
fun answerCall(pk: PublicKey) = tox.answerCall(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: 144, removed: 35, total 109
@@ -144,6 +144,7 @@ class ToxWrapper(
 
// ToxAv, probably move these.
fun startCall(pk: PublicKey) = av.call(contactByKey(pk), 128, 0)
fun answerCall(pk: PublicKey) = av.answer(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)