refactor: CommandClient & Connections

This commit is contained in:
世界
2026-01-15 08:16:35 +08:00
parent cd0ae262f1
commit b327532ddb
9 changed files with 255 additions and 172 deletions

View File

@@ -211,6 +211,7 @@ dependencies {
// API 23+ dependencies (play/other) // API 23+ dependencies (play/other)
"playImplementation"("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion23") "playImplementation"("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion23")
"playImplementation"("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion23") "playImplementation"("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion23")
"playImplementation"("androidx.lifecycle:lifecycle-process:$lifecycleVersion23")
"playImplementation"("androidx.room:room-runtime:$roomVersion23") "playImplementation"("androidx.room:room-runtime:$roomVersion23")
"playImplementation"("androidx.work:work-runtime-ktx:$workVersion23") "playImplementation"("androidx.work:work-runtime-ktx:$workVersion23")
"playImplementation"("androidx.camera:camera-view:$cameraVersion23") "playImplementation"("androidx.camera:camera-view:$cameraVersion23")
@@ -222,6 +223,7 @@ dependencies {
"otherImplementation"("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion23") "otherImplementation"("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion23")
"otherImplementation"("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion23") "otherImplementation"("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion23")
"otherImplementation"("androidx.lifecycle:lifecycle-process:$lifecycleVersion23")
"otherImplementation"("androidx.room:room-runtime:$roomVersion23") "otherImplementation"("androidx.room:room-runtime:$roomVersion23")
"otherImplementation"("androidx.work:work-runtime-ktx:$workVersion23") "otherImplementation"("androidx.work:work-runtime-ktx:$workVersion23")
"otherImplementation"("androidx.camera:camera-view:$cameraVersion23") "otherImplementation"("androidx.camera:camera-view:$cameraVersion23")
@@ -233,6 +235,7 @@ dependencies {
// API 21 dependencies (otherLegacy) // API 21 dependencies (otherLegacy)
"otherLegacyImplementation"("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion21") "otherLegacyImplementation"("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion21")
"otherLegacyImplementation"("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion21") "otherLegacyImplementation"("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion21")
"otherLegacyImplementation"("androidx.lifecycle:lifecycle-process:$lifecycleVersion21")
"otherLegacyImplementation"("androidx.room:room-runtime:$roomVersion21") "otherLegacyImplementation"("androidx.room:room-runtime:$roomVersion21")
"otherLegacyImplementation"("androidx.work:work-runtime-ktx:$workVersion21") "otherLegacyImplementation"("androidx.work:work-runtime-ktx:$workVersion21")
"otherLegacyImplementation"("androidx.camera:camera-view:$cameraVersion21") "otherLegacyImplementation"("androidx.camera:camera-view:$cameraVersion21")

View File

@@ -61,6 +61,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -541,14 +542,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
val connectionsViewModel: ConnectionsViewModel? = val connectionsViewModel: ConnectionsViewModel? =
if (isConnectionsRoute) { if (isConnectionsRoute) {
viewModel( viewModel()
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return ConnectionsViewModel(dashboardViewModel.commandClient) as T
}
}
)
} else { } else {
null null
} }
@@ -973,18 +967,21 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
// Connections ModalBottomSheet // Connections ModalBottomSheet
if (showConnectionsSheet && !useNavigationRail) { if (showConnectionsSheet && !useNavigationRail) {
val connectionsSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val connectionsSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val connectionsViewModel: ConnectionsViewModel = viewModel( val connectionsViewModel: ConnectionsViewModel = viewModel()
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return ConnectionsViewModel(dashboardViewModel.commandClient) as T
}
}
)
val connectionsUiState by connectionsViewModel.uiState.collectAsState() val connectionsUiState by connectionsViewModel.uiState.collectAsState()
var selectedConnectionId by remember { mutableStateOf<String?>(null) } var selectedConnectionId by remember { mutableStateOf<String?>(null) }
val selectedConnection = connectionsUiState.allConnections.find { it.id == selectedConnectionId } val selectedConnection = connectionsUiState.allConnections.find { it.id == selectedConnectionId }
LaunchedEffect(Unit) {
connectionsViewModel.setVisible(true)
}
DisposableEffect(Unit) {
onDispose {
connectionsViewModel.setVisible(false)
}
}
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { onDismissRequest = {
showConnectionsSheet = false showConnectionsSheet = false

View File

@@ -43,6 +43,7 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -334,6 +335,16 @@ fun ConnectionsScreen(
) { ) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) {
viewModel.setVisible(true)
}
DisposableEffect(Unit) {
onDispose {
viewModel.setVisible(false)
}
}
LaunchedEffect(serviceStatus) { LaunchedEffect(serviceStatus) {
viewModel.updateServiceStatus(serviceStatus) viewModel.updateServiceStatus(serviceStatus)
} }

View File

@@ -1,6 +1,7 @@
package io.nekohasekai.sfa.compose.screen.connections package io.nekohasekai.sfa.compose.screen.connections
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.nekohasekai.libbox.ConnectionEvents
import io.nekohasekai.libbox.Connections import io.nekohasekai.libbox.Connections
import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.Libbox
import io.nekohasekai.sfa.compose.base.BaseViewModel import io.nekohasekai.sfa.compose.base.BaseViewModel
@@ -10,14 +11,16 @@ import io.nekohasekai.sfa.ktx.toList
import io.nekohasekai.sfa.compose.model.Connection import io.nekohasekai.sfa.compose.model.Connection
import io.nekohasekai.sfa.compose.model.ConnectionSort import io.nekohasekai.sfa.compose.model.ConnectionSort
import io.nekohasekai.sfa.compose.model.ConnectionStateFilter import io.nekohasekai.sfa.compose.model.ConnectionStateFilter
import io.nekohasekai.sfa.utils.AppLifecycleObserver
import io.nekohasekai.sfa.utils.CommandClient import io.nekohasekai.sfa.utils.CommandClient
import java.util.concurrent.atomic.AtomicLong
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.isActive import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
data class ConnectionsUiState( data class ConnectionsUiState(
@@ -35,70 +38,62 @@ sealed class ConnectionsEvent : ScreenEvent {
data object AllConnectionsClosed : ConnectionsEvent() data object AllConnectionsClosed : ConnectionsEvent()
} }
class ConnectionsViewModel( class ConnectionsViewModel : BaseViewModel<ConnectionsUiState, ConnectionsEvent>(), CommandClient.Handler {
private val sharedCommandClient: CommandClient? = null, private val commandClient = CommandClient(
) : BaseViewModel<ConnectionsUiState, ConnectionsEvent>(), CommandClient.Handler { viewModelScope,
private val commandClient: CommandClient CommandClient.ConnectionType.Connections,
private val isUsingSharedClient: Boolean this,
)
private val _serviceStatus = MutableStateFlow(Status.Stopped) private val _serviceStatus = MutableStateFlow(Status.Stopped)
val serviceStatus = _serviceStatus.asStateFlow() val serviceStatus = _serviceStatus.asStateFlow()
private var lastServiceStatus: Status = Status.Stopped private var lastServiceStatus: Status = Status.Stopped
private var connectionJob: Job? = null
private var rawConnections: Connections? = null private val _isVisible = MutableStateFlow(false)
init { private var connectionsStore: Connections? = null
if (sharedCommandClient != null) { private val connectionsMutex = Mutex()
commandClient = sharedCommandClient private val connectionsGeneration = AtomicLong(0)
isUsingSharedClient = true
commandClient.addHandler(this)
} else {
commandClient = CommandClient(
viewModelScope,
CommandClient.ConnectionType.Connections,
this,
)
isUsingSharedClient = false
}
}
override fun createInitialState() = ConnectionsUiState() override fun createInitialState() = ConnectionsUiState()
override fun onCleared() { init {
super.onCleared() viewModelScope.launch {
connectionJob?.cancel() combine(
connectionJob = null AppLifecycleObserver.isForeground,
if (isUsingSharedClient) { _isVisible,
commandClient.removeHandler(this) _serviceStatus
} else { ) { foreground, visible, status ->
commandClient.disconnect() Triple(foreground, visible, status)
}.collect { (foreground, visible, status) ->
val shouldConnect = foreground && visible && status == Status.Started
if (shouldConnect) {
updateState { copy(isLoading = true) }
commandClient.connect()
} else {
commandClient.disconnect()
}
}
} }
} }
private fun handleServiceStatusChange(status: Status) { fun setVisible(visible: Boolean) {
if (status == Status.Started) { _isVisible.value = visible
if (!isUsingSharedClient) { }
updateState { copy(isLoading = true) }
connectionJob?.cancel() override fun onCleared() {
connectionJob = viewModelScope.launch(Dispatchers.IO) { super.onCleared()
while (isActive) { commandClient.disconnect()
try { }
commandClient.connect()
break private suspend fun handleServiceStatusChange(status: Status) {
} catch (e: Exception) { if (status != Status.Started) {
delay(100) withContext(Dispatchers.Default) {
} connectionsMutex.withLock {
} connectionsStore = null
} }
connectionsGeneration.incrementAndGet()
} }
} else {
connectionJob?.cancel()
connectionJob = null
if (!isUsingSharedClient) {
commandClient.disconnect()
}
rawConnections = null
updateState { updateState {
copy(connections = emptyList(), allConnections = emptyList(), isLoading = false) copy(connections = emptyList(), allConnections = emptyList(), isLoading = false)
} }
@@ -116,17 +111,17 @@ class ConnectionsViewModel(
fun setStateFilter(filter: ConnectionStateFilter) { fun setStateFilter(filter: ConnectionStateFilter) {
updateState { copy(stateFilter = filter) } updateState { copy(stateFilter = filter) }
rawConnections?.let { processConnections(it) } requestConnectionsRefresh()
} }
fun setSort(sort: ConnectionSort) { fun setSort(sort: ConnectionSort) {
updateState { copy(sort = sort) } updateState { copy(sort = sort) }
rawConnections?.let { processConnections(it) } requestConnectionsRefresh()
} }
fun setSearchText(text: String) { fun setSearchText(text: String) {
updateState { copy(searchText = text) } updateState { copy(searchText = text) }
rawConnections?.let { processConnections(it) } requestConnectionsRefresh()
} }
fun toggleSearch() { fun toggleSearch() {
@@ -138,7 +133,7 @@ class ConnectionsViewModel(
) )
} }
if (!newSearchActive) { if (!newSearchActive) {
rawConnections?.let { processConnections(it) } requestConnectionsRefresh()
} }
} }
@@ -175,50 +170,102 @@ class ConnectionsViewModel(
} }
override fun onDisconnected() { override fun onDisconnected() {
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Default) {
rawConnections = null connectionsMutex.withLock {
updateState { connectionsStore = null
copy(connections = emptyList(), allConnections = emptyList(), isLoading = false) }
connectionsGeneration.incrementAndGet()
withContext(Dispatchers.Main) {
updateState {
copy(connections = emptyList(), allConnections = emptyList(), isLoading = false)
}
} }
} }
} }
override fun updateConnections(connections: Connections) { override fun writeConnectionEvents(events: ConnectionEvents) {
rawConnections = connections viewModelScope.launch(Dispatchers.Default) {
processConnections(connections) val generation = connectionsGeneration.get()
} val snapshot = connectionsMutex.withLock {
if (connectionsStore == null) {
private fun processConnections(connections: Connections) { connectionsStore = Libbox.newConnections()
connectionJob?.cancel() }
connectionJob = viewModelScope.launch(Dispatchers.Default) { val store = connectionsStore ?: return@withLock null
val currentState = uiState.value store.applyEvents(events)
buildConnectionLists(store, uiState.value)
val allConnectionList = connections.iterator().toList() } ?: return@launch
.filter { it.outboundType != "dns" } if (connectionsGeneration.get() != generation) {
.map { Connection.from(it) } return@launch
connections.filterState(currentState.stateFilter.libboxValue)
when (currentState.sort) {
ConnectionSort.ByDate -> connections.sortByDate()
ConnectionSort.ByTraffic -> connections.sortByTraffic()
ConnectionSort.ByTrafficTotal -> connections.sortByTrafficTotal()
} }
val connectionList = connections.iterator().toList()
.filter { it.outboundType != "dns" }
.map { Connection.from(it) }
.filter { it.performSearch(currentState.searchText) }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (connectionsGeneration.get() != generation) {
return@withContext
}
updateState { updateState {
copy( copy(
connections = connectionList, connections = snapshot.connections,
allConnections = allConnectionList, allConnections = snapshot.allConnections,
isLoading = false, isLoading = false,
) )
} }
} }
} }
} }
private fun requestConnectionsRefresh() {
viewModelScope.launch(Dispatchers.Default) {
val generation = connectionsGeneration.get()
val snapshot = connectionsMutex.withLock {
val store = connectionsStore ?: return@withLock null
buildConnectionLists(store, uiState.value)
} ?: return@launch
if (connectionsGeneration.get() != generation) {
return@launch
}
withContext(Dispatchers.Main) {
if (connectionsGeneration.get() != generation) {
return@withContext
}
updateState {
copy(
connections = snapshot.connections,
allConnections = snapshot.allConnections,
isLoading = false,
)
}
}
}
}
private fun buildConnectionLists(
connections: Connections,
currentState: ConnectionsUiState,
): ConnectionLists {
val allConnectionList = connections.iterator().toList()
.filter { it.outboundType != "dns" }
.map { Connection.from(it) }
connections.filterState(currentState.stateFilter.libboxValue)
when (currentState.sort) {
ConnectionSort.ByDate -> connections.sortByDate()
ConnectionSort.ByTraffic -> connections.sortByTraffic()
ConnectionSort.ByTrafficTotal -> connections.sortByTrafficTotal()
}
val connectionList = connections.iterator().toList()
.filter { it.outboundType != "dns" }
.map { Connection.from(it) }
.filter { it.performSearch(currentState.searchText) }
return ConnectionLists(
connections = connectionList,
allConnections = allConnectionList,
)
}
private data class ConnectionLists(
val connections: List<Connection>,
val allConnections: List<Connection>,
)
} }

View File

@@ -1,11 +1,9 @@
package io.nekohasekai.sfa.compose.screen.dashboard package io.nekohasekai.sfa.compose.screen.dashboard
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.nekohasekai.libbox.Connections
import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.OutboundGroup import io.nekohasekai.libbox.OutboundGroup
import io.nekohasekai.libbox.StatusMessage import io.nekohasekai.libbox.StatusMessage
import io.nekohasekai.sfa.ktx.toList
import io.nekohasekai.sfa.bg.BoxService import io.nekohasekai.sfa.bg.BoxService
import io.nekohasekai.sfa.compose.base.BaseViewModel import io.nekohasekai.sfa.compose.base.BaseViewModel
import io.nekohasekai.sfa.compose.base.UiEvent import io.nekohasekai.sfa.compose.base.UiEvent
@@ -14,6 +12,7 @@ import io.nekohasekai.sfa.database.Profile
import io.nekohasekai.sfa.database.ProfileManager import io.nekohasekai.sfa.database.ProfileManager
import io.nekohasekai.sfa.database.Settings import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.database.TypedProfile import io.nekohasekai.sfa.database.TypedProfile
import io.nekohasekai.sfa.utils.AppLifecycleObserver
import io.nekohasekai.sfa.utils.CommandClient import io.nekohasekai.sfa.utils.CommandClient
import io.nekohasekai.sfa.utils.HTTPClient import io.nekohasekai.sfa.utils.HTTPClient
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -135,7 +134,6 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
CommandClient.ConnectionType.Status, CommandClient.ConnectionType.Status,
CommandClient.ConnectionType.ClashMode, CommandClient.ConnectionType.ClashMode,
CommandClient.ConnectionType.Groups, CommandClient.ConnectionType.Groups,
CommandClient.ConnectionType.Connections,
), ),
this, this,
) )
@@ -157,6 +155,17 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
init { init {
loadProfiles() loadProfiles()
ProfileManager.registerCallback(::onProfilesChanged) ProfileManager.registerCallback(::onProfilesChanged)
viewModelScope.launch {
AppLifecycleObserver.isForeground.collect { foreground ->
if (_serviceStatus.value != Status.Started) return@collect
if (foreground) {
commandClient.connect()
} else {
commandClient.disconnect()
}
}
}
} }
override fun onCleared() { override fun onCleared() {
@@ -447,7 +456,9 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
when (status) { when (status) {
Status.Started -> { Status.Started -> {
checkDeprecatedNotes() checkDeprecatedNotes()
commandClient.connect() if (AppLifecycleObserver.isForeground.value) {
commandClient.connect()
}
reloadSystemProxyStatus() reloadSystemProxyStatus()
reloadStartedAt() reloadStartedAt()
} }
@@ -458,6 +469,7 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
copy( copy(
hasGroups = false, hasGroups = false,
groupsCount = 0, groupsCount = 0,
connectionsCount = 0,
serviceStartTime = null, serviceStartTime = null,
clashModeVisible = false, clashModeVisible = false,
systemProxyVisible = false, systemProxyVisible = false,
@@ -587,6 +599,7 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
goroutines = status.goroutines.toString(), goroutines = status.goroutines.toString(),
// Only set trafficVisible to true, never back to false from status updates // Only set trafficVisible to true, never back to false from status updates
trafficVisible = if (status.trafficAvailable) true else trafficVisible, trafficVisible = if (status.trafficAvailable) true else trafficVisible,
connectionsCount = status.connectionsIn,
connectionsIn = status.connectionsIn.toString(), connectionsIn = status.connectionsIn.toString(),
connectionsOut = status.connectionsOut.toString(), connectionsOut = status.connectionsOut.toString(),
uplink = "${Libbox.formatBytes(status.uplink)}/s", uplink = "${Libbox.formatBytes(status.uplink)}/s",
@@ -633,13 +646,6 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
} }
} }
override fun updateConnections(connections: Connections) {
viewModelScope.launch(Dispatchers.Main) {
val count = connections.iterator().toList().count { it.outboundType != "dns" }
updateState { copy(connectionsCount = count) }
}
}
fun toggleCardSettingsDialog() { fun toggleCardSettingsDialog() {
updateState { updateState {
copy(showCardSettingsDialog = !showCardSettingsDialog) copy(showCardSettingsDialog = !showCardSettingsDialog)

View File

@@ -9,13 +9,11 @@ import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.compose.model.Group import io.nekohasekai.sfa.compose.model.Group
import io.nekohasekai.sfa.compose.model.GroupItem import io.nekohasekai.sfa.compose.model.GroupItem
import io.nekohasekai.sfa.compose.model.toList import io.nekohasekai.sfa.compose.model.toList
import io.nekohasekai.sfa.utils.AppLifecycleObserver
import io.nekohasekai.sfa.utils.CommandClient import io.nekohasekai.sfa.utils.CommandClient
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -39,7 +37,6 @@ class GroupsViewModel(
private val _serviceStatus = MutableStateFlow(Status.Stopped) private val _serviceStatus = MutableStateFlow(Status.Stopped)
val serviceStatus = _serviceStatus.asStateFlow() val serviceStatus = _serviceStatus.asStateFlow()
private var lastServiceStatus: Status = Status.Stopped private var lastServiceStatus: Status = Status.Stopped
private var connectionJob: Job? = null
init { init {
if (sharedCommandClient != null) { if (sharedCommandClient != null) {
@@ -55,14 +52,32 @@ class GroupsViewModel(
) )
isUsingSharedClient = false isUsingSharedClient = false
} }
viewModelScope.launch {
AppLifecycleObserver.isForeground.collect { foreground ->
if (lastServiceStatus != Status.Started) return@collect
if (foreground) {
if (isUsingSharedClient) {
commandClient.addHandler(this@GroupsViewModel)
} else {
updateState { copy(isLoading = true) }
commandClient.connect()
}
} else {
if (isUsingSharedClient) {
commandClient.removeHandler(this@GroupsViewModel)
} else {
commandClient.disconnect()
}
}
}
}
} }
override fun createInitialState() = GroupsUiState() override fun createInitialState() = GroupsUiState()
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
connectionJob?.cancel()
connectionJob = null
if (isUsingSharedClient) { if (isUsingSharedClient) {
commandClient.removeHandler(this) commandClient.removeHandler(this)
} else { } else {
@@ -72,25 +87,11 @@ class GroupsViewModel(
private fun handleServiceStatusChange(status: Status) { private fun handleServiceStatusChange(status: Status) {
if (status == Status.Started) { if (status == Status.Started) {
if (!isUsingSharedClient) { if (!isUsingSharedClient && AppLifecycleObserver.isForeground.value) {
updateState { updateState { copy(isLoading = true) }
copy(isLoading = true) commandClient.connect()
}
connectionJob?.cancel()
connectionJob = viewModelScope.launch(Dispatchers.IO) {
while (isActive) {
try {
commandClient.connect()
break
} catch (e: Exception) {
delay(100)
}
}
}
} }
} else { } else {
connectionJob?.cancel()
connectionJob = null
if (!isUsingSharedClient) { if (!isUsingSharedClient) {
commandClient.disconnect() commandClient.disconnect()
} }
@@ -243,8 +244,6 @@ class GroupsViewModel(
} }
override fun updateGroups(newGroups: MutableList<OutboundGroup>) { override fun updateGroups(newGroups: MutableList<OutboundGroup>) {
connectionJob?.cancel()
connectionJob = null
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
val currentGroups = uiState.value.groups val currentGroups = uiState.value.groups
val newGroupsMap = newGroups.associateBy { it.tag } val newGroupsMap = newGroups.associateBy { it.tag }

View File

@@ -5,6 +5,7 @@ import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.LogEntry import io.nekohasekai.libbox.LogEntry
import io.nekohasekai.sfa.compose.util.AnsiColorUtils import io.nekohasekai.sfa.compose.util.AnsiColorUtils
import io.nekohasekai.sfa.constant.Status import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.utils.AppLifecycleObserver
import io.nekohasekai.sfa.utils.CommandClient import io.nekohasekai.sfa.utils.CommandClient
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@@ -24,6 +25,20 @@ class LogViewModel : BaseLogViewModel(), CommandClient.Handler {
connectionType = CommandClient.ConnectionType.Log, connectionType = CommandClient.ConnectionType.Log,
handler = this, handler = this,
) )
private var lastServiceStatus: Status = Status.Stopped
init {
viewModelScope.launch {
AppLifecycleObserver.isForeground.collect { foreground ->
if (lastServiceStatus != Status.Started) return@collect
if (foreground) {
commandClient.connect()
} else {
commandClient.disconnect()
}
}
}
}
private fun processLogEntry(entry: LogEntry): ProcessedLogEntry { private fun processLogEntry(entry: LogEntry): ProcessedLogEntry {
val level = LogLevel.entries.find { it.priority == entry.level } ?: LogLevel.Default val level = LogLevel.entries.find { it.priority == entry.level } ?: LogLevel.Default
@@ -35,11 +50,14 @@ class LogViewModel : BaseLogViewModel(), CommandClient.Handler {
} }
override fun updateServiceStatus(status: Status) { override fun updateServiceStatus(status: Status) {
lastServiceStatus = status
_uiState.update { it.copy(serviceStatus = status) } _uiState.update { it.copy(serviceStatus = status) }
when (status) { when (status) {
Status.Started -> { Status.Started -> {
commandClient.connect() if (AppLifecycleObserver.isForeground.value) {
commandClient.connect()
}
} }
Status.Stopped, Status.Stopping -> { Status.Stopped, Status.Stopping -> {

View File

@@ -0,0 +1,25 @@
package io.nekohasekai.sfa.utils
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
object AppLifecycleObserver : DefaultLifecycleObserver {
private val _isForeground = MutableStateFlow(true)
val isForeground: StateFlow<Boolean> = _isForeground.asStateFlow()
fun register() {
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
}
override fun onStart(owner: LifecycleOwner) {
_isForeground.value = true
}
override fun onStop(owner: LifecycleOwner) {
_isForeground.value = false
}
}

View File

@@ -5,7 +5,7 @@ import go.Seq
import io.nekohasekai.libbox.CommandClient import io.nekohasekai.libbox.CommandClient
import io.nekohasekai.libbox.CommandClientHandler import io.nekohasekai.libbox.CommandClientHandler
import io.nekohasekai.libbox.CommandClientOptions import io.nekohasekai.libbox.CommandClientOptions
import io.nekohasekai.libbox.Connections import io.nekohasekai.libbox.ConnectionEvents
import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.LogEntry import io.nekohasekai.libbox.LogEntry
import io.nekohasekai.libbox.LogIterator import io.nekohasekai.libbox.LogIterator
@@ -15,10 +15,6 @@ import io.nekohasekai.libbox.StatusMessage
import io.nekohasekai.libbox.StringIterator import io.nekohasekai.libbox.StringIterator
import io.nekohasekai.sfa.ktx.toList import io.nekohasekai.sfa.ktx.toList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
open class CommandClient( open class CommandClient(
private val scope: CoroutineScope, private val scope: CoroutineScope,
@@ -87,7 +83,7 @@ open class CommandClient(
fun updateClashMode(newMode: String) {} fun updateClashMode(newMode: String) {}
fun updateConnections(connections: Connections) {} fun writeConnectionEvents(events: ConnectionEvents) {}
} }
private var commandClient: CommandClient? = null private var commandClient: CommandClient? = null
@@ -109,27 +105,8 @@ open class CommandClient(
} }
options.statusInterval = 1 * 1000 * 1000 * 1000 options.statusInterval = 1 * 1000 * 1000 * 1000
val commandClient = CommandClient(clientHandler, options) val commandClient = CommandClient(clientHandler, options)
scope.launch(Dispatchers.IO) { commandClient.connect()
for (i in 1..10) { this.commandClient = commandClient
delay(100 + i.toLong() * 50)
try {
commandClient.connect()
} catch (ignored: Exception) {
continue
}
if (!isActive) {
runCatching {
commandClient.disconnect()
}
return@launch
}
this@CommandClient.commandClient = commandClient
return@launch
}
runCatching {
commandClient.disconnect()
}
}
} }
fun disconnect() { fun disconnect() {
@@ -197,9 +174,9 @@ open class CommandClient(
getAllHandlers().forEach { it.updateClashMode(newMode) } getAllHandlers().forEach { it.updateClashMode(newMode) }
} }
override fun writeConnections(message: Connections?) { override fun writeConnectionEvents(events: ConnectionEvents?) {
if (message == null) return if (events == null) return
getAllHandlers().forEach { it.updateConnections(message) } getAllHandlers().forEach { it.writeConnectionEvents(events) }
} }
} }
} }