From 003e47db3af07ee8f58bb97ee22681241bf9c17b Mon Sep 17 00:00:00 2001 From: asivery Date: Sat, 7 Oct 2023 02:57:05 +0200 Subject: [PATCH 1/6] ui/tunnel: make a tunnel automatically disconnect when connecting to one of specified Wi-Fi networks Signed-off-by: asivery --- gradle/libs.versions.toml | 1 + .../wireguard/config/BadConfigException.java | 3 +- .../java/com/wireguard/config/Config.java | 62 ++++++++++++- ui/build.gradle.kts | 1 + ui/src/main/AndroidManifest.xml | 5 ++ .../wireguard/android/model/TunnelManager.kt | 87 +++++++++++++++++++ .../android/viewmodel/ConfigProxy.kt | 28 +++++- .../res/layout/tunnel_detail_fragment.xml | 53 ++++++++++- .../res/layout/tunnel_editor_fragment.xml | 56 ++++++++++++ ui/src/main/res/values/strings.xml | 6 ++ 10 files changed, 296 insertions(+), 6 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 408d84cba..fe8309d36 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,6 +21,7 @@ jsr305 = "com.google.code.findbugs:jsr305:3.0.2" junit = "junit:junit:4.13.2" kotlinx-coroutines-android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0" zxing-android-embedded = "com.journeyapps:zxing-android-embedded:4.3.0" +androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version = "2.8.1" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/tunnel/src/main/java/com/wireguard/config/BadConfigException.java b/tunnel/src/main/java/com/wireguard/config/BadConfigException.java index db022e149..ff65bfb65 100644 --- a/tunnel/src/main/java/com/wireguard/config/BadConfigException.java +++ b/tunnel/src/main/java/com/wireguard/config/BadConfigException.java @@ -99,7 +99,8 @@ public enum Reason { MISSING_SECTION, SYNTAX_ERROR, UNKNOWN_ATTRIBUTE, - UNKNOWN_SECTION + UNKNOWN_SECTION, + INVALID_ADDITIONAL_CONFIG } public enum Section { diff --git a/tunnel/src/main/java/com/wireguard/config/Config.java b/tunnel/src/main/java/com/wireguard/config/Config.java index ee9cebce9..7b9d975ff 100644 --- a/tunnel/src/main/java/com/wireguard/config/Config.java +++ b/tunnel/src/main/java/com/wireguard/config/Config.java @@ -33,10 +33,15 @@ public final class Config { private final Interface interfaze; private final List peers; + private final boolean enableAutoDisconnect; + private final String autoDisconnectNetworks; + private Config(final Builder builder) { interfaze = Objects.requireNonNull(builder.interfaze, "An [Interface] section is required"); // Defensively copy to ensure immutability even if the Builder is reused. peers = Collections.unmodifiableList(new ArrayList<>(builder.peers)); + enableAutoDisconnect = builder.enableAutoDisconnect; + autoDisconnectNetworks = builder.autoDisconnectNetworks; } /** @@ -71,8 +76,25 @@ public static Config parse(final BufferedReader reader) @Nullable String line; while ((line = reader.readLine()) != null) { final int commentIndex = line.indexOf('#'); - if (commentIndex != -1) + if (commentIndex != -1) { + final String commentData = line.substring(commentIndex); + if(commentData.startsWith("#ADD;")){ + final String[] tokens = commentData.split(";", 3); + if(tokens.length != 3){ + throw new BadConfigException(Section.CONFIG, Location.TOP_LEVEL, Reason.INVALID_ADDITIONAL_CONFIG, line); + } + final String tokenName = tokens[1]; + final String tokenValue = tokens[2]; + switch (tokenName) { + case "wifi_auto_disconnect" -> + builder.setAutoDisconnect("1".equalsIgnoreCase(tokenValue)); + case "wifi_auto_disconnect_networks" -> + builder.setAutoDisconnectNetworks(tokenValue); + } + continue; + } line = line.substring(0, commentIndex); + } line = line.trim(); if (line.isEmpty()) continue; @@ -120,6 +142,27 @@ public boolean equals(final Object obj) { return interfaze.equals(other.interfaze) && peers.equals(other.peers); } + + /** + * Returns whether or not the connection should be disconnected, once the user connects to a + * specified network. + * + * @return whether or not the connection should be disconnected, once the user connects to a + * specified network + */ + public boolean isAutoDisconnectEnabled() { + return enableAutoDisconnect; + } + + /** + * Returns the list of Wi-Fi networks that should cause this connection to disconnect. + * + * @return the list of Wi-Fi networks that should cause this connection to disconnect + */ + public String getAutoDisconnectNetworks(){ + return autoDisconnectNetworks; + } + /** * Returns the interface section of the configuration. * @@ -140,7 +183,7 @@ public List getPeers() { @Override public int hashCode() { - return 31 * interfaze.hashCode() + peers.hashCode(); + return 33 * (enableAutoDisconnect ? 1 : 0) + 32 * autoDisconnectNetworks.hashCode() + 31 * interfaze.hashCode() + peers.hashCode(); } /** @@ -165,6 +208,8 @@ public String toWgQuickString() { sb.append("[Interface]\n").append(interfaze.toWgQuickString()); for (final Peer peer : peers) sb.append("\n[Peer]\n").append(peer.toWgQuickString()); + sb.append("\n#ADD;wifi_auto_disconnect;").append(enableAutoDisconnect ? '1' : '0'); + sb.append("\n#ADD;wifi_auto_disconnect_networks;").append(autoDisconnectNetworks); return sb.toString(); } @@ -189,11 +234,24 @@ public static final class Builder { // No default; must be provided before building. @Nullable private Interface interfaze; + private boolean enableAutoDisconnect = false; + private String autoDisconnectNetworks = ""; + public Builder addPeer(final Peer peer) { peers.add(peer); return this; } + public Builder setAutoDisconnect(final boolean enable){ + enableAutoDisconnect = enable; + return this; + } + + public Builder setAutoDisconnectNetworks(final String networks){ + autoDisconnectNetworks = networks; + return this; + } + public Builder addPeers(final Collection peers) { this.peers.addAll(peers); return this; diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index 5cdcb6a1c..aebd87a75 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -80,6 +80,7 @@ dependencies { implementation(libs.google.material) implementation(libs.zxing.android.embedded) implementation(libs.kotlinx.coroutines.android) + implementation(libs.androidx.work.runtime.ktx) coreLibraryDesugaring(libs.desugarJdkLibs) } diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml index 72617f3b1..017160d1a 100644 --- a/ui/src/main/AndroidManifest.xml +++ b/ui/src/main/AndroidManifest.xml @@ -7,6 +7,11 @@ + + + + + ") -> null + this.startsWith("\"") -> this.substring(1, this.length - 1) + else -> null + } + } + + applicationScope.launch { + if (ssid == null) { + Log.e(WIFI_TAG, "Cannot detect Wi-Fi name - missing permissions!") + return@launch + } + val manager = getTunnelManager() + val tunnels = manager.getTunnels().filter { tunnel -> + tunnel.getConfigAsync().isAutoDisconnectEnabled && tunnel.getConfigAsync().autoDisconnectNetworks.contains(ssid) + } + tunnels.forEach { tunnel -> + try { + manager.setTunnelState(tunnel, Tunnel.State.DOWN) + Log.d(WIFI_TAG, "Disabled tunnel ${tunnel.name}") + } catch (e: Throwable) { + Log.d(WIFI_TAG, ErrorMessages[e]) + } + } + } + } + } + + fun doWork() { + val connectivityManager = + context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val networkRequest = NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build() + try { + connectivityManager.unregisterNetworkCallback(networkCallback) + } catch (_: Exception) { + } + connectivityManager.registerNetworkCallback(networkRequest, networkCallback) + } + } + + class WifiWorker(val context: Context, parameters: WorkerParameters) : Worker(context, parameters) { + private val generic = GenericWifiWorker(context) + override fun doWork(): Result { + generic.doWork() + return Result.success() + } + } + suspend fun getTunnelState(tunnel: ObservableTunnel): Tunnel.State = withContext(Dispatchers.Main.immediate) { tunnel.onStateChanged(withContext(Dispatchers.IO) { getBackend().getState(tunnel) }) } @@ -251,5 +337,6 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { companion object { private const val TAG = "WireGuard/TunnelManager" + private const val WIFI_TAG = "WireGuard/TunnelManager/WIFI" } } diff --git a/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt index c73b1efce..a27ca7527 100644 --- a/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt +++ b/ui/src/main/java/com/wireguard/android/viewmodel/ConfigProxy.kt @@ -8,16 +8,34 @@ import android.os.Build import android.os.Parcel import android.os.Parcelable import androidx.core.os.ParcelCompat +import androidx.databinding.BaseObservable +import androidx.databinding.Bindable import androidx.databinding.ObservableArrayList import androidx.databinding.ObservableList +import com.wireguard.android.BR import com.wireguard.config.BadConfigException import com.wireguard.config.Config import com.wireguard.config.Peer -class ConfigProxy : Parcelable { +class ConfigProxy : BaseObservable, Parcelable { val `interface`: InterfaceProxy val peers: ObservableList = ObservableArrayList() + @get:Bindable + var enableAutoDisconnect = false + set(value) { + field = value + notifyPropertyChanged(BR.enableAutoDisconnect) + } + + @get:Bindable + var autoDisconnectNetworks: String = "" + set(value) { + field = value + notifyPropertyChanged(BR.autoDisconnectNetworks) + } + + private constructor(parcel: Parcel) { `interface` = ParcelCompat.readParcelable(parcel, InterfaceProxy::class.java.classLoader, InterfaceProxy::class.java) ?: InterfaceProxy() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -25,6 +43,8 @@ class ConfigProxy : Parcelable { } else { parcel.readTypedList(peers, PeerProxy.CREATOR) } + enableAutoDisconnect = ParcelCompat.readSerializable(parcel, Boolean::class.java.classLoader, Boolean::class.java) ?: false + autoDisconnectNetworks = ParcelCompat.readSerializable(parcel, String::class.java.classLoader, String::class.java) ?: String() peers.forEach { it.bind(this) } } @@ -35,6 +55,8 @@ class ConfigProxy : Parcelable { peers.add(proxy) proxy.bind(this) } + enableAutoDisconnect = other.isAutoDisconnectEnabled + autoDisconnectNetworks = other.autoDisconnectNetworks } constructor() { @@ -57,6 +79,8 @@ class ConfigProxy : Parcelable { return Config.Builder() .setInterface(`interface`.resolve()) .addPeers(resolvedPeers) + .setAutoDisconnect(enableAutoDisconnect) + .setAutoDisconnectNetworks(autoDisconnectNetworks) .build() } @@ -67,6 +91,8 @@ class ConfigProxy : Parcelable { } else { dest.writeTypedList(peers) } + dest.writeSerializable(enableAutoDisconnect) + dest.writeSerializable(autoDisconnectNetworks) } private class ConfigProxyCreator : Parcelable.Creator { diff --git a/ui/src/main/res/layout/tunnel_detail_fragment.xml b/ui/src/main/res/layout/tunnel_detail_fragment.xml index 332df04af..f4805f75a 100644 --- a/ui/src/main/res/layout/tunnel_detail_fragment.xml +++ b/ui/src/main/res/layout/tunnel_detail_fragment.xml @@ -304,6 +304,55 @@ + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/ui/src/main/res/layout/tunnel_editor_fragment.xml b/ui/src/main/res/layout/tunnel_editor_fragment.xml index 0350486ba..e3155ca97 100644 --- a/ui/src/main/res/layout/tunnel_editor_fragment.xml +++ b/ui/src/main/res/layout/tunnel_editor_fragment.xml @@ -261,6 +261,62 @@ tools:text="4 excluded applications" /> + + + + + + + + + + + + + Authenticate to view private key Authentication failure Authentication failure: %s + Wi-Fi Settings + Disconnect when using to one of these networks: + Network1, Network2, Network3 + Networks + Blacklisted Wi-Fi networks + <Disabled> From 75fe7a3ad6cb5e5dee69812449cb7a6aee96af3d Mon Sep 17 00:00:00 2001 From: asivery Date: Sun, 8 Oct 2023 00:51:43 +0200 Subject: [PATCH 2/6] ui: auto disable wifi worker when not needed Signed-off-by: asivery --- .../wireguard/android/model/TunnelManager.kt | 62 +++++++++++++------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt index 26f1f5a78..bb2c5ab7c 100644 --- a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt +++ b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt @@ -115,30 +115,45 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { applicationScope.launch { try { onTunnelsLoaded(withContext(Dispatchers.IO) { configStore.enumerate() }, withContext(Dispatchers.IO) { getBackend().runningTunnelNames }) - PeriodicWorkRequest - .Builder( - WifiWorker::class.java, 15, - TimeUnit.MINUTES, 5, TimeUnit.MINUTES - ) - .setConstraints( - Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .setRequiredNetworkType(NetworkType.UNMETERED) - .build() - ) - .build() - .also { - WorkManager.getInstance(context) - .enqueueUniquePeriodicWork("WIFI_WORKER", ExistingPeriodicWorkPolicy.UPDATE, it) - } - // Fire once, at the start - GenericWifiWorker(context).doWork() + loadOrUnloadWifiWorker() } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) } } } + private suspend fun loadOrUnloadWifiWorker() { + val workerTag = "WIFI_WORKER" + val hasAnyTunnelsNeedingWifi = + getTunnels().any { tunnel -> getTunnelState(tunnel) == Tunnel.State.UP && tunnel.getConfigAsync().isAutoDisconnectEnabled } + if (hasAnyTunnelsNeedingWifi) { + Log.d(WIFI_TAG, "There's one or more tunnels that check for Wi-Fi connectivity - (re)loading worker") + PeriodicWorkRequest + .Builder( + WifiWorker::class.java, 15, + TimeUnit.MINUTES, 5, TimeUnit.MINUTES + ) + .addTag(workerTag) + .setConstraints( + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiredNetworkType(NetworkType.UNMETERED) + .build() + ) + .build() + .also { + WorkManager.getInstance(context) + .enqueueUniquePeriodicWork("WIFI_WORKER", ExistingPeriodicWorkPolicy.UPDATE, it) + } + // Fire once, at the start + GenericWifiWorker(context).doWork() + } else { + // Kill the worker - no need to keep it running + Log.d(WIFI_TAG, "There's no more connected tunnels that check for Wi-Fi functionality - killing worker") + WorkManager.getInstance(context).cancelAllWorkByTag(workerTag) + } + } + private fun onTunnelsLoaded(present: Iterable, running: Collection) { for (name in present) addToList(name, null, if (running.contains(name)) Tunnel.State.UP else Tunnel.State.DOWN) @@ -239,6 +254,12 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { saveState() if (throwable != null) throw throwable + try { + if (tunnel.getConfigAsync().isAutoDisconnectEnabled) + loadOrUnloadWifiWorker() + } catch (e: Throwable) { + // Ignore + } newState } @@ -273,6 +294,9 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { } class GenericWifiWorker(val context: Context) { + private var connectivityManager = + context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + private var networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager @@ -306,8 +330,6 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { } fun doWork() { - val connectivityManager = - context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val networkRequest = NetworkRequest.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) .build() From 6ac0916747c839df4cb5f232ced98692ea4366b0 Mon Sep 17 00:00:00 2001 From: asivery Date: Mon, 9 Oct 2023 01:58:56 +0200 Subject: [PATCH 3/6] ui: ask for location permissions when enabling auto-disconnect Signed-off-by: asivery --- .../android/fragment/TunnelEditorFragment.kt | 99 ++++++++++++++++++- ui/src/main/res/values/strings.xml | 4 + 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt index edf4b2267..d718130df 100644 --- a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt +++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt @@ -4,7 +4,12 @@ */ package com.wireguard.android.fragment +import android.Manifest +import android.app.AlertDialog import android.content.Context +import android.content.DialogInterface +import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import android.text.InputType import android.util.Log @@ -18,6 +23,9 @@ import android.view.WindowManager import android.view.inputmethod.InputMethodManager import android.widget.EditText import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat import androidx.core.os.BundleCompat import androidx.core.view.MenuProvider import androidx.lifecycle.Lifecycle @@ -34,6 +42,9 @@ import com.wireguard.android.util.ErrorMessages import com.wireguard.android.viewmodel.ConfigProxy import com.wireguard.config.Config import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine /** * Fragment for editing a WireGuard configuration. @@ -112,10 +123,29 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider { selectedTunnel = tunnel } + private lateinit var requestPermissionLauncher: ActivityResultLauncher> + private var requestPermissionCallback: ((permissions: Map) -> Unit)? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { + requestPermissionCallback?.invoke(it); + } + } + + private suspend fun suspendRequestPermissions(permissions: Array): Map { + return suspendCoroutine { cont -> + requestPermissionCallback = { + cont.resume(it) + requestPermissionCallback = null + } + requestPermissionLauncher?.launch(permissions) + } + } + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { if (menuItem.itemId == R.id.menu_action_save) { binding ?: return false - val newConfig = try { + var newConfig = try { binding!!.config!!.resolve() } catch (e: Throwable) { val error = ErrorMessages[e] @@ -127,6 +157,73 @@ class TunnelEditorFragment : BaseFragment(), MenuProvider { } val activity = requireActivity() activity.lifecycleScope.launch { + if (newConfig.isAutoDisconnectEnabled) run { + val disableFeature = { + // The user has changed their mind - disable the feature, but leave the network list in place + newConfig = Config.Builder() + .setInterface(newConfig.`interface`) + .addPeers(newConfig.peers) + .setAutoDisconnect(false) + .setAutoDisconnectNetworks(newConfig.autoDisconnectNetworks) + .build() + } + + val requiredPermissions = arrayListOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + requiredPermissions.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + } + val allRequiredPermissionsGranted = + requiredPermissions.all { ContextCompat.checkSelfPermission(activity, it) == PackageManager.PERMISSION_GRANTED } + if (!allRequiredPermissionsGranted) { + val shouldAsk = suspendCancellableCoroutine { cont -> + AlertDialog.Builder(activity) + .setMessage(R.string.wifi_location_needed_message) + .setTitle(R.string.wifi_location_needed_title) + .create() + .run { + val listener = DialogInterface.OnClickListener { _, button -> + when (button) { + AlertDialog.BUTTON_POSITIVE -> cont.resume(true) + else -> cont.resume(false) + } + } + cont.invokeOnCancellation { this.dismiss() } + setButton(AlertDialog.BUTTON_POSITIVE, context.getText(R.string.wifi_location_ok), listener) + setButton(AlertDialog.BUTTON_NEGATIVE, context.getText(R.string.wifi_location_cancel), listener) + show() + } + } + if (!shouldAsk) { + disableFeature() + return@run + } + + // Ask for the permissions + val basePermissionsGranted = suspendRequestPermissions( + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + ).values.all { it } + if (!basePermissionsGranted) { + disableFeature() + return@run + } + + // If we're not on android Q, we're done - all required permissions granted + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return@run + } + + // If we're on android Q or more, the background location needs its separate permission - it can't be + // requested together with the 2 base location ones. + val backgroundPermissionGranted = suspendRequestPermissions(arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION)).values.all { it } + if (!backgroundPermissionGranted) { + disableFeature() + } + } + } + when { tunnel == null -> { Log.d(TAG, "Attempting to create new tunnel " + binding!!.name) diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index c23944be3..2bf695580 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -263,4 +263,8 @@ Networks Blacklisted Wi-Fi networks <Disabled> + To use the auto-disconnect feature, you need to enable fine location access for this application. + Location permissions needed + Ok + Cancel From c4a26e99dd938ea03624f5d83e52fafcca4acb45 Mon Sep 17 00:00:00 2001 From: asivery Date: Mon, 9 Oct 2023 15:29:56 +0200 Subject: [PATCH 4/6] ui: fix wifi ssid checks Signed-off-by: asivery --- ui/src/main/java/com/wireguard/android/model/TunnelManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt index bb2c5ab7c..8b272b6b5 100644 --- a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt +++ b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt @@ -315,7 +315,7 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { } val manager = getTunnelManager() val tunnels = manager.getTunnels().filter { tunnel -> - tunnel.getConfigAsync().isAutoDisconnectEnabled && tunnel.getConfigAsync().autoDisconnectNetworks.contains(ssid) + tunnel.getConfigAsync().isAutoDisconnectEnabled && tunnel.getConfigAsync().autoDisconnectNetworks.split(",").any{ it.trim() == ssid } } tunnels.forEach { tunnel -> try { From 9a8a1b4c11710496a683867c4fee6970aeb7d8ea Mon Sep 17 00:00:00 2001 From: asivery Date: Tue, 7 Nov 2023 01:51:28 +0100 Subject: [PATCH 5/6] ui: fix wifi service auto disconnecting on connection, if connected to blocked network Signed-off-by: asivery --- .../wireguard/android/model/TunnelManager.kt | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt index 8b272b6b5..49ab76aee 100644 --- a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt +++ b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt @@ -123,6 +123,7 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { } private suspend fun loadOrUnloadWifiWorker() { + if(!tunnels.isCompleted) return; // This should be loaded only if the tunnels have all been properly initialized val workerTag = "WIFI_WORKER" val hasAnyTunnelsNeedingWifi = getTunnels().any { tunnel -> getTunnelState(tunnel) == Tunnel.State.UP && tunnel.getConfigAsync().isAutoDisconnectEnabled } @@ -294,19 +295,29 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { } class GenericWifiWorker(val context: Context) { + private val initWifiName: String?; + init{ + initWifiName = getCurrentWifiName() + } + private fun getCurrentWifiName(): String? { + val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + val ssid: String? = wifiManager.connectionInfo.ssid.run { + when { + this.contains("") -> null + this.startsWith("\"") -> this.substring(1, this.length - 1) + else -> null + } + } + return ssid + } + private var connectivityManager = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager private var networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { - val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager - val ssid: String? = wifiManager.connectionInfo.ssid.run { - when { - this.contains("") -> null - this.startsWith("\"") -> this.substring(1, this.length - 1) - else -> null - } - } + val ssid = getCurrentWifiName() + if(ssid == initWifiName) return; applicationScope.launch { if (ssid == null) { From 17795f0ee75f6609dba329cc34321d0ffbc2695a Mon Sep 17 00:00:00 2001 From: asivery Date: Fri, 8 Mar 2024 03:17:00 +0100 Subject: [PATCH 6/6] ui/tunnel: Fix application hang after autoconnecting after a device reboot Signed-off-by: asivery --- .../com/wireguard/android/backend/Tunnel.java | 10 ++++++++++ .../android/model/ObservableTunnel.kt | 2 +- .../wireguard/android/model/TunnelManager.kt | 20 ++++++++++--------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java b/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java index 766df443e..3de9cc811 100644 --- a/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java +++ b/tunnel/src/main/java/com/wireguard/android/backend/Tunnel.java @@ -54,4 +54,14 @@ public static State of(final boolean running) { return running ? UP : DOWN; } } + + /** + * Enum class to represent the reason for why a tunnel's state has changed + */ + enum StateChangeReason { + USER_INTERACTION, + EXTERNAL_INTENT, + AUTO_START, + WIFI_WORKER; + } } diff --git a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt index aa237aeef..d18ce0cd4 100644 --- a/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt +++ b/ui/src/main/java/com/wireguard/android/model/ObservableTunnel.kt @@ -63,7 +63,7 @@ class ObservableTunnel internal constructor( suspend fun setStateAsync(state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) { if (state != this@ObservableTunnel.state) - manager.setTunnelState(this@ObservableTunnel, state) + manager.setTunnelState(this@ObservableTunnel, state, Tunnel.StateChangeReason.USER_INTERACTION) else this@ObservableTunnel.state } diff --git a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt index 49ab76aee..8960e5455 100644 --- a/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt +++ b/ui/src/main/java/com/wireguard/android/model/TunnelManager.kt @@ -187,7 +187,7 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { if (previouslyRunning.isEmpty()) return withContext(Dispatchers.IO) { try { - tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(Dispatchers.IO + SupervisorJob()) { setTunnelState(it, Tunnel.State.UP) } } + tunnelMap.filter { previouslyRunning.contains(it.name) }.map { async(Dispatchers.IO + SupervisorJob()) { setTunnelState(it, Tunnel.State.UP, Tunnel.StateChangeReason.AUTO_START) } } .awaitAll() } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) @@ -241,7 +241,7 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { newName!! } - suspend fun setTunnelState(tunnel: ObservableTunnel, state: Tunnel.State): Tunnel.State = withContext(Dispatchers.Main.immediate) { + suspend fun setTunnelState(tunnel: ObservableTunnel, state: Tunnel.State, reason: Tunnel.StateChangeReason): Tunnel.State = withContext(Dispatchers.Main.immediate) { var newState = tunnel.state var throwable: Throwable? = null try { @@ -255,11 +255,13 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { saveState() if (throwable != null) throw throwable - try { - if (tunnel.getConfigAsync().isAutoDisconnectEnabled) - loadOrUnloadWifiWorker() - } catch (e: Throwable) { - // Ignore + if(reason != Tunnel.StateChangeReason.AUTO_START){ + try { + if (tunnel.getConfigAsync().isAutoDisconnectEnabled) + loadOrUnloadWifiWorker() + } catch (e: Throwable) { + // Ignore + } } newState } @@ -286,7 +288,7 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { val tunnels = manager.getTunnels() val tunnel = tunnels[tunnelName] ?: return@launch try { - manager.setTunnelState(tunnel, state) + manager.setTunnelState(tunnel, state, Tunnel.StateChangeReason.EXTERNAL_INTENT) } catch (e: Throwable) { Toast.makeText(context, ErrorMessages[e], Toast.LENGTH_LONG).show() } @@ -330,7 +332,7 @@ class TunnelManager(private val configStore: ConfigStore) : BaseObservable() { } tunnels.forEach { tunnel -> try { - manager.setTunnelState(tunnel, Tunnel.State.DOWN) + manager.setTunnelState(tunnel, Tunnel.State.DOWN, Tunnel.StateChangeReason.WIFI_WORKER) Log.d(WIFI_TAG, "Disabled tunnel ${tunnel.name}") } catch (e: Throwable) { Log.d(WIFI_TAG, ErrorMessages[e])