diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/NetworkQualifiers.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/NetworkQualifiers.kt new file mode 100644 index 0000000..f89048b --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/NetworkQualifiers.kt @@ -0,0 +1,15 @@ +package com.zaneschepke.wireguardautotunnel.module + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class Wifi + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class MobileData + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class Ethernet diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt index 0dfb304..af089fd 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/ServiceModule.kt @@ -13,15 +13,19 @@ import dagger.hilt.android.scopes.ServiceScoped @Module @InstallIn(ServiceComponent::class) abstract class ServiceModule { - @Binds - @ServiceScoped - abstract fun provideWifiService(wifiService: WifiService): NetworkService @Binds + @Wifi @ServiceScoped - abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService + abstract fun provideWifiService(wifiService: WifiService): NetworkService @Binds + @MobileData @ServiceScoped - abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService + abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService + + @Binds + @Ethernet + @ServiceScoped + abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelService.kt index c127f6f..eeaf958 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelService.kt @@ -1,7 +1,6 @@ package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel import android.content.Intent -import android.net.NetworkCapabilities import android.os.IBinder import android.os.PowerManager import androidx.core.app.ServiceCompat @@ -13,17 +12,16 @@ 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.Ethernet import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher +import com.zaneschepke.wireguardautotunnel.module.MobileData +import com.zaneschepke.wireguardautotunnel.module.Wifi import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelEvent import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.AutoTunnelState import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.NetworkState -import com.zaneschepke.wireguardautotunnel.service.network.EthernetService -import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService import com.zaneschepke.wireguardautotunnel.service.network.NetworkService -import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus -import com.zaneschepke.wireguardautotunnel.service.network.WifiService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationAction import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification @@ -31,7 +29,6 @@ import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage -import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable import com.zaneschepke.wireguardautotunnel.util.extensions.onNotRunning import dagger.hilt.android.AndroidEntryPoint @@ -64,13 +61,16 @@ class AutoTunnelService : LifecycleService() { lateinit var rootShell: Provider @Inject - lateinit var wifiService: NetworkService + @Wifi + lateinit var wifiService: NetworkService @Inject - lateinit var mobileDataService: NetworkService + @MobileData + lateinit var mobileDataService: NetworkService @Inject - lateinit var ethernetService: NetworkService + @Ethernet + lateinit var ethernetService: NetworkService @Inject lateinit var appDataRepository: Provider @@ -242,6 +242,7 @@ class AutoTunnelService : LifecycleService() { ) { double, networkState -> AutoTunnelState(tunnelService.get().vpnState.value, networkState, double.first, double.second) }.collect { state -> + Timber.d("Network state: ${state.networkState}") autoTunnelStateFlow.update { it.copy(vpnState = state.vpnState, networkState = state.networkState, settings = state.settings, tunnels = state.tunnels) } @@ -256,19 +257,14 @@ class AutoTunnelService : LifecycleService() { @OptIn(FlowPreview::class) private fun combineNetworkEventsJob(): Flow { return combine( - wifiService.networkStatus, - mobileDataService.networkStatus, - ethernetService.networkStatus, - ) { wifi, mobileData, ethernet -> + wifiService.status, + mobileDataService.status, + ) { wifi, mobileData -> NetworkState( - wifi.isConnected, - mobileData.isConnected, - ethernet.isConnected, - when (wifi) { - is NetworkStatus.CapabilitiesChanged -> getWifiSSID(wifi.networkCapabilities) - is NetworkStatus.Available -> autoTunnelStateFlow.value.networkState.wifiName - is NetworkStatus.Unavailable -> null - }, + wifi.available, + mobileData.available, + false, + wifi.name ) }.distinctUntilChanged().filterNot { it.isWifiConnected && it.wifiName == null }.debounce(500L) } @@ -285,28 +281,6 @@ class AutoTunnelService : LifecycleService() { }.distinctUntilChanged() } - private suspend fun getWifiSSID(networkCapabilities: NetworkCapabilities): String? { - return withContext(ioDispatcher) { - val settings = settings() - if (settings.isWifiNameByShellEnabled) return@withContext rootShell.get().getCurrentWifiName() - wifiService.getNetworkName(networkCapabilities) - }.also { - if (it?.contains(Constants.UNREADABLE_SSID) == true) { - Timber.w("SSID unreadable: missing permissions") - } else { - Timber.i("Detected valid SSID") - } - } - } - - private suspend fun settings(): Settings { - return if (autoTunnelStateFlow.value == defaultState) { - appDataRepository.get().settings.getSettings() - } else { - autoTunnelStateFlow.value.settings - } - } - @OptIn(FlowPreview::class) private fun startAutoTunnelJob() = lifecycleScope.launch(ioDispatcher) { Timber.i("Starting auto-tunnel network event watcher") diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt deleted file mode 100644 index 4c87665..0000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/BaseNetworkService.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.zaneschepke.wireguardautotunnel.service.network - -import android.content.Context -import android.net.ConnectivityManager -import android.net.Network -import android.net.NetworkCapabilities -import android.net.NetworkRequest -import android.net.wifi.WifiManager -import android.os.Build -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart -import timber.log.Timber - -abstract class BaseNetworkService>( - val context: Context, - networkCapability: Int, -) : NetworkService { - private val connectivityManager = - context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - - val wifiManager = - context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager - - private fun checkHasCapability(networkCapability: Int): Boolean { - val network = connectivityManager.activeNetwork - val networkCapabilities = connectivityManager.getNetworkCapabilities(network) - return networkCapabilities?.hasTransport(networkCapability) == true - } - - override val networkStatus = - callbackFlow { - val networkStatusCallback = - when (Build.VERSION.SDK_INT) { - in Build.VERSION_CODES.S..Int.MAX_VALUE -> { - object : - ConnectivityManager.NetworkCallback( - FLAG_INCLUDE_LOCATION_INFO, - ) { - override fun onAvailable(network: Network) { - trySend(NetworkStatus.Available(network)) - } - - override fun onLost(network: Network) { - trySend(NetworkStatus.Unavailable()) - } - - override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { - trySend( - NetworkStatus.CapabilitiesChanged( - network, - networkCapabilities, - ), - ) - } - } - } - - else -> { - object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - trySend(NetworkStatus.Available(network)) - } - - override fun onLost(network: Network) { - trySend(NetworkStatus.Unavailable()) - } - - override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { - trySend( - NetworkStatus.CapabilitiesChanged( - network, - networkCapabilities, - ), - ) - } - } - } - } - val request = - NetworkRequest.Builder() - .addTransportType(networkCapability) - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - .build() - connectivityManager.registerNetworkCallback(request, networkStatusCallback) - - awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) } - }.onStart { - // needed for services that are not yet available as it will impact later combine flows if we don't emit - emit(NetworkStatus.Unavailable()) - }.catch { - Timber.e(it) - emit(NetworkStatus.Unavailable()) - } -} - -inline fun Flow.map( - crossinline onUnavailable: suspend () -> Result, - crossinline onAvailable: suspend (network: Network) -> Result, - crossinline onCapabilitiesChanged: - suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result, -): Flow = map { status -> - when (status) { - is NetworkStatus.Unavailable -> onUnavailable() - is NetworkStatus.Available -> onAvailable(status.network) - is NetworkStatus.CapabilitiesChanged -> - onCapabilitiesChanged( - status.network, - status.networkCapabilities, - ) - } -} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/EthernetService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/EthernetService.kt index 1ce2e66..efc4872 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/EthernetService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/EthernetService.kt @@ -1,18 +1,64 @@ package com.zaneschepke.wireguardautotunnel.service.network import android.content.Context +import android.net.ConnectivityManager +import android.net.Network import android.net.NetworkCapabilities +import android.net.NetworkRequest import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import timber.log.Timber import javax.inject.Inject class EthernetService @Inject constructor( @ApplicationContext context: Context, -) : - BaseNetworkService(context, NetworkCapabilities.TRANSPORT_ETHERNET) { +) : NetworkService { - override fun isNetworkSecure(): Boolean { - return true + private val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + override val status = callbackFlow { + val networkStatusCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + trySend(NetworkStatus.Available(network)) + } + override fun onLost(network: Network) { + trySend(NetworkStatus.Unavailable()) + } + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + trySend( + NetworkStatus.CapabilitiesChanged( + network, + networkCapabilities, + ), + ) + } + } + val request = + NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + .build() + connectivityManager.registerNetworkCallback(request, networkStatusCallback) + + awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) } + }.onStart { + // needed for services that are not yet available as it will impact later combine flows if we don't emit + emit(NetworkStatus.Unavailable()) + }.catch { + Timber.e(it) + emit(NetworkStatus.Unavailable()) + }.map { + when (it) { + is NetworkStatus.Available, is NetworkStatus.CapabilitiesChanged -> Status(true, null) + is NetworkStatus.Unavailable -> Status(false, null) + } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/MobileDataService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/MobileDataService.kt index 63ff72d..6899a2d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/MobileDataService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/MobileDataService.kt @@ -1,17 +1,63 @@ package com.zaneschepke.wireguardautotunnel.service.network import android.content.Context +import android.net.ConnectivityManager +import android.net.Network import android.net.NetworkCapabilities +import android.net.NetworkRequest import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import timber.log.Timber import javax.inject.Inject class MobileDataService @Inject constructor( @ApplicationContext context: Context, -) : - BaseNetworkService(context, NetworkCapabilities.TRANSPORT_CELLULAR) { - override fun isNetworkSecure(): Boolean { - return false +) : NetworkService { + private val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + override val status = callbackFlow { + val networkStatusCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + trySend(NetworkStatus.Available(network)) + } + override fun onLost(network: Network) { + trySend(NetworkStatus.Unavailable()) + } + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + trySend( + NetworkStatus.CapabilitiesChanged( + network, + networkCapabilities, + ), + ) + } + } + val request = + NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + .build() + connectivityManager.registerNetworkCallback(request, networkStatusCallback) + + awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) } + }.onStart { + // needed for services that are not yet available as it will impact later combine flows if we don't emit + emit(NetworkStatus.Unavailable()) + }.catch { + Timber.e(it) + emit(NetworkStatus.Unavailable()) + }.map { + when (it) { + is NetworkStatus.Available, is NetworkStatus.CapabilitiesChanged -> Status(true, null) + is NetworkStatus.Unavailable -> Status(false, null) + } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkService.kt index 385851f..6c46076 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/NetworkService.kt @@ -1,14 +1,27 @@ package com.zaneschepke.wireguardautotunnel.service.network +import android.net.Network import android.net.NetworkCapabilities import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map -interface NetworkService { - fun getNetworkName(networkCapabilities: NetworkCapabilities): String? { - return null - } - - fun isNetworkSecure(): Boolean - - val networkStatus: Flow +interface NetworkService { + val status: Flow +} + +inline fun Flow.map( + crossinline onUnavailable: suspend () -> Result, + crossinline onAvailable: suspend (network: Network) -> Result, + crossinline onCapabilitiesChanged: + suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result, +): Flow = map { status -> + when (status) { + is NetworkStatus.Unavailable -> onUnavailable() + is NetworkStatus.Available -> onAvailable(status.network) + is NetworkStatus.CapabilitiesChanged -> + onCapabilitiesChanged( + status.network, + status.networkCapabilities, + ) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/Status.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/Status.kt new file mode 100644 index 0000000..6a6bf91 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/Status.kt @@ -0,0 +1,6 @@ +package com.zaneschepke.wireguardautotunnel.service.network + +data class Status( + val available: Boolean, + val name: String?, +) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/WifiService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/WifiService.kt index 7a56835..dc27d4b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/WifiService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/network/WifiService.kt @@ -1,22 +1,134 @@ package com.zaneschepke.wireguardautotunnel.service.network import android.content.Context +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback.FLAG_INCLUDE_LOCATION_INFO +import android.net.Network import android.net.NetworkCapabilities +import android.net.NetworkRequest import android.net.wifi.SupplicantState +import android.net.wifi.WifiManager import android.os.Build +import com.wireguard.android.util.RootShell +import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository +import com.zaneschepke.wireguardautotunnel.module.AppShell +import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.transform +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import timber.log.Timber import javax.inject.Inject +import javax.inject.Provider class WifiService @Inject constructor( - @ApplicationContext context: Context, -) : - BaseNetworkService(context, NetworkCapabilities.TRANSPORT_WIFI) { + @ApplicationContext private val context: Context, + private val settingsRepository: SettingsRepository, + @AppShell private val rootShell: Provider +) : NetworkService { - override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? { + val mutex = Mutex() + + private var ssid : String? = null + private var available : Boolean = false + + private val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + override val status = callbackFlow { + val networkStatusCallback = + when (Build.VERSION.SDK_INT) { + in Build.VERSION_CODES.S..Int.MAX_VALUE -> { + object : + ConnectivityManager.NetworkCallback( + FLAG_INCLUDE_LOCATION_INFO, + ) { + override fun onAvailable(network: Network) { + trySend(NetworkStatus.Available(network)) + } + + override fun onLost(network: Network) { + trySend(NetworkStatus.Unavailable()) + } + + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + trySend( + NetworkStatus.CapabilitiesChanged( + network, + networkCapabilities, + ), + ) + } + } + } + + else -> { + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + trySend(NetworkStatus.Available(network)) + } + + override fun onLost(network: Network) { + trySend(NetworkStatus.Unavailable()) + } + + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + trySend( + NetworkStatus.CapabilitiesChanged( + network, + networkCapabilities, + ), + ) + } + } + } + } + val request = + NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + .build() + connectivityManager.registerNetworkCallback(request, networkStatusCallback) + + awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) } + }.onStart { + // needed for services that are not yet available as it will impact later combine flows if we don't emit + emit(NetworkStatus.Unavailable()) + }.catch { + Timber.e(it) + emit(NetworkStatus.Unavailable()) + }.transform { + when(it) { + is NetworkStatus.Available -> mutex.withLock { + available = true + } + is NetworkStatus.CapabilitiesChanged -> mutex.withLock { + if(available) { + available = false + Timber.d("Getting SSID from capabilities") + ssid = getNetworkName(it.networkCapabilities) + } + emit(Status(true, ssid)) + } + is NetworkStatus.Unavailable -> emit(Status(false, null)) + } + } + + private suspend fun getNetworkName(networkCapabilities: NetworkCapabilities): String? { + if(settingsRepository.getSettings().isWifiNameByShellEnabled) return rootShell.get().getCurrentWifiName() var ssid = networkCapabilities.getWifiName() if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) { + val wifiManager = + context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + + @Suppress("DEPRECATION") val info = wifiManager.connectionInfo if (info.supplicantState === SupplicantState.COMPLETED) { ssid = info.ssid @@ -24,9 +136,4 @@ constructor( } return ssid?.trim('"') } - - override fun isNetworkSecure(): Boolean { - // TODO - return false - } }