Compare commits

..

2 Commits

Author SHA1 Message Date
Zane Schepke c71c4e5b29 chore: bump version and notes 2025-03-19 22:35:21 -04:00
Zane Schepke 7f0fea3766 fix: improve wifi monitoring to better handle permission changes 2025-03-19 21:51:54 -04:00
11 changed files with 165 additions and 45 deletions
@@ -1,6 +1,8 @@
package com.zaneschepke.wireguardautotunnel
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Build
import android.os.Bundle
@@ -32,12 +34,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
@@ -68,6 +72,7 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import javax.inject.Inject
import kotlin.system.exitProcess
@@ -83,6 +88,11 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var shortcutManager: ShortcutManager
@Inject
lateinit var networkMonitor: NetworkMonitor
private var lastLocationPermissionState: Boolean? = null
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
@@ -256,4 +266,22 @@ class MainActivity : AppCompatActivity() {
}
}
}
override fun onResume() {
super.onResume()
checkPermissionAndNotify()
}
private fun checkPermissionAndNotify() {
val hasLocation = ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION,
) == PackageManager.PERMISSION_GRANTED
if (lastLocationPermissionState != hasLocation) {
Timber.d("Location permission changed to: $hasLocation")
if (hasLocation) {
networkMonitor.sendLocationPermissionsGrantedBroadcast()
}
lastLocationPermissionState = hasLocation
}
}
}
@@ -56,9 +56,6 @@ class ServiceManager @Inject constructor(
runCatching {
autoTunnelService = CompletableDeferred()
startService(AutoTunnelService::class.java, background)
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { autoTunnelService.await() }
?: throw IllegalStateException("AutoTunnelService start timed out")
service.start()
_autoTunnelActive.update { true }
}.onFailure {
Timber.e(it)
@@ -94,9 +94,11 @@ class AutoTunnelService : LifecycleService() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Timber.d("onStartCommand executed with startId: $startId")
serviceManager.autoTunnelService.complete(this)
return super.onStartCommand(intent, flags, startId)
start()
return START_STICKY
}
fun start() {
@@ -178,8 +180,8 @@ class AutoTunnelService : LifecycleService() {
combineSettings(),
appDataRepository.get().settings.flow
.distinctUntilChanged { old, new -> old.isKernelEnabled == new.isKernelEnabled } // Only emit when isKernelEnabled changes
.flatMapLatest { settings ->
networkMonitor.getNetworkStatusFlow(true, settings.isKernelEnabled)
.flatMapLatest {
networkMonitor.networkStatusFlow
.flowOn(ioDispatcher)
.map { buildNetworkState(it) }
}
@@ -228,7 +228,7 @@ abstract class BaseTunnel(
}
private suspend fun monitorNetworkStatus() {
networkMonitor.getNetworkStatusFlow(includeWifiSsid = false, useRootShell = false)
networkMonitor.networkStatusFlow
.flowOn(ioDispatcher)
.collectLatest { status ->
val isAvailable = status !is NetworkStatus.Disconnected
@@ -13,6 +13,7 @@ import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
import com.zaneschepke.wireguardautotunnel.core.tunnel.UserspaceTunnel
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -20,6 +21,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking
import org.amnezia.awg.backend.Backend
import org.amnezia.awg.backend.GoBackend
import org.amnezia.awg.backend.RootTunnelActionHandler
@@ -101,8 +103,8 @@ class TunnelModule {
@Provides
@Singleton
fun provideNetworkMonitor(@ApplicationContext context: Context): NetworkMonitor {
return AndroidNetworkMonitor(context)
fun provideNetworkMonitor(@ApplicationContext context: Context, settingsRepository: AppSettingRepository): NetworkMonitor {
return AndroidNetworkMonitor(context) { runBlocking { settingsRepository.get().isWifiNameByShellEnabled } }
}
@Singleton
+2 -2
View File
@@ -1,7 +1,7 @@
object Constants {
const val VERSION_NAME = "3.7.1"
const val VERSION_NAME = "3.7.2"
const val JVM_TARGET = "17"
const val VERSION_CODE = 37100
const val VERSION_CODE = 37200
const val TARGET_SDK = 35
const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -0,0 +1,5 @@
What's new:
- Auto tunnel regression fix
- Tile sync improvements
- Optimize wifi name querying
- Improve network monitoring permission checks
+1 -1
View File
@@ -20,7 +20,7 @@ pinLockCompose = "1.0.4"
roomVersion = "2.6.1"
timber = "5.0.1"
tunnel = "1.2.7"
androidGradlePlugin = "8.10.0-beta01"
androidGradlePlugin = "8.8.0-alpha05"
kotlin = "2.1.10"
ksp = "2.1.10-1.0.31"
composeBom = "2025.03.00"
+8 -6
View File
@@ -1,9 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"
tools:targetApi="29" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
</manifest>
@@ -1,60 +1,134 @@
package com.zaneschepke.networkmonitor
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.location.LocationManager
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 com.wireguard.android.util.RootShell
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.runBlocking
import timber.log.Timber
class AndroidNetworkMonitor(
context: Context,
private val useRootShellCallback: suspend () -> Boolean,
) : NetworkMonitor {
companion object {
const val LOCATION_GRANTED = "LOCATION_PERMISSIONS_GRANTED"
const val LOCATION_SERVICES_FILTER = "android.location.PROVIDERS_CHANGED"
}
private val appContext = context.applicationContext
private val packageName = appContext.packageName
private val connectivityManager = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager?
private val locationManager = appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private val rootShell = RootShell(context)
private var includeWifiSsid = false
private var useRootShell = false
@get:Synchronized @set:Synchronized
var currentSsid: String? = null
@get:Synchronized @set:Synchronized
var wifiConnected = false
data class WifiState(val connected: Boolean = false, val ssid: String? = null)
data class TransportState(val connected: Boolean = false)
private val wifiFlow: Flow<WifiState> = callbackFlow {
var currentSsid: String?
@Suppress("DEPRECATION")
fun getWifiSsid(): String? {
if (!includeWifiSsid || wifiManager == null) return null
return if (useRootShell) {
return if (runBlocking { useRootShellCallback() }) {
rootShell.getCurrentWifiName()
} else {
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf {
it != "<unknown>" && it.isNotEmpty()
if (wifiManager == null) return null
try {
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf { it.isNotEmpty() }
} catch (e: Exception) {
Timber.e(e)
null
}
}
}
fun handleUnknownWifi() {
val newSsid = getWifiSsid()
// Only update if new SSID is valid; preserve existing valid SSID otherwise
if (newSsid != null && newSsid != WifiManager.UNKNOWN_SSID) {
currentSsid = newSsid
trySend(WifiState(connected = wifiConnected, ssid = currentSsid))
} else if (currentSsid == null || currentSsid == WifiManager.UNKNOWN_SSID) {
currentSsid = newSsid
trySend(WifiState(connected = wifiConnected, ssid = currentSsid))
}
Timber.d("handleUnknownWifi: currentSsid=$currentSsid")
}
val locationPermissionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Timber.d("locationPermissionReceiver received intent with action: ${intent.action}")
if (intent.action == "$packageName.$LOCATION_GRANTED") {
Timber.d("Received update: Precise and all-the-time location permissions are enabled")
handleUnknownWifi()
}
}
}
val locationServicesReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == LOCATION_SERVICES_FILTER) {
val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
val isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
val isLocationServicesEnabled = isGpsEnabled || isNetworkEnabled
Timber.d("Location Services state changed. Enabled: $isLocationServicesEnabled, GPS: $isGpsEnabled, Network: $isNetworkEnabled")
if (isLocationServicesEnabled) handleUnknownWifi()
}
}
}
// Use RECEIVER_NOT_EXPORTED for Android 14+ compatibility
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
Context.RECEIVER_EXPORTED
} else {
0
}
appContext.registerReceiver(
locationPermissionReceiver,
IntentFilter("$packageName.$LOCATION_GRANTED"),
flags,
)
appContext.registerReceiver(
locationServicesReceiver,
IntentFilter(LOCATION_SERVICES_FILTER),
flags,
)
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Timber.d("Wi-Fi onAvailable: network=$network")
currentSsid = getWifiSsid()
wifiConnected = true
trySend(WifiState(connected = true, ssid = currentSsid))
}
override fun onLost(network: Network) {
Timber.d("Wi-Fi onLost: network=$network")
currentSsid = null
wifiConnected = false
trySend(WifiState(connected = false, ssid = null))
}
@@ -71,7 +145,11 @@ class AndroidNetworkMonitor(
connectivityManager.registerNetworkCallback(request, callback)
trySend(WifiState())
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
awaitClose {
connectivityManager.unregisterNetworkCallback(callback)
appContext.unregisterReceiver(locationPermissionReceiver)
appContext.unregisterReceiver(locationServicesReceiver)
}
}
private val cellularFlow: Flow<TransportState> = callbackFlow {
@@ -95,7 +173,9 @@ class AndroidNetworkMonitor(
connectivityManager.registerNetworkCallback(request, callback)
trySend(TransportState())
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
awaitClose {
connectivityManager.unregisterNetworkCallback(callback)
}
}
private val ethernetFlow: Flow<TransportState> = callbackFlow {
@@ -122,21 +202,24 @@ class AndroidNetworkMonitor(
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
override fun getNetworkStatusFlow(includeWifiSsid: Boolean, useRootShell: Boolean): Flow<NetworkStatus> {
this.includeWifiSsid = includeWifiSsid
this.useRootShell = useRootShell
return combine(wifiFlow, cellularFlow, ethernetFlow) { wifi, cellular, ethernet ->
val hasAnyConnection = wifi.connected || cellular.connected || ethernet.connected
if (hasAnyConnection) {
NetworkStatus.Connected(
wifiSsid = wifi.ssid,
wifiConnected = wifi.connected,
cellularConnected = cellular.connected,
ethernetConnected = ethernet.connected,
)
} else {
NetworkStatus.Disconnected
}.also { Timber.d("NetworkStatus: $it") }
}.distinctUntilChanged()
override val networkStatusFlow = combine(wifiFlow, cellularFlow, ethernetFlow) { wifi, cellular, ethernet ->
val hasAnyConnection = wifi.connected || cellular.connected || ethernet.connected
if (hasAnyConnection) {
NetworkStatus.Connected(
wifiSsid = wifi.ssid,
wifiConnected = wifi.connected,
cellularConnected = cellular.connected,
ethernetConnected = ethernet.connected,
)
} else {
NetworkStatus.Disconnected
}.also { Timber.d("NetworkStatus: $it") }
}.distinctUntilChanged()
override fun sendLocationPermissionsGrantedBroadcast() {
val action = "$packageName.$LOCATION_GRANTED"
val intent = Intent(action)
Timber.d("Sending broadcast: $action")
appContext.sendBroadcast(intent)
}
}
@@ -3,5 +3,6 @@ package com.zaneschepke.networkmonitor
import kotlinx.coroutines.flow.Flow
interface NetworkMonitor {
fun getNetworkStatusFlow(includeWifiSsid: Boolean, useRootShell: Boolean): Flow<NetworkStatus>
val networkStatusFlow: Flow<NetworkStatus>
fun sendLocationPermissionsGrantedBroadcast()
}