refactor: CommandClient & Connections
This commit is contained in:
@@ -211,6 +211,7 @@ dependencies {
|
||||
// API 23+ dependencies (play/other)
|
||||
"playImplementation"("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion23")
|
||||
"playImplementation"("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion23")
|
||||
"playImplementation"("androidx.lifecycle:lifecycle-process:$lifecycleVersion23")
|
||||
"playImplementation"("androidx.room:room-runtime:$roomVersion23")
|
||||
"playImplementation"("androidx.work:work-runtime-ktx:$workVersion23")
|
||||
"playImplementation"("androidx.camera:camera-view:$cameraVersion23")
|
||||
@@ -222,6 +223,7 @@ dependencies {
|
||||
|
||||
"otherImplementation"("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion23")
|
||||
"otherImplementation"("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion23")
|
||||
"otherImplementation"("androidx.lifecycle:lifecycle-process:$lifecycleVersion23")
|
||||
"otherImplementation"("androidx.room:room-runtime:$roomVersion23")
|
||||
"otherImplementation"("androidx.work:work-runtime-ktx:$workVersion23")
|
||||
"otherImplementation"("androidx.camera:camera-view:$cameraVersion23")
|
||||
@@ -233,6 +235,7 @@ dependencies {
|
||||
// API 21 dependencies (otherLegacy)
|
||||
"otherLegacyImplementation"("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion21")
|
||||
"otherLegacyImplementation"("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion21")
|
||||
"otherLegacyImplementation"("androidx.lifecycle:lifecycle-process:$lifecycleVersion21")
|
||||
"otherLegacyImplementation"("androidx.room:room-runtime:$roomVersion21")
|
||||
"otherLegacyImplementation"("androidx.work:work-runtime-ktx:$workVersion21")
|
||||
"otherLegacyImplementation"("androidx.camera:camera-view:$cameraVersion21")
|
||||
|
||||
@@ -61,6 +61,7 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -541,14 +542,7 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
||||
|
||||
val connectionsViewModel: ConnectionsViewModel? =
|
||||
if (isConnectionsRoute) {
|
||||
viewModel(
|
||||
factory = object : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return ConnectionsViewModel(dashboardViewModel.commandClient) as T
|
||||
}
|
||||
}
|
||||
)
|
||||
viewModel()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -973,18 +967,21 @@ class MainActivity : ComponentActivity(), ServiceConnection.Callback {
|
||||
// Connections ModalBottomSheet
|
||||
if (showConnectionsSheet && !useNavigationRail) {
|
||||
val connectionsSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
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 connectionsViewModel: ConnectionsViewModel = viewModel()
|
||||
val connectionsUiState by connectionsViewModel.uiState.collectAsState()
|
||||
var selectedConnectionId by remember { mutableStateOf<String?>(null) }
|
||||
val selectedConnection = connectionsUiState.allConnections.find { it.id == selectedConnectionId }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
connectionsViewModel.setVisible(true)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
connectionsViewModel.setVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
showConnectionsSheet = false
|
||||
|
||||
@@ -43,6 +43,7 @@ import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -334,6 +335,16 @@ fun ConnectionsScreen(
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.setVisible(true)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
viewModel.setVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(serviceStatus) {
|
||||
viewModel.updateServiceStatus(serviceStatus)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.nekohasekai.sfa.compose.screen.connections
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.nekohasekai.libbox.ConnectionEvents
|
||||
import io.nekohasekai.libbox.Connections
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
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.ConnectionSort
|
||||
import io.nekohasekai.sfa.compose.model.ConnectionStateFilter
|
||||
import io.nekohasekai.sfa.utils.AppLifecycleObserver
|
||||
import io.nekohasekai.sfa.utils.CommandClient
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
data class ConnectionsUiState(
|
||||
@@ -35,70 +38,62 @@ sealed class ConnectionsEvent : ScreenEvent {
|
||||
data object AllConnectionsClosed : ConnectionsEvent()
|
||||
}
|
||||
|
||||
class ConnectionsViewModel(
|
||||
private val sharedCommandClient: CommandClient? = null,
|
||||
) : BaseViewModel<ConnectionsUiState, ConnectionsEvent>(), CommandClient.Handler {
|
||||
private val commandClient: CommandClient
|
||||
private val isUsingSharedClient: Boolean
|
||||
class ConnectionsViewModel : BaseViewModel<ConnectionsUiState, ConnectionsEvent>(), CommandClient.Handler {
|
||||
private val commandClient = CommandClient(
|
||||
viewModelScope,
|
||||
CommandClient.ConnectionType.Connections,
|
||||
this,
|
||||
)
|
||||
|
||||
private val _serviceStatus = MutableStateFlow(Status.Stopped)
|
||||
val serviceStatus = _serviceStatus.asStateFlow()
|
||||
private var lastServiceStatus: Status = Status.Stopped
|
||||
private var connectionJob: Job? = null
|
||||
|
||||
private var rawConnections: Connections? = null
|
||||
private val _isVisible = MutableStateFlow(false)
|
||||
|
||||
init {
|
||||
if (sharedCommandClient != null) {
|
||||
commandClient = sharedCommandClient
|
||||
isUsingSharedClient = true
|
||||
commandClient.addHandler(this)
|
||||
} else {
|
||||
commandClient = CommandClient(
|
||||
viewModelScope,
|
||||
CommandClient.ConnectionType.Connections,
|
||||
this,
|
||||
)
|
||||
isUsingSharedClient = false
|
||||
}
|
||||
}
|
||||
private var connectionsStore: Connections? = null
|
||||
private val connectionsMutex = Mutex()
|
||||
private val connectionsGeneration = AtomicLong(0)
|
||||
|
||||
override fun createInitialState() = ConnectionsUiState()
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
connectionJob?.cancel()
|
||||
connectionJob = null
|
||||
if (isUsingSharedClient) {
|
||||
commandClient.removeHandler(this)
|
||||
} else {
|
||||
commandClient.disconnect()
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
combine(
|
||||
AppLifecycleObserver.isForeground,
|
||||
_isVisible,
|
||||
_serviceStatus
|
||||
) { foreground, visible, status ->
|
||||
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) {
|
||||
if (status == Status.Started) {
|
||||
if (!isUsingSharedClient) {
|
||||
updateState { copy(isLoading = true) }
|
||||
connectionJob?.cancel()
|
||||
connectionJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
while (isActive) {
|
||||
try {
|
||||
commandClient.connect()
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
fun setVisible(visible: Boolean) {
|
||||
_isVisible.value = visible
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
commandClient.disconnect()
|
||||
}
|
||||
|
||||
private suspend fun handleServiceStatusChange(status: Status) {
|
||||
if (status != Status.Started) {
|
||||
withContext(Dispatchers.Default) {
|
||||
connectionsMutex.withLock {
|
||||
connectionsStore = null
|
||||
}
|
||||
connectionsGeneration.incrementAndGet()
|
||||
}
|
||||
} else {
|
||||
connectionJob?.cancel()
|
||||
connectionJob = null
|
||||
if (!isUsingSharedClient) {
|
||||
commandClient.disconnect()
|
||||
}
|
||||
rawConnections = null
|
||||
updateState {
|
||||
copy(connections = emptyList(), allConnections = emptyList(), isLoading = false)
|
||||
}
|
||||
@@ -116,17 +111,17 @@ class ConnectionsViewModel(
|
||||
|
||||
fun setStateFilter(filter: ConnectionStateFilter) {
|
||||
updateState { copy(stateFilter = filter) }
|
||||
rawConnections?.let { processConnections(it) }
|
||||
requestConnectionsRefresh()
|
||||
}
|
||||
|
||||
fun setSort(sort: ConnectionSort) {
|
||||
updateState { copy(sort = sort) }
|
||||
rawConnections?.let { processConnections(it) }
|
||||
requestConnectionsRefresh()
|
||||
}
|
||||
|
||||
fun setSearchText(text: String) {
|
||||
updateState { copy(searchText = text) }
|
||||
rawConnections?.let { processConnections(it) }
|
||||
requestConnectionsRefresh()
|
||||
}
|
||||
|
||||
fun toggleSearch() {
|
||||
@@ -138,7 +133,7 @@ class ConnectionsViewModel(
|
||||
)
|
||||
}
|
||||
if (!newSearchActive) {
|
||||
rawConnections?.let { processConnections(it) }
|
||||
requestConnectionsRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,50 +170,102 @@ class ConnectionsViewModel(
|
||||
}
|
||||
|
||||
override fun onDisconnected() {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
rawConnections = null
|
||||
updateState {
|
||||
copy(connections = emptyList(), allConnections = emptyList(), isLoading = false)
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
connectionsMutex.withLock {
|
||||
connectionsStore = null
|
||||
}
|
||||
connectionsGeneration.incrementAndGet()
|
||||
withContext(Dispatchers.Main) {
|
||||
updateState {
|
||||
copy(connections = emptyList(), allConnections = emptyList(), isLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateConnections(connections: Connections) {
|
||||
rawConnections = connections
|
||||
processConnections(connections)
|
||||
}
|
||||
|
||||
private fun processConnections(connections: Connections) {
|
||||
connectionJob?.cancel()
|
||||
connectionJob = viewModelScope.launch(Dispatchers.Default) {
|
||||
val currentState = uiState.value
|
||||
|
||||
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()
|
||||
override fun writeConnectionEvents(events: ConnectionEvents) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val generation = connectionsGeneration.get()
|
||||
val snapshot = connectionsMutex.withLock {
|
||||
if (connectionsStore == null) {
|
||||
connectionsStore = Libbox.newConnections()
|
||||
}
|
||||
val store = connectionsStore ?: return@withLock null
|
||||
store.applyEvents(events)
|
||||
buildConnectionLists(store, uiState.value)
|
||||
} ?: return@launch
|
||||
if (connectionsGeneration.get() != generation) {
|
||||
return@launch
|
||||
}
|
||||
|
||||
val connectionList = connections.iterator().toList()
|
||||
.filter { it.outboundType != "dns" }
|
||||
.map { Connection.from(it) }
|
||||
.filter { it.performSearch(currentState.searchText) }
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (connectionsGeneration.get() != generation) {
|
||||
return@withContext
|
||||
}
|
||||
updateState {
|
||||
copy(
|
||||
connections = connectionList,
|
||||
allConnections = allConnectionList,
|
||||
connections = snapshot.connections,
|
||||
allConnections = snapshot.allConnections,
|
||||
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>,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package io.nekohasekai.sfa.compose.screen.dashboard
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.nekohasekai.libbox.Connections
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.libbox.OutboundGroup
|
||||
import io.nekohasekai.libbox.StatusMessage
|
||||
import io.nekohasekai.sfa.ktx.toList
|
||||
import io.nekohasekai.sfa.bg.BoxService
|
||||
import io.nekohasekai.sfa.compose.base.BaseViewModel
|
||||
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.Settings
|
||||
import io.nekohasekai.sfa.database.TypedProfile
|
||||
import io.nekohasekai.sfa.utils.AppLifecycleObserver
|
||||
import io.nekohasekai.sfa.utils.CommandClient
|
||||
import io.nekohasekai.sfa.utils.HTTPClient
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -135,7 +134,6 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
||||
CommandClient.ConnectionType.Status,
|
||||
CommandClient.ConnectionType.ClashMode,
|
||||
CommandClient.ConnectionType.Groups,
|
||||
CommandClient.ConnectionType.Connections,
|
||||
),
|
||||
this,
|
||||
)
|
||||
@@ -157,6 +155,17 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
||||
init {
|
||||
loadProfiles()
|
||||
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() {
|
||||
@@ -447,7 +456,9 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
||||
when (status) {
|
||||
Status.Started -> {
|
||||
checkDeprecatedNotes()
|
||||
commandClient.connect()
|
||||
if (AppLifecycleObserver.isForeground.value) {
|
||||
commandClient.connect()
|
||||
}
|
||||
reloadSystemProxyStatus()
|
||||
reloadStartedAt()
|
||||
}
|
||||
@@ -458,6 +469,7 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
||||
copy(
|
||||
hasGroups = false,
|
||||
groupsCount = 0,
|
||||
connectionsCount = 0,
|
||||
serviceStartTime = null,
|
||||
clashModeVisible = false,
|
||||
systemProxyVisible = false,
|
||||
@@ -587,6 +599,7 @@ class DashboardViewModel : BaseViewModel<DashboardUiState, UiEvent>(), CommandCl
|
||||
goroutines = status.goroutines.toString(),
|
||||
// Only set trafficVisible to true, never back to false from status updates
|
||||
trafficVisible = if (status.trafficAvailable) true else trafficVisible,
|
||||
connectionsCount = status.connectionsIn,
|
||||
connectionsIn = status.connectionsIn.toString(),
|
||||
connectionsOut = status.connectionsOut.toString(),
|
||||
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() {
|
||||
updateState {
|
||||
copy(showCardSettingsDialog = !showCardSettingsDialog)
|
||||
|
||||
@@ -9,13 +9,11 @@ import io.nekohasekai.sfa.constant.Status
|
||||
import io.nekohasekai.sfa.compose.model.Group
|
||||
import io.nekohasekai.sfa.compose.model.GroupItem
|
||||
import io.nekohasekai.sfa.compose.model.toList
|
||||
import io.nekohasekai.sfa.utils.AppLifecycleObserver
|
||||
import io.nekohasekai.sfa.utils.CommandClient
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -39,7 +37,6 @@ class GroupsViewModel(
|
||||
private val _serviceStatus = MutableStateFlow(Status.Stopped)
|
||||
val serviceStatus = _serviceStatus.asStateFlow()
|
||||
private var lastServiceStatus: Status = Status.Stopped
|
||||
private var connectionJob: Job? = null
|
||||
|
||||
init {
|
||||
if (sharedCommandClient != null) {
|
||||
@@ -55,14 +52,32 @@ class GroupsViewModel(
|
||||
)
|
||||
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 onCleared() {
|
||||
super.onCleared()
|
||||
connectionJob?.cancel()
|
||||
connectionJob = null
|
||||
if (isUsingSharedClient) {
|
||||
commandClient.removeHandler(this)
|
||||
} else {
|
||||
@@ -72,25 +87,11 @@ class GroupsViewModel(
|
||||
|
||||
private fun handleServiceStatusChange(status: Status) {
|
||||
if (status == Status.Started) {
|
||||
if (!isUsingSharedClient) {
|
||||
updateState {
|
||||
copy(isLoading = true)
|
||||
}
|
||||
connectionJob?.cancel()
|
||||
connectionJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
while (isActive) {
|
||||
try {
|
||||
commandClient.connect()
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isUsingSharedClient && AppLifecycleObserver.isForeground.value) {
|
||||
updateState { copy(isLoading = true) }
|
||||
commandClient.connect()
|
||||
}
|
||||
} else {
|
||||
connectionJob?.cancel()
|
||||
connectionJob = null
|
||||
if (!isUsingSharedClient) {
|
||||
commandClient.disconnect()
|
||||
}
|
||||
@@ -243,8 +244,6 @@ class GroupsViewModel(
|
||||
}
|
||||
|
||||
override fun updateGroups(newGroups: MutableList<OutboundGroup>) {
|
||||
connectionJob?.cancel()
|
||||
connectionJob = null
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val currentGroups = uiState.value.groups
|
||||
val newGroupsMap = newGroups.associateBy { it.tag }
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.libbox.LogEntry
|
||||
import io.nekohasekai.sfa.compose.util.AnsiColorUtils
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
import io.nekohasekai.sfa.utils.AppLifecycleObserver
|
||||
import io.nekohasekai.sfa.utils.CommandClient
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.update
|
||||
@@ -24,6 +25,20 @@ class LogViewModel : BaseLogViewModel(), CommandClient.Handler {
|
||||
connectionType = CommandClient.ConnectionType.Log,
|
||||
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 {
|
||||
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) {
|
||||
lastServiceStatus = status
|
||||
_uiState.update { it.copy(serviceStatus = status) }
|
||||
|
||||
when (status) {
|
||||
Status.Started -> {
|
||||
commandClient.connect()
|
||||
if (AppLifecycleObserver.isForeground.value) {
|
||||
commandClient.connect()
|
||||
}
|
||||
}
|
||||
|
||||
Status.Stopped, Status.Stopping -> {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import go.Seq
|
||||
import io.nekohasekai.libbox.CommandClient
|
||||
import io.nekohasekai.libbox.CommandClientHandler
|
||||
import io.nekohasekai.libbox.CommandClientOptions
|
||||
import io.nekohasekai.libbox.Connections
|
||||
import io.nekohasekai.libbox.ConnectionEvents
|
||||
import io.nekohasekai.libbox.Libbox
|
||||
import io.nekohasekai.libbox.LogEntry
|
||||
import io.nekohasekai.libbox.LogIterator
|
||||
@@ -15,10 +15,6 @@ import io.nekohasekai.libbox.StatusMessage
|
||||
import io.nekohasekai.libbox.StringIterator
|
||||
import io.nekohasekai.sfa.ktx.toList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
open class CommandClient(
|
||||
private val scope: CoroutineScope,
|
||||
@@ -87,7 +83,7 @@ open class CommandClient(
|
||||
|
||||
fun updateClashMode(newMode: String) {}
|
||||
|
||||
fun updateConnections(connections: Connections) {}
|
||||
fun writeConnectionEvents(events: ConnectionEvents) {}
|
||||
}
|
||||
|
||||
private var commandClient: CommandClient? = null
|
||||
@@ -109,27 +105,8 @@ open class CommandClient(
|
||||
}
|
||||
options.statusInterval = 1 * 1000 * 1000 * 1000
|
||||
val commandClient = CommandClient(clientHandler, options)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
for (i in 1..10) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
commandClient.connect()
|
||||
this.commandClient = commandClient
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
@@ -197,9 +174,9 @@ open class CommandClient(
|
||||
getAllHandlers().forEach { it.updateClashMode(newMode) }
|
||||
}
|
||||
|
||||
override fun writeConnections(message: Connections?) {
|
||||
if (message == null) return
|
||||
getAllHandlers().forEach { it.updateConnections(message) }
|
||||
override fun writeConnectionEvents(events: ConnectionEvents?) {
|
||||
if (events == null) return
|
||||
getAllHandlers().forEach { it.writeConnectionEvents(events) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user