fix: improve location querying

closes #498
This commit is contained in:
Zane Schepke 2024-12-21 22:14:31 -05:00
parent fe45a2fad9
commit 25b986ef2f
9 changed files with 284 additions and 189 deletions

View File

@ -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

View File

@ -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<WifiService>
@Binds
@Wifi
@ServiceScoped
abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService<MobileDataService>
abstract fun provideWifiService(wifiService: WifiService): NetworkService
@Binds
@MobileData
@ServiceScoped
abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService<EthernetService>
abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService
@Binds
@Ethernet
@ServiceScoped
abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService
}

View File

@ -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<RootShell>
@Inject
lateinit var wifiService: NetworkService<WifiService>
@Wifi
lateinit var wifiService: NetworkService
@Inject
lateinit var mobileDataService: NetworkService<MobileDataService>
@MobileData
lateinit var mobileDataService: NetworkService
@Inject
lateinit var ethernetService: NetworkService<EthernetService>
@Ethernet
lateinit var ethernetService: NetworkService
@Inject
lateinit var appDataRepository: Provider<AppDataRepository>
@ -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<NetworkState> {
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")

View File

@ -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<T : BaseNetworkService<T>>(
val context: Context,
networkCapability: Int,
) : NetworkService<T> {
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 <Result> Flow<NetworkStatus>.map(
crossinline onUnavailable: suspend () -> Result,
crossinline onAvailable: suspend (network: Network) -> Result,
crossinline onCapabilitiesChanged:
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result,
): Flow<Result> = map { status ->
when (status) {
is NetworkStatus.Unavailable -> onUnavailable()
is NetworkStatus.Available -> onAvailable(status.network)
is NetworkStatus.CapabilitiesChanged ->
onCapabilitiesChanged(
status.network,
status.networkCapabilities,
)
}
}

View File

@ -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<EthernetService>(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)
}
}
}

View File

@ -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<MobileDataService>(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)
}
}
}

View File

@ -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<T> {
fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
return null
}
fun isNetworkSecure(): Boolean
val networkStatus: Flow<NetworkStatus>
interface NetworkService {
val status: Flow<Status>
}
inline fun <Result> Flow<NetworkStatus>.map(
crossinline onUnavailable: suspend () -> Result,
crossinline onAvailable: suspend (network: Network) -> Result,
crossinline onCapabilitiesChanged:
suspend (network: Network, networkCapabilities: NetworkCapabilities) -> Result,
): Flow<Result> = map { status ->
when (status) {
is NetworkStatus.Unavailable -> onUnavailable()
is NetworkStatus.Available -> onAvailable(status.network)
is NetworkStatus.CapabilitiesChanged ->
onCapabilitiesChanged(
status.network,
status.networkCapabilities,
)
}
}

View File

@ -0,0 +1,6 @@
package com.zaneschepke.wireguardautotunnel.service.network
data class Status(
val available: Boolean,
val name: String?,
)

View File

@ -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<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI) {
@ApplicationContext private val context: Context,
private val settingsRepository: SettingsRepository,
@AppShell private val rootShell: Provider<RootShell>
) : 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
}
}