android: standby duty-cycle в фоне — defer ping/reconnect/probe через standby_wait
standby.h/c: duty-cycle ACTIVE/SLEEP, standby_wait() будит в burst сразу, в SLEEP — до следующего burst; notify_network_activity для раннего пробуждения. Интеграция (по флагу UTUN_HAVE_STANDBY, десктоп без изменений): - conn_mgr: bg_ping/candidate_ping в SLEEP defer через standby_wait - route_connectivity: probe_node откладывается (PROBE_STATUS_DEFERRED) - topo_group_connect: пауза между попытками — активный режим 1с, фон standby_wait - jni_bridge/ConfigProvider/NetworkActivityMonitor: set_active + сетевая активность - chat_setting: standby_active_sec/sleep_sec/min_sleep_sec + юнит-тест test_standby (31 проверок)
This commit is contained in:
1 parent
1ec94de852
commit
b203a48063
26 files changed
+921
-14
No files matched your search
@@ -7,6 +7,7 @@ import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.*
|
||||
@@ -133,7 +134,7 @@ class MainActivity : ComponentActivity() {
|
||||
)
|
||||
}
|
||||
}
|
||||
SnackbarHost(snackbarHostState, Modifier.align(Alignment.BottomCenter))
|
||||
SnackbarHost(snackbarHostState, Modifier.align(Alignment.BottomCenter).navigationBarsPadding())
|
||||
}
|
||||
|
||||
/* Show join dialog after QR scan returns */
|
||||
|
||||
@@ -42,6 +42,9 @@ class ConfigProvider(private val context: Context) {
|
||||
private val chatCompressorMaxGainDb = intPreferencesKey("chat_compressor_max_gain_db")
|
||||
private val chatCompressorRiseRate = intPreferencesKey("chat_compressor_rise_rate")
|
||||
private val chatMediaDownloadMaxPeers = intPreferencesKey("chat_media_download_max_peers")
|
||||
private val chatStandbyActiveSec = intPreferencesKey("chat_standby_active_sec")
|
||||
private val chatStandbySleepSec = intPreferencesKey("chat_standby_sleep_sec")
|
||||
private val chatStandbyMinSleepSec = intPreferencesKey("chat_standby_min_sleep_sec")
|
||||
private val firstLaunchDone = booleanPreferencesKey("first_launch_done")
|
||||
private val connectionModeKey = stringPreferencesKey("connection_mode")
|
||||
|
||||
@@ -117,6 +120,9 @@ class ConfigProvider(private val context: Context) {
|
||||
key == "chat.compressor_max_gain_db" -> context.dataStore.data.first()[chatCompressorMaxGainDb] ?: 25
|
||||
key == "chat.compressor_rise_rate" -> context.dataStore.data.first()[chatCompressorRiseRate] ?: 10
|
||||
key == "chat.media_download_max_peers" -> context.dataStore.data.first()[chatMediaDownloadMaxPeers] ?: 3
|
||||
key == "chat.standby_active_sec" -> context.dataStore.data.first()[chatStandbyActiveSec] ?: 2
|
||||
key == "chat.standby_sleep_sec" -> context.dataStore.data.first()[chatStandbySleepSec] ?: 60
|
||||
key == "chat.standby_min_sleep_sec" -> context.dataStore.data.first()[chatStandbyMinSleepSec] ?: 15
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
@@ -144,6 +150,9 @@ class ConfigProvider(private val context: Context) {
|
||||
"chat.compressor_max_gain_db" -> prefs[chatCompressorMaxGainDb] = value.toIntOrNull() ?: 25
|
||||
"chat.compressor_rise_rate" -> prefs[chatCompressorRiseRate] = value.toIntOrNull() ?: 10
|
||||
"chat.media_download_max_peers" -> prefs[chatMediaDownloadMaxPeers] = value.toIntOrNull() ?: 3
|
||||
"chat.standby_active_sec" -> prefs[chatStandbyActiveSec] = value.toIntOrNull() ?: 2
|
||||
"chat.standby_sleep_sec" -> prefs[chatStandbySleepSec] = value.toIntOrNull() ?: 60
|
||||
"chat.standby_min_sleep_sec" -> prefs[chatStandbyMinSleepSec] = value.toIntOrNull() ?: 15
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,6 +345,9 @@ class ConfigProvider(private val context: Context) {
|
||||
appendLine("compressor_max_gain_db=$chatCompMaxGain")
|
||||
appendLine("compressor_rise_rate=$chatCompRiseRate")
|
||||
appendLine("media_download_max_peers=$chatDlMaxPeers")
|
||||
appendLine("standby_active_sec=${getInt("chat.standby_active_sec")}")
|
||||
appendLine("standby_sleep_sec=${getInt("chat.standby_sleep_sec")}")
|
||||
appendLine("standby_min_sleep_sec=${getInt("chat.standby_min_sleep_sec")}")
|
||||
appendLine()
|
||||
appendLine("[debug]")
|
||||
appendLine("console_level=info")
|
||||
|
||||
@@ -56,6 +56,9 @@ object NativeLib {
|
||||
/* ── App foreground/background (Android lifecycle) ── */
|
||||
fun setActive(active: Boolean) { nativeSetActive(active) }
|
||||
|
||||
/* ── External network activity (ConnectivityManager) ── */
|
||||
fun notifyNetworkActive() { nativeNotifyNetworkActive() }
|
||||
|
||||
fun sendMessage(channelId: String, text: String) { nativeSendMessage(channelId, text) }
|
||||
fun createChannel(name: String, channelId: String) { nativeCreateChannel(name, channelId) }
|
||||
fun connectNode(address: String, port: Int, pubkeyHex: String) { nativeConnectNode(address, port, pubkeyHex) }
|
||||
@@ -144,6 +147,7 @@ object NativeLib {
|
||||
private external fun nativeIsResponsive(): Boolean
|
||||
private external fun nativeIsRunning(): Boolean
|
||||
private external fun nativeSetActive(active: Boolean)
|
||||
private external fun nativeNotifyNetworkActive()
|
||||
|
||||
/* ── Channel members JNI ── */
|
||||
private external fun nativeGetChannelMembers(channelId: String): String?
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.utun.chat.data
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import com.utun.chat.ChatApplication
|
||||
|
||||
/**
|
||||
* Слушатель внешней сетевой активности (ConnectivityManager.OnNetworkActiveListener).
|
||||
*
|
||||
* Пробуждает standby-ядро досрочно: когда в фазе SLEEP прошёл min_sleep,
|
||||
* приход сетевой активности запускает следующий активный цикл раньше.
|
||||
*
|
||||
* В колбэке не делаем тяжёлую работу — только дёшевый JNI-вызов
|
||||
* NativeLib.notifyNetworkActive(), который постит событие на uasync-поток.
|
||||
*/
|
||||
object NetworkActivityMonitor {
|
||||
private var started = false
|
||||
|
||||
private val listener = ConnectivityManager.OnNetworkActiveListener {
|
||||
NativeLib.notifyNetworkActive()
|
||||
}
|
||||
|
||||
fun start() {
|
||||
if (started) return
|
||||
/* addDefaultNetworkActiveListener доступен с API 28 (P) */
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return
|
||||
val ctx = ChatApplication.instance.applicationContext
|
||||
val cm = ctx.getSystemService(ConnectivityManager::class.java) ?: return
|
||||
cm.addDefaultNetworkActiveListener(listener)
|
||||
started = true
|
||||
LogManager.addLog("INFO", "NET", "network activity listener started")
|
||||
/* Если сеть уже активна в момент запуска — сразу уведомляем */
|
||||
if (cm.isDefaultNetworkActive) {
|
||||
NativeLib.notifyNetworkActive()
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (!started) return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return
|
||||
val ctx = ChatApplication.instance.applicationContext
|
||||
val cm = ctx.getSystemService(ConnectivityManager::class.java) ?: return
|
||||
cm.removeDefaultNetworkActiveListener(listener)
|
||||
started = false
|
||||
LogManager.addLog("INFO", "NET", "network activity listener stopped")
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import com.utun.chat.ChatApplication
|
||||
import com.utun.chat.MainActivity
|
||||
import com.utun.chat.data.LogManager
|
||||
import com.utun.chat.data.NativeLib
|
||||
import com.utun.chat.data.NetworkActivityMonitor
|
||||
|
||||
class HeadlessService : Service() {
|
||||
override fun onCreate() {
|
||||
@@ -45,6 +46,7 @@ class HeadlessService : Service() {
|
||||
val ok = NativeLib.start(configText)
|
||||
if (ok) {
|
||||
LogManager.addLog("INFO", "SRV", "uTun core started")
|
||||
NetworkActivityMonitor.start()
|
||||
} else {
|
||||
LogManager.addLog("ERROR", "SRV", "uTun start failed")
|
||||
}
|
||||
@@ -52,6 +54,7 @@ class HeadlessService : Service() {
|
||||
|
||||
override fun onDestroy() {
|
||||
LogManager.addLog("INFO", "SRV", "uTun service stopping")
|
||||
NetworkActivityMonitor.stop()
|
||||
NativeLib.stop()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -99,6 +99,9 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
|
||||
var logFile by remember { mutableStateOf("") }
|
||||
var logUdpIp by remember { mutableStateOf("") }
|
||||
var logUdpPort by remember { mutableStateOf("") }
|
||||
var standbyActiveSec by remember { mutableStateOf("2") }
|
||||
var standbySleepSec by remember { mutableStateOf("60") }
|
||||
var standbyMinSleepSec by remember { mutableStateOf("15") }
|
||||
val pubKey by vm.pubKey.collectAsState()
|
||||
val generatingKeys by vm.generatingKeys.collectAsState()
|
||||
|
||||
@@ -124,6 +127,9 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
|
||||
logFile = provider.getString("debug.log_file") ?: "utun.log"
|
||||
logUdpIp = provider.getString("log_udp.ip") ?: "192.168.40.250"
|
||||
logUdpPort = provider.getInt("log_udp.port").toString().let { if (it == "0") "9999" else it }
|
||||
standbyActiveSec = provider.getInt("chat.standby_active_sec").toString()
|
||||
standbySleepSec = provider.getInt("chat.standby_sleep_sec").toString()
|
||||
standbyMinSleepSec = provider.getInt("chat.standby_min_sleep_sec").toString()
|
||||
val port = logUdpPort.toIntOrNull() ?: 0
|
||||
if (logUdpIp.isNotEmpty() && port > 0) NativeLib.setUdpLogTarget(logUdpIp, port)
|
||||
val stored = provider.getString("debug.categories") ?: ""
|
||||
@@ -301,6 +307,28 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
|
||||
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
|
||||
/* ── Standby (background duty-cycle) ── */
|
||||
Text("Standby", style = MaterialTheme.typography.titleMedium)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(standbyActiveSec,
|
||||
{ v -> standbyActiveSec = v; val nb = v.toIntOrNull(); if (nb != null && nb in 1..3600) {
|
||||
scope.launch { provider.setValue("chat.standby_active_sec", v) }; NativeLib.setChatSetting("standby_active_sec", v) } },
|
||||
label = { Text("Active (sec)") }, modifier = Modifier.weight(1f).heightIn(min = 40.dp),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), singleLine = true)
|
||||
OutlinedTextField(standbySleepSec,
|
||||
{ v -> standbySleepSec = v; val nb = v.toIntOrNull(); if (nb != null && nb in 1..86400) {
|
||||
scope.launch { provider.setValue("chat.standby_sleep_sec", v) }; NativeLib.setChatSetting("standby_sleep_sec", v) } },
|
||||
label = { Text("Sleep (sec)") }, modifier = Modifier.weight(1f).heightIn(min = 40.dp),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), singleLine = true)
|
||||
}
|
||||
OutlinedTextField(standbyMinSleepSec,
|
||||
{ v -> standbyMinSleepSec = v; val nb = v.toIntOrNull(); if (nb != null && nb in 1..86400) {
|
||||
scope.launch { provider.setValue("chat.standby_min_sleep_sec", v) }; NativeLib.setChatSetting("standby_min_sleep_sec", v) } },
|
||||
label = { Text("Min sleep (sec)") }, modifier = Modifier.heightIn(min = 40.dp),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), singleLine = true)
|
||||
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
|
||||
/* Debug */
|
||||
Text("Debug", style = MaterialTheme.typography.titleMedium)
|
||||
DebugLevelDropdown(label = "Global Level", current = debugLevel.uppercase(), onSelect = setDebug)
|
||||
|
||||
Reference in new issue
Block a user