Own pending radio starts and diagnose every signaling queue refusal
This commit is contained in:
1 parent
16f52c0136
commit
b24bee7a89
1 file changed
+61
-12
@@ -9,6 +9,7 @@ import com.utun.chat.ChatApplication
|
||||
import com.utun.chat.R
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -24,8 +25,10 @@ data class RadioCallState(
|
||||
)
|
||||
data class RadioCallActivation(val id: Long, val channelId: String)
|
||||
|
||||
/** Живые события, без replay в UI. Ядро владеет deadline, приложение — уведомлением.
|
||||
* Принятие сначала поднимает видимую Activity и обычную рацию, затем отправляет ACCEPT. */
|
||||
/** На main хранит живое приглашение рации; StateFlow повторяет текущий снимок после пересоздания UI.
|
||||
* Ядро владеет deadline/сигналингом, приложение — оповещением и запросом async-включения обычной рации.
|
||||
* Входящий ACCEPT отправляется только после activated(success=true) из видимой Activity.
|
||||
* Завершение приглашения не выключает обычную рацию. Отказ очереди логируется и завершает локальную попытку. */
|
||||
object RadioCallController {
|
||||
private const val START = 1
|
||||
private const val READY = 2
|
||||
@@ -35,6 +38,10 @@ object RadioCallController {
|
||||
private const val FAIL = 6
|
||||
private const val BUSY = 7
|
||||
private const val CONTINUE = 8
|
||||
private const val RESULT_NOTIFICATION_ID = 1004 // Отдельно от incoming, foreground-сервисов и результата P2P-звонка.
|
||||
private data class PendingStart(val id: Long, val group: Long, val peer: Long)
|
||||
private var pendingStart: PendingStart? = null // START принят приложением, первое событие ядра ещё не получено.
|
||||
private var clearJob: Job? = null // Только задержка удаления ENDED-карточки; deadline вызова принадлежит ядру.
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
private val _state = MutableStateFlow<RadioCallState?>(null)
|
||||
val state: StateFlow<RadioCallState?> = _state
|
||||
@@ -43,7 +50,7 @@ object RadioCallController {
|
||||
private var visible = false
|
||||
private var answerOnVisible = 0L
|
||||
private var ready = false
|
||||
fun busy(): Boolean = _state.value?.phase?.let { it != RadioCallPhase.ENDED } == true
|
||||
fun busy(): Boolean = pendingStart != null || _state.value?.phase?.let { it != RadioCallPhase.ENDED } == true
|
||||
|
||||
fun start(channel: String, peer: Long) {
|
||||
checkMain()
|
||||
@@ -51,10 +58,16 @@ object RadioCallController {
|
||||
LogManager.addLog("WARN", "radio", "Call start unavailable core=$ready busy=${busy()}")
|
||||
showOutcome(3); return
|
||||
}
|
||||
val group = channel.toULongOrNull()?.toLong() ?: return
|
||||
val group = channel.toULongOrNull()?.toLong()
|
||||
if (group == null || group == 0L || peer == 0L) {
|
||||
LogManager.addLog("ERROR", "radio", "Invalid call target channel=$channel peer=$peer")
|
||||
showOutcome(5); return
|
||||
}
|
||||
var id = java.security.SecureRandom().nextLong()
|
||||
if (id == 0L) id = 1
|
||||
if (!NativeLib.radioCall(id, START, group, peer)) showOutcome(5)
|
||||
pendingStart = PendingStart(id, group, peer)
|
||||
syncActivity()
|
||||
if (!command(id, START, group, peer)) { pendingStart = null; syncActivity(); showOutcome(5) }
|
||||
}
|
||||
|
||||
fun setVisible(value: Boolean) {
|
||||
@@ -68,6 +81,7 @@ object RadioCallController {
|
||||
|
||||
/** Старый PendingIntent не принимает новый вызов и не восстанавливает завершённый. */
|
||||
fun notificationIntent(intent: Intent?) {
|
||||
checkMain()
|
||||
if (intent?.getBooleanExtra(IncomingCallManager.EXTRA_RADIO_CALL, false) != true) return
|
||||
val id = intent.getLongExtra(IncomingCallManager.EXTRA_CALL_ID, 0)
|
||||
if (!intent.getBooleanExtra(IncomingCallManager.EXTRA_ANSWER_RADIO, false)) return
|
||||
@@ -86,7 +100,11 @@ object RadioCallController {
|
||||
}
|
||||
return
|
||||
}
|
||||
if (StreamController.blocksCapture()) { NativeLib.radioCall(st.id, FAIL); return }
|
||||
if (StreamController.blocksCapture()) {
|
||||
LogManager.addLog("WARN", "radio", "Call capture blocked by stream id=${st.id}")
|
||||
command(st.id, FAIL)
|
||||
finish(st, 7); return
|
||||
}
|
||||
IncomingCallManager.stopRinging()
|
||||
_state.value = st.copy(phase = RadioCallPhase.PREPARING)
|
||||
_activation.value = RadioCallActivation(st.id, st.channelId)
|
||||
@@ -100,7 +118,7 @@ object RadioCallController {
|
||||
_activation.value = null
|
||||
LogManager.addLog(if (success) "INFO" else "ERROR", "radio", "Call activation id=$id success=$success incoming=${st.incoming}")
|
||||
if (st.incoming) {
|
||||
if (!NativeLib.radioCall(id, if (success) ACCEPT else FAIL)) finish(st, 7)
|
||||
if (!command(id, if (success) ACCEPT else FAIL)) finish(st, 7)
|
||||
} else if (success) clear() else finish(st, 7)
|
||||
}
|
||||
|
||||
@@ -108,14 +126,17 @@ object RadioCallController {
|
||||
checkMain()
|
||||
val st = _state.value ?: return
|
||||
if (id != st.id || st.phase == RadioCallPhase.ENDED) return
|
||||
NativeLib.radioCall(id, if (st.incoming && st.phase == RadioCallPhase.INCOMING) DECLINE else CANCEL)
|
||||
command(id, if (st.incoming && st.phase == RadioCallPhase.INCOMING) DECLINE else CANCEL)
|
||||
finish(st, if (st.incoming && st.phase == RadioCallPhase.INCOMING) 2 else 4)
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
checkMain()
|
||||
ready = false
|
||||
if (busy()) end()
|
||||
if (NativeLib.isReady()) {
|
||||
if (_state.value?.phase?.let { it != RadioCallPhase.ENDED } == true) end()
|
||||
pendingStart?.let { command(it.id, CANCEL) }
|
||||
}
|
||||
clear()
|
||||
}
|
||||
|
||||
@@ -187,7 +208,7 @@ object RadioCallController {
|
||||
if (st.id != id || st.phase != RadioCallPhase.WAIT_DECISION) return
|
||||
_state.value = st.copy(phase = RadioCallPhase.OUTGOING)
|
||||
LogManager.addLog("INFO", "radio", "Caller overrides phone busy id=$id")
|
||||
if (!NativeLib.radioCall(id, CONTINUE)) { finish(st, 5); return }
|
||||
if (!command(id, CONTINUE)) { finish(st, 5); return }
|
||||
CallRingback.startConnecting()
|
||||
}
|
||||
|
||||
@@ -214,7 +235,8 @@ object RadioCallController {
|
||||
val ctx = ChatApplication.instance
|
||||
val text = AppLanguage.text(reasonText(reason))
|
||||
if (visible) { Toast.makeText(ctx, text, Toast.LENGTH_LONG).show(); return }
|
||||
requireNotNull(ctx.getSystemService(NotificationManager::class.java)) { "notification manager unavailable" }.notify(1002,
|
||||
IncomingCallManager.init()
|
||||
requireNotNull(ctx.getSystemService(NotificationManager::class.java)) { "notification manager unavailable" }.notify(RESULT_NOTIFICATION_ID,
|
||||
NotificationCompat.Builder(ctx, IncomingCallManager.CHANNEL_CALL)
|
||||
.setSmallIcon(android.R.drawable.ic_dialog_info).setContentTitle(AppLanguage.text(R.string.radio_call))
|
||||
.setContentText(text).setAutoCancel(true).build())
|
||||
@@ -223,5 +245,32 @@ object RadioCallController {
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkMain() = check(Looper.myLooper() == Looper.getMainLooper()) { "RadioCallController requires main thread" }
|
||||
/** Отказ/исключение JNI — отказ постановки команды, а true ещё не означает успех сигналинга. */
|
||||
private fun command(id: Long, action: Int, group: Long = 0, peer: Long = 0): Boolean {
|
||||
try {
|
||||
if (NativeLib.radioCall(id, action, group, peer)) {
|
||||
LogManager.addLog("DEBUG", "radio", "Call command queued id=$id action=$action group=$group peer=$peer")
|
||||
return true
|
||||
}
|
||||
LogManager.addLog("ERROR", "radio", "Call command rejected id=$id action=$action group=$group peer=$peer")
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "radio", "Call command failed id=$id action=$action group=$group peer=$peer: $e")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** CoreActivity сохраняет намерение до подтверждения очереди; его отказ не прерывает сигналинг/очистку UI. */
|
||||
private fun syncActivity() {
|
||||
val active = busy()
|
||||
try { CoreActivity.radioCall(active) } catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "radio", "Call activity update failed active=$active id=${_state.value?.id} " +
|
||||
"pending=${pendingStart?.id}; retained for next sync: $e")
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkMain() = check(Looper.myLooper() == Looper.getMainLooper()) {
|
||||
val message = "RadioCallController requires main thread: caller=${Thread.currentThread().name}"
|
||||
LogManager.addLog("ERROR", "radio", message)
|
||||
message
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user