refactor: CommandClient & Connections
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
|
||||||
private val commandClient: CommandClient
|
|
||||||
private val isUsingSharedClient: Boolean
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (sharedCommandClient != null) {
|
|
||||||
commandClient = sharedCommandClient
|
|
||||||
isUsingSharedClient = true
|
|
||||||
commandClient.addHandler(this)
|
|
||||||
} else {
|
|
||||||
commandClient = CommandClient(
|
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
CommandClient.ConnectionType.Connections,
|
CommandClient.ConnectionType.Connections,
|
||||||
this,
|
this,
|
||||||
)
|
)
|
||||||
isUsingSharedClient = false
|
|
||||||
}
|
private val _serviceStatus = MutableStateFlow(Status.Stopped)
|
||||||
}
|
val serviceStatus = _serviceStatus.asStateFlow()
|
||||||
|
private var lastServiceStatus: Status = Status.Stopped
|
||||||
|
|
||||||
|
private val _isVisible = MutableStateFlow(false)
|
||||||
|
|
||||||
|
private var connectionsStore: Connections? = null
|
||||||
|
private val connectionsMutex = Mutex()
|
||||||
|
private val connectionsGeneration = AtomicLong(0)
|
||||||
|
|
||||||
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
|
||||||
|
) { 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 {
|
} else {
|
||||||
commandClient.disconnect()
|
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()
|
|
||||||
connectionJob = viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
while (isActive) {
|
|
||||||
try {
|
|
||||||
commandClient.connect()
|
|
||||||
break
|
|
||||||
} catch (e: Exception) {
|
|
||||||
delay(100)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
override fun onCleared() {
|
||||||
}
|
super.onCleared()
|
||||||
} else {
|
|
||||||
connectionJob?.cancel()
|
|
||||||
connectionJob = null
|
|
||||||
if (!isUsingSharedClient) {
|
|
||||||
commandClient.disconnect()
|
commandClient.disconnect()
|
||||||
}
|
}
|
||||||
rawConnections = null
|
|
||||||
|
private suspend fun handleServiceStatusChange(status: Status) {
|
||||||
|
if (status != Status.Started) {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
connectionsMutex.withLock {
|
||||||
|
connectionsStore = null
|
||||||
|
}
|
||||||
|
connectionsGeneration.incrementAndGet()
|
||||||
|
}
|
||||||
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,24 +170,77 @@ class ConnectionsViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDisconnected() {
|
override fun onDisconnected() {
|
||||||
viewModelScope.launch(Dispatchers.Main) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
rawConnections = null
|
connectionsMutex.withLock {
|
||||||
|
connectionsStore = null
|
||||||
|
}
|
||||||
|
connectionsGeneration.incrementAndGet()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
updateState {
|
updateState {
|
||||||
copy(connections = emptyList(), allConnections = emptyList(), isLoading = false)
|
copy(connections = emptyList(), allConnections = emptyList(), isLoading = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateConnections(connections: Connections) {
|
|
||||||
rawConnections = connections
|
|
||||||
processConnections(connections)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processConnections(connections: Connections) {
|
override fun writeConnectionEvents(events: ConnectionEvents) {
|
||||||
connectionJob?.cancel()
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
connectionJob = viewModelScope.launch(Dispatchers.Default) {
|
val generation = connectionsGeneration.get()
|
||||||
val currentState = uiState.value
|
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
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (connectionsGeneration.get() != generation) {
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
updateState {
|
||||||
|
copy(
|
||||||
|
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()
|
val allConnectionList = connections.iterator().toList()
|
||||||
.filter { it.outboundType != "dns" }
|
.filter { it.outboundType != "dns" }
|
||||||
.map { Connection.from(it) }
|
.map { Connection.from(it) }
|
||||||
@@ -210,15 +258,14 @@ class ConnectionsViewModel(
|
|||||||
.map { Connection.from(it) }
|
.map { Connection.from(it) }
|
||||||
.filter { it.performSearch(currentState.searchText) }
|
.filter { it.performSearch(currentState.searchText) }
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
return ConnectionLists(
|
||||||
updateState {
|
|
||||||
copy(
|
|
||||||
connections = connectionList,
|
connections = connectionList,
|
||||||
allConnections = allConnectionList,
|
allConnections = allConnectionList,
|
||||||
isLoading = false,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
private data class ConnectionLists(
|
||||||
}
|
val connections: List<Connection>,
|
||||||
|
val allConnections: List<Connection>,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
if (AppLifecycleObserver.isForeground.value) {
|
||||||
commandClient.connect()
|
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)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
connectionJob?.cancel()
|
|
||||||
connectionJob = viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
while (isActive) {
|
|
||||||
try {
|
|
||||||
commandClient.connect()
|
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 }
|
||||||
|
|||||||
@@ -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,12 +50,15 @@ 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 -> {
|
||||||
|
if (AppLifecycleObserver.isForeground.value) {
|
||||||
commandClient.connect()
|
commandClient.connect()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Status.Stopped, Status.Stopping -> {
|
Status.Stopped, Status.Stopping -> {
|
||||||
commandClient.disconnect()
|
commandClient.disconnect()
|
||||||
|
|||||||
@@ -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.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) {
|
|
||||||
for (i in 1..10) {
|
|
||||||
delay(100 + i.toLong() * 50)
|
|
||||||
try {
|
|
||||||
commandClient.connect()
|
commandClient.connect()
|
||||||
} catch (ignored: Exception) {
|
this.commandClient = commandClient
|
||||||
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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user