Merge branch 'fix/accept-actually-answers' into 'main'
Accept answers the call, and a call that ended goes away See merge request paulhitt/flamenet-messenger-android!114
This commit was merged in pull request #141.
This commit is contained in:
@@ -102,6 +102,35 @@ class CallManager(
|
|||||||
*/
|
*/
|
||||||
private var ringGuard: Runnable? = null
|
private var ringGuard: Runnable? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ends a call whose far end stopped answering the media.
|
||||||
|
*
|
||||||
|
* `call-end` is the polite path and usually arrives first. When it does not,
|
||||||
|
* because it was lost or the app was somewhere it could not be read, this is
|
||||||
|
* what notices. Short, because a call whose audio has stopped is over
|
||||||
|
* whatever the signalling says.
|
||||||
|
*/
|
||||||
|
private var goneGuard: Runnable? = null
|
||||||
|
|
||||||
|
private fun armGoneGuard() {
|
||||||
|
if (goneGuard != null) return
|
||||||
|
val guard =
|
||||||
|
Runnable {
|
||||||
|
goneGuard = null
|
||||||
|
if (state == State.ACTIVE || state == State.CONNECTING) {
|
||||||
|
android.util.Log.i("fnmsg-call", "far end gone, ending the call")
|
||||||
|
hangUp(notifyPeer = true, reason = "timeout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
goneGuard = guard
|
||||||
|
main.postDelayed(guard, GONE_TIMEOUT_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelGoneGuard() {
|
||||||
|
goneGuard?.let { main.removeCallbacks(it) }
|
||||||
|
goneGuard = null
|
||||||
|
}
|
||||||
|
|
||||||
private fun armRingGuard() {
|
private fun armRingGuard() {
|
||||||
cancelRingGuard()
|
cancelRingGuard()
|
||||||
val guard =
|
val guard =
|
||||||
@@ -258,11 +287,22 @@ class CallManager(
|
|||||||
when (newState) {
|
when (newState) {
|
||||||
PeerConnection.IceConnectionState.CONNECTED,
|
PeerConnection.IceConnectionState.CONNECTED,
|
||||||
PeerConnection.IceConnectionState.COMPLETED,
|
PeerConnection.IceConnectionState.COMPLETED,
|
||||||
-> state = State.ACTIVE
|
-> {
|
||||||
|
cancelGoneGuard()
|
||||||
|
state = State.ACTIVE
|
||||||
|
}
|
||||||
|
|
||||||
// CLOSED is what our OWN close() produces.
|
// CLOSED is what our OWN close() produces.
|
||||||
PeerConnection.IceConnectionState.CLOSED -> Unit
|
PeerConnection.IceConnectionState.CLOSED -> Unit
|
||||||
|
|
||||||
|
// The far end went away. Transient by definition,
|
||||||
|
// WebRTC recovers from it often, so this is a grace
|
||||||
|
// rather than a hang-up: if it comes back the timer
|
||||||
|
// is cancelled below. Without it a lost `call-end`
|
||||||
|
// left the screen up until the dial guard gave up
|
||||||
|
// three quarters of a minute later.
|
||||||
|
PeerConnection.IceConnectionState.DISCONNECTED -> armGoneGuard()
|
||||||
|
|
||||||
PeerConnection.IceConnectionState.FAILED -> hangUp(notifyPeer = true)
|
PeerConnection.IceConnectionState.FAILED -> hangUp(notifyPeer = true)
|
||||||
|
|
||||||
else -> Unit
|
else -> Unit
|
||||||
@@ -584,6 +624,8 @@ class CallManager(
|
|||||||
dialGuard = null
|
dialGuard = null
|
||||||
connectGuard?.let { main.removeCallbacks(it) }
|
connectGuard?.let { main.removeCallbacks(it) }
|
||||||
connectGuard = null
|
connectGuard = null
|
||||||
|
cancelGoneGuard()
|
||||||
|
cancelRingGuard()
|
||||||
val active = current
|
val active = current
|
||||||
if (notifyPeer && active != null) {
|
if (notifyPeer && active != null) {
|
||||||
// Best effort.
|
// Best effort.
|
||||||
@@ -646,6 +688,12 @@ class CallManager(
|
|||||||
* only when their hang-up never reached us.
|
* only when their hang-up never reached us.
|
||||||
*/
|
*/
|
||||||
const val RING_TIMEOUT_MS = 60_000L
|
const val RING_TIMEOUT_MS = 60_000L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How long a connected call may stay disconnected before it is over.
|
||||||
|
* Long enough for a real blip, short enough not to be a stuck screen.
|
||||||
|
*/
|
||||||
|
const val GONE_TIMEOUT_MS = 8_000L
|
||||||
const val CONNECT_TIMEOUT_MS = 30_000L
|
const val CONNECT_TIMEOUT_MS = 30_000L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package io.flamenet.messenger.app
|
package io.flamenet.messenger.app
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.activity.viewModels
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@@ -152,10 +154,36 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val model: MessengerViewModel by viewModels()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept, pressed on the notification, has to answer the call.
|
||||||
|
*
|
||||||
|
* The intent has carried `EXTRA_ANSWER_CALL` since the notification was
|
||||||
|
* written and nothing read it, so pressing Accept only opened the app and
|
||||||
|
* left the answering to a polling tick that refuses a stale offer. The call
|
||||||
|
* then looked like it had dropped the moment you took it.
|
||||||
|
*/
|
||||||
|
private fun answerIfAsked(intent: Intent?) {
|
||||||
|
if (intent?.getBooleanExtra(MessengerService.EXTRA_ANSWER_CALL, false) == true) {
|
||||||
|
intent.removeExtra(MessengerService.EXTRA_ANSWER_CALL)
|
||||||
|
model.answerParkedCall()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
setIntent(intent)
|
||||||
|
answerIfAsked(intent)
|
||||||
|
}
|
||||||
|
|
||||||
// The handover: exactly one poller at a time.
|
// The handover: exactly one poller at a time.
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
MessengerService.stop(this)
|
MessengerService.stop(this)
|
||||||
|
// Also on the way in: a cold start from the notification arrives here
|
||||||
|
// rather than through onNewIntent.
|
||||||
|
answerIfAsked(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
|
|||||||
@@ -310,6 +310,12 @@ class MessengerService : Service() {
|
|||||||
// accident while reaching for the torch.
|
// accident while reaching for the torch.
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setAutoCancel(false)
|
.setAutoCancel(false)
|
||||||
|
// Goes away by itself when the offer it is showing has expired.
|
||||||
|
// `take` refuses a parked offer older than the caller's own
|
||||||
|
// patience, so a notification that outlived that was a button
|
||||||
|
// which opened the app onto nothing and read as a dropped call.
|
||||||
|
// Android cancels this without needing the service to notice.
|
||||||
|
.setTimeoutAfter(CallManager.DIAL_TIMEOUT_MS)
|
||||||
.setFullScreenIntent(full, true)
|
.setFullScreenIntent(full, true)
|
||||||
.setStyle(NotificationCompat.CallStyle.forIncomingCall(caller, decline, full))
|
.setStyle(NotificationCompat.CallStyle.forIncomingCall(caller, decline, full))
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -2334,6 +2334,16 @@ class MessengerViewModel(
|
|||||||
onStateChanged = { state ->
|
onStateChanged = { state ->
|
||||||
// Filed once, at the end.
|
// Filed once, at the end.
|
||||||
if (state == CallManager.State.ENDED || state == CallManager.State.IDLE) {
|
if (state == CallManager.State.ENDED || state == CallManager.State.IDLE) {
|
||||||
|
// The ring notification belongs to the service, and the
|
||||||
|
// manager knows nothing of it. A ring that ended here,
|
||||||
|
// by the caller hanging up or by the guard giving up,
|
||||||
|
// used to leave it standing: an Answer button for a
|
||||||
|
// call that no longer existed. Seen on a device.
|
||||||
|
runCatching {
|
||||||
|
androidx.core.app.NotificationManagerCompat
|
||||||
|
.from(getApplication())
|
||||||
|
.cancel(3)
|
||||||
|
}
|
||||||
calls?.current?.let { active ->
|
calls?.current?.let { active ->
|
||||||
if (!callFiled) {
|
if (!callFiled) {
|
||||||
callFiled = true
|
callFiled = true
|
||||||
@@ -3430,7 +3440,49 @@ class MessengerViewModel(
|
|||||||
* arrived, `CallManager` moved to RINGING, and the only notification posted
|
* arrived, `CallManager` moved to RINGING, and the only notification posted
|
||||||
* was the service's own "running" notice.
|
* was the service's own "running" notice.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Answer the call the notification is showing, now.
|
||||||
|
*
|
||||||
|
* The notification's Accept button has always carried
|
||||||
|
* `EXTRA_ANSWER_CALL`, and nothing ever read it. Tapping Accept therefore
|
||||||
|
* did what any other tap did: it opened the app. Answering was left to the
|
||||||
|
* next turn of the polling loop, which refuses a parked offer older than
|
||||||
|
* the caller's patience, so a call answered a little late opened the app
|
||||||
|
* onto nothing and looked like it had dropped.
|
||||||
|
*/
|
||||||
|
fun answerParkedCall() {
|
||||||
|
val parked = PendingIncomingCall.take() ?: return
|
||||||
|
runCatching {
|
||||||
|
androidx.core.app.NotificationManagerCompat
|
||||||
|
.from(getApplication())
|
||||||
|
.cancel(3)
|
||||||
|
}
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val manager = callManager()
|
||||||
|
val turn = session?.turnCredentials()
|
||||||
|
manager.onSignal(parked.fromUser, parked.signal, turn, parked.at)
|
||||||
|
// The offer only sets it ringing; this is the answer itself.
|
||||||
|
manager.accept(turn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun pausePolling() {
|
fun pausePolling() {
|
||||||
|
// Never while a call is in flight. This loop is the only thing that
|
||||||
|
// routes `call-end` into the manager that owns the call screen; the
|
||||||
|
// service polls too, but it turns offers into notifications and knows
|
||||||
|
// nothing about a call already up. Stopping here mid-call left the far
|
||||||
|
// end's hang-up with nowhere to land, so the screen sat there offering
|
||||||
|
// to answer a call that had gone, until the dial guard gave up 45
|
||||||
|
// seconds later. Seen on a device.
|
||||||
|
when (calls?.state) {
|
||||||
|
CallManager.State.DIALLING,
|
||||||
|
CallManager.State.RINGING,
|
||||||
|
CallManager.State.CONNECTING,
|
||||||
|
CallManager.State.ACTIVE,
|
||||||
|
-> return
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
polling?.cancel()
|
polling?.cancel()
|
||||||
polling = null
|
polling = null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user