diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/BackendQualifiers.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/BackendQualifiers.kt index 88d7443..6612356 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/BackendQualifiers.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/BackendQualifiers.kt @@ -9,3 +9,11 @@ annotation class Kernel @Qualifier @Retention(AnnotationRetention.BINARY) annotation class Userspace + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class TunnelShell + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class AppShell diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt index dc19286..5c9ceff 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt @@ -25,9 +25,18 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) class TunnelModule { + @Provides @Singleton - fun provideRootShell(@ApplicationContext context: Context): RootShell { + @TunnelShell + fun provideTunnelRootShell(@ApplicationContext context: Context): RootShell { + return RootShell(context) + } + + @Provides + @Singleton + @AppShell + fun provideAppRootShell(@ApplicationContext context: Context): RootShell { return RootShell(context) } @@ -40,14 +49,14 @@ class TunnelModule { @Provides @Singleton @Userspace - fun provideUserspaceBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend { + fun provideUserspaceBackend(@ApplicationContext context: Context, @TunnelShell rootShell: RootShell): Backend { return GoBackend(context, RootTunnelActionHandler(rootShell)) } @Provides @Singleton @Kernel - fun provideKernelBackend(@ApplicationContext context: Context, rootShell: RootShell): Backend { + fun provideKernelBackend(@ApplicationContext context: Context, @TunnelShell rootShell: RootShell): Backend { return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell), RootTunnelActionHandler(rootShell)) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelService.kt index 994ce53..a606a37 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/AutoTunnelService.kt @@ -12,6 +12,7 @@ import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.module.AppShell import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher import com.zaneschepke.wireguardautotunnel.service.network.EthernetService @@ -25,7 +26,6 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName -import com.zaneschepke.wireguardautotunnel.util.extensions.isDown import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable import com.zaneschepke.wireguardautotunnel.util.extensions.onNotRunning import dagger.hilt.android.AndroidEntryPoint @@ -34,7 +34,6 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.update @@ -50,6 +49,7 @@ class AutoTunnelService : LifecycleService() { private val foregroundId = 122 @Inject + @AppShell lateinit var rootShell: Provider @Inject @@ -143,7 +143,7 @@ class AutoTunnelService : LifecycleService() { super.onDestroy() } - private fun launchWatcherNotification(description: String = getString(R.string.watcher_notification_text_active)) { + private fun launchWatcherNotification(description: String = getString(R.string.monitoring_state_changes)) { val notification = notificationService.createNotification( channelId = getString(R.string.watcher_channel_id), @@ -209,28 +209,16 @@ class AutoTunnelService : LifecycleService() { when (status) { is NetworkStatus.Available -> { Timber.i("Gained Mobile data connection") - autoTunnelStateFlow.update { - it.copy( - isMobileDataConnected = true, - ) - } + emitMobileDataConnected(true) } is NetworkStatus.CapabilitiesChanged -> { - autoTunnelStateFlow.update { - it.copy( - isMobileDataConnected = true, - ) - } + emitMobileDataConnected(true) Timber.i("Mobile data capabilities changed") } is NetworkStatus.Unavailable -> { - autoTunnelStateFlow.update { - it.copy( - isMobileDataConnected = false, - ) - } + emitMobileDataConnected(false) Timber.i("Lost mobile data connection") } } @@ -283,6 +271,7 @@ class AutoTunnelService : LifecycleService() { old.map { it.isActive } != new.map { it.isActive } }, ) { settings, tunnels -> + Timber.d("Tunnels or settings changed!") autoTunnelStateFlow.value.copy( settings = settings, tunnels = tunnels, @@ -299,7 +288,7 @@ class AutoTunnelService : LifecycleService() { Timber.i("Starting vpn state watcher") withContext(ioDispatcher) { tunnelService.get().vpnState.distinctUntilChanged { old, new -> - old.tunnelConfig == new.tunnelConfig && old.status == new.status + old.tunnelConfig?.id == new.tunnelConfig?.id }.collect { state -> autoTunnelStateFlow.update { it.copy(vpnState = state) @@ -367,7 +356,7 @@ class AutoTunnelService : LifecycleService() { mobileDataJob = null } - private fun updateEthernet(connected: Boolean) { + private fun emitEthernetConnected(connected: Boolean) { autoTunnelStateFlow.update { it.copy( isEthernetConnected = connected, @@ -375,7 +364,7 @@ class AutoTunnelService : LifecycleService() { } } - private fun updateWifi(connected: Boolean) { + private fun emitWifiConnected(connected: Boolean) { autoTunnelStateFlow.update { it.copy( isWifiConnected = connected, @@ -383,6 +372,22 @@ class AutoTunnelService : LifecycleService() { } } + private fun emitWifiSSID(ssid: String) { + autoTunnelStateFlow.update { + it.copy( + currentNetworkSSID = ssid, + ) + } + } + + private fun emitMobileDataConnected(connected: Boolean) { + autoTunnelStateFlow.update { + it.copy( + isMobileDataConnected = connected, + ) + } + } + private suspend fun watchForEthernetConnectivityChanges() { withContext(ioDispatcher) { Timber.i("Starting ethernet data watcher") @@ -390,16 +395,16 @@ class AutoTunnelService : LifecycleService() { when (status) { is NetworkStatus.Available -> { Timber.i("Gained Ethernet connection") - updateEthernet(true) + emitEthernetConnected(true) } is NetworkStatus.CapabilitiesChanged -> { Timber.i("Ethernet capabilities changed") - updateEthernet(true) + emitEthernetConnected(true) } is NetworkStatus.Unavailable -> { - updateEthernet(false) + emitEthernetConnected(false) Timber.i("Lost Ethernet connection") } } @@ -414,12 +419,12 @@ class AutoTunnelService : LifecycleService() { when (status) { is NetworkStatus.Available -> { Timber.i("Gained Wi-Fi connection") - updateWifi(true) + emitWifiConnected(true) } is NetworkStatus.CapabilitiesChanged -> { Timber.i("Wifi capabilities changed") - updateWifi(true) + emitWifiConnected(true) val ssid = getWifiSSID(status.networkCapabilities) ssid?.let { name -> if (name.contains(Constants.UNREADABLE_SSID)) { @@ -428,16 +433,12 @@ class AutoTunnelService : LifecycleService() { Timber.i("Detected valid SSID") } appDataRepository.appState.setCurrentSsid(name) - autoTunnelStateFlow.update { - it.copy( - currentNetworkSSID = name, - ) - } + emitWifiSSID(name) } ?: Timber.w("Failed to read ssid") } is NetworkStatus.Unavailable -> { - updateWifi(false) + emitWifiConnected(false) Timber.i("Lost Wi-Fi connection") } } @@ -461,17 +462,17 @@ class AutoTunnelService : LifecycleService() { private suspend fun handleNetworkEventChanges() { withContext(ioDispatcher) { Timber.i("Starting network event watcher") - autoTunnelStateFlow.collectLatest { watcherState -> + autoTunnelStateFlow.collect { watcherState -> val autoTunnel = "Auto-tunnel watcher" - Timber.d("New watcher state!") // delay for rapid network state changes and then collect latest delay(Constants.WATCHER_COLLECTION_DELAY) val activeTunnel = watcherState.vpnState.tunnelConfig val defaultTunnel = appDataRepository.getPrimaryOrFirstTunnel() + val isTunnelDown = tunnelService.get().getState() == TunnelState.DOWN when { watcherState.isEthernetConditionMet() -> { Timber.i("$autoTunnel - tunnel on on ethernet condition met") - if (watcherState.vpnState.isDown()) { + if (isTunnelDown) { defaultTunnel?.let { tunnelService.get().startTunnel(it) } @@ -483,7 +484,7 @@ class AutoTunnelService : LifecycleService() { val mobileDataTunnel = getMobileDataTunnel() val tunnel = mobileDataTunnel ?: defaultTunnel - if (watcherState.vpnState.isDown() || activeTunnel?.isMobileDataTunnel == false) { + if (isTunnelDown || activeTunnel?.isMobileDataTunnel == false) { tunnel?.let { tunnelService.get().startTunnel(it) } @@ -492,7 +493,7 @@ class AutoTunnelService : LifecycleService() { watcherState.isTunnelOffOnMobileDataConditionMet() -> { Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off") - if (!watcherState.vpnState.isDown()) { + if (!isTunnelDown) { activeTunnel?.let { tunnelService.get().stopTunnel(it) } @@ -502,20 +503,20 @@ class AutoTunnelService : LifecycleService() { watcherState.isUntrustedWifiConditionMet() -> { Timber.i("Untrusted wifi condition met") if (activeTunnel == null || watcherState.isCurrentSSIDActiveTunnelNetwork() == false || - watcherState.vpnState.isDown() + isTunnelDown ) { Timber.i( "$autoTunnel - tunnel on ssid not associated with current tunnel condition met", ) watcherState.getTunnelWithMatchingTunnelNetwork()?.let { Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}") - if (watcherState.vpnState.isDown() || activeTunnel?.id != it.id) { + if (isTunnelDown || activeTunnel?.id != it.id) { tunnelService.get().startTunnel(it) } } ?: suspend { Timber.i("No tunnel associated with this SSID, using defaults") val default = appDataRepository.getPrimaryOrFirstTunnel() - if (default?.name != tunnelService.get().name || watcherState.vpnState.isDown()) { + if (default?.name != tunnelService.get().name || isTunnelDown) { default?.let { tunnelService.get().startTunnel(it) } @@ -528,22 +529,22 @@ class AutoTunnelService : LifecycleService() { Timber.i( "$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off", ) - if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) } + if (!isTunnelDown) activeTunnel?.let { tunnelService.get().stopTunnel(it) } } watcherState.isTunnelOffOnWifiConditionMet() -> { Timber.i( "$autoTunnel - tunnel off on wifi condition met, turning vpn off", ) - if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) } - } - - watcherState.isTunnelOffOnNoConnectivityMet() -> { - Timber.i( - "$autoTunnel - tunnel off on no connectivity met, turning vpn off", - ) - if (!watcherState.vpnState.isDown()) activeTunnel?.let { tunnelService.get().stopTunnel(it) } + if (!isTunnelDown) activeTunnel?.let { tunnelService.get().stopTunnel(it) } } +// TODO disable for this now +// watcherState.isTunnelOffOnNoConnectivityMet() -> { +// Timber.i( +// "$autoTunnel - tunnel off on no connectivity met, turning vpn off", +// ) +// if (!isTunnelDown) activeTunnel?.let { tunnelService.get().stopTunnel(it) } +// } else -> { Timber.i("$autoTunnel - no condition met") diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt index 08180c1..2b4c005 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/ServiceManager.kt @@ -27,12 +27,14 @@ class ServiceManager companion object : SingletonHolder(::ServiceManager) private fun startService(cls: Class, background: Boolean) { - val intent = Intent(context, cls) - if (background) { - context.startForegroundService(intent) - } else { - context.startService(intent) - } + runCatching { + val intent = Intent(context, cls) + if (background) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + }.onFailure { Timber.e(it) } } suspend fun startAutoTunnel(background: Boolean) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelBackgroundService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelBackgroundService.kt index 620631b..7bec3d5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelBackgroundService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelBackgroundService.kt @@ -62,7 +62,7 @@ class TunnelBackgroundService : LifecycleService() { return notificationService.createNotification( getString(R.string.vpn_channel_id), getString(R.string.vpn_channel_name), - getString(R.string.tunnel_start_text), + getString(R.string.tunnel_running), description = "", ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt index a500bb3..98982f6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.amnezia.awg.backend.Tunnel import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import javax.inject.Provider @@ -52,6 +53,8 @@ constructor( private var statsJob: Job? = null + private val runningHandle = AtomicBoolean(false) + private suspend fun backend(): Any { val settings = appDataRepository.settings.getSettings() if (settings.isKernelEnabled) return kernelBackend.get() @@ -91,8 +94,14 @@ constructor( override suspend fun startTunnel(tunnelConfig: TunnelConfig, background: Boolean): Result { return withContext(ioDispatcher) { + if (runningHandle.get() == true && tunnelConfig == vpnState.value.tunnelConfig) { + Timber.w("Tunnel already running") + return@withContext Result.success(vpnState.value.status) + } + runningHandle.set(true) onBeforeStart(tunnelConfig) - if (background) startBackgroundService() + val settings = appDataRepository.settings.getSettings() + if (background || settings.isKernelEnabled) startBackgroundService() setState(tunnelConfig, TunnelState.UP).onSuccess { emitTunnelState(it) }.onFailure { @@ -112,6 +121,7 @@ constructor( onStopFailed() }.also { stopBackgroundService() + runningHandle.set(false) } } } @@ -146,6 +156,7 @@ constructor( } cancelStatsJob() resetBackendStatistics() + runningHandle.set(false) } private suspend fun shutDownActiveTunnel(config: TunnelConfig) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt index eeffcc8..2177f5c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig @@ -60,6 +61,7 @@ import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.extensions.isBatteryOptimizationsDisabled import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl +import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight @OptIn(ExperimentalFoundationApi::class) @Composable @@ -213,7 +215,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) ) LazyColumn( horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, + verticalArrangement = Arrangement.spacedBy(5.dp.scaledHeight(), Alignment.Top), modifier = Modifier .fillMaxSize().padding(it) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt index d3ce4e9..4de62be 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt @@ -9,6 +9,7 @@ import com.wireguard.android.util.RootShell import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.module.AppShell import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.util.FileUtils @@ -29,7 +30,7 @@ class SettingsViewModel @Inject constructor( private val appDataRepository: AppDataRepository, - private val rootShell: Provider, + @AppShell private val rootShell: Provider, private val fileUtils: FileUtils, @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) : ViewModel() { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelViewModel.kt index 55c787c..83104ba 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelViewModel.kt @@ -6,6 +6,7 @@ import com.wireguard.android.util.RootShell import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository +import com.zaneschepke.wireguardautotunnel.module.AppShell import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.util.StringValue @@ -23,7 +24,7 @@ class AutoTunnelViewModel @Inject constructor( private val appDataRepository: AppDataRepository, - private val rootShell: Provider, + @AppShell private val rootShell: Provider, @IoDispatcher private val ioDispatcher: CoroutineDispatcher, ) : ViewModel() { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt index 03375d1..89a15fc 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt @@ -21,7 +21,6 @@ object Constants { const val SYSTEM_EXEMPT_SERVICE_TYPE_ID = 1024 const val SUBSCRIPTION_TIMEOUT = 5_000L - const val FOCUS_REQUEST_DELAY = 500L const val TRANSITION_ANIMATION_TIME = 200 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt index 34405a9..d3cdc28 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt @@ -4,8 +4,6 @@ import androidx.compose.ui.graphics.Color import com.wireguard.android.util.RootShell import com.wireguard.config.Peer import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus -import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState -import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree import com.zaneschepke.wireguardautotunnel.ui.theme.Straw @@ -23,10 +21,6 @@ fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? { return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis) } -fun VpnState.isDown(): Boolean { - return this.status == TunnelState.DOWN -} - fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus { // TODO add never connected status after duration return this.latestHandshakeSeconds().let { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1777125..04e744e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -225,4 +225,6 @@ Kernel not supported Start auto-tunnel Stop auto-tunnel + Tunnel running + Monitoring state changes