From b8bd5ddd7fb4ba9d4d34dae5d85a7b79f6121822 Mon Sep 17 00:00:00 2001
From: alperozturk
Date: Tue, 21 Oct 2025 11:06:31 +0200
Subject: [PATCH 01/14] fix: connectivity service impl
# Conflicts:
# app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
---
.../network/ConnectivityServiceImpl.java | 211 +++++++-----------
1 file changed, 82 insertions(+), 129 deletions(-)
diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
index ad6f07a0456b..35fb96471091 100644
--- a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
+++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
@@ -12,7 +12,6 @@
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
-import android.net.NetworkInfo;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
@@ -25,8 +24,10 @@
import org.apache.commons.httpclient.HttpStatus;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
import androidx.annotation.NonNull;
-import androidx.core.net.ConnectivityManagerCompat;
import kotlin.jvm.functions.Function1;
class ConnectivityServiceImpl implements ConnectivityService {
@@ -39,6 +40,7 @@ class ConnectivityServiceImpl implements ConnectivityService {
private final ClientFactory clientFactory;
private final GetRequestBuilder requestBuilder;
private final WalledCheckCache walledCheckCache;
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final Handler mainThreadHandler = new Handler(Looper.getMainLooper());
static class GetRequestBuilder implements Function1 {
@@ -62,48 +64,61 @@ public GetMethod invoke(String url) {
@Override
public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) {
- new Thread(() -> {
- Network activeNetwork = platformConnectivityManager.getActiveNetwork();
- NetworkCapabilities networkCapabilities = platformConnectivityManager.getNetworkCapabilities(activeNetwork);
- boolean hasInternet = networkCapabilities != null && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
-
- boolean result;
- if (hasInternet) {
- result = !isInternetWalled();
- } else {
- Log_OC.e(TAG, "network and server not available");
- result = false;
- }
-
- mainThreadHandler.post(() -> callback.onComplete(result));
- }).start();
+ executor.submit(() -> {
+ boolean isAvailable = !isInternetWalled();
+ mainThreadHandler.post(() -> callback.onComplete(isAvailable));
+ });
}
+ /**
+ * Checks whether the device is currently connected to a network
+ * that has verified Internet access.
+ *
+ * This method performs multiple levels of validation:
+ *
+ * - Ensures there is an active network connection.
+ * - Retrieves and checks network capabilities.
+ * - Verifies that the active network provides and has validated Internet access.
+ * - Confirms that the network uses a supported transport type
+ * (Wi-Fi, Cellular, Ethernet, VPN, etc.).
+ *
+ *
+ * @return {@code true} if the device is connected to the Internet via a valid transport type;
+ * {@code false} otherwise.
+ */
@Override
public boolean isConnected() {
Network nw = platformConnectivityManager.getActiveNetwork();
- NetworkCapabilities actNw = platformConnectivityManager.getNetworkCapabilities(nw);
+ if (nw == null) {
+ return false;
+ }
+ NetworkCapabilities actNw = platformConnectivityManager.getNetworkCapabilities(nw);
if (actNw == null) {
- Log_OC.e(TAG, "network capabilities is null");
return false;
}
- if (actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
- actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
- actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ||
- actNw.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
- actNw.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) ||
- actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) {
- return true;
- }
+ // Verify that the network both claims to provide Internet
+ // and has been validated (i.e., Internet is actually reachable).
+ if (actNw.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && actNw.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
- actNw.hasTransport(NetworkCapabilities.TRANSPORT_USB)) {
- return true;
+ // Check if the active network uses one of the recognized transport types.
+ if (actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
+ actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
+ actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ||
+ actNw.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
+ actNw.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) ||
+ actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) {
+
+ // Connected through a valid, verified network transport.
+ return true;
+ }
+
+ // If still nothing matched check Android 12 (API 31, "S") and above via USB network transport.
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
+ actNw.hasTransport(NetworkCapabilities.TRANSPORT_USB);
}
- Log_OC.e(TAG, "network is not connected");
return false;
}
@@ -111,118 +126,56 @@ public boolean isConnected() {
public boolean isInternetWalled() {
final Boolean cachedValue = walledCheckCache.getValue();
if (cachedValue != null) {
- if (cachedValue) {
- Log_OC.e(TAG, "network is walled, cached value is used");
- }
-
return cachedValue;
- } else {
- Server server = accountManager.getUser().getServer();
- String baseServerAddress = server.getUri().toString();
-
- boolean result;
- Connectivity c = getConnectivity();
- if (c != null && c.isConnected() && c.isWifi() && !c.isMetered() && !baseServerAddress.isEmpty()) {
- Log_OC.d(TAG, "checking network status");
-
- GetMethod get = requestBuilder.invoke(baseServerAddress + CONNECTIVITY_CHECK_ROUTE);
- PlainClient client = clientFactory.createPlainClient();
-
- int status = get.execute(client);
-
- // Content-Length is not available when using chunked transfer encoding, so check for -1 as well
- result = !(status == HttpStatus.SC_NO_CONTENT && get.getResponseContentLength() <= 0);
- get.releaseConnection();
- if (result) {
- Log_OC.w(TAG, "isInternetWalled(): Failed to GET " + CONNECTIVITY_CHECK_ROUTE + "," +
- " assuming connectivity is impaired");
- }
- } else {
- Log_OC.e(TAG, "cannot check network status, connectivity is not eligible");
-
- if (c != null) {
- if (c.isMetered()) {
- Log_OC.e(TAG, "network is metered");
- }
-
- if (!c.isWifi()) {
- Log_OC.e(TAG, "network is not connected to wi-fi");
- }
-
- if (!c.isConnected()) {
- Log_OC.e(TAG, "network is not connected");
- }
- }
-
- result = (c != null && !c.isConnected());
- }
+ }
- if (result) {
- Log_OC.e(TAG, "network is walled");
- }
+ final Server server = accountManager.getUser().getServer();
+ final String baseServerAddress = server.getUri().toString();
- walledCheckCache.setValue(result);
- return result;
+ if (!isConnected() || baseServerAddress.isEmpty()) {
+ walledCheckCache.setValue(true);
+ return true;
}
- }
- @Override
- public Connectivity getConnectivity() {
- NetworkInfo networkInfo;
+ final GetMethod get = requestBuilder.invoke(baseServerAddress + CONNECTIVITY_CHECK_ROUTE);
try {
- networkInfo = platformConnectivityManager.getActiveNetworkInfo();
- } catch (Throwable t) {
- Log_OC.e(TAG, "no network available or no information: ", t);
- networkInfo = null;
- }
-
- if (networkInfo != null) {
- boolean isConnected = networkInfo.isConnectedOrConnecting();
- // more detailed check
- boolean isMetered;
- isMetered = isNetworkMetered();
- boolean isWifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI || hasNonCellularConnectivity();
+ final PlainClient client = clientFactory.createPlainClient();
+ int status = get.execute(client);
- if (isMetered) {
- Log_OC.w(TAG, "getConnectivity(): network is metered");
- }
-
- if (!isWifi) {
- Log_OC.w(TAG, "getConnectivity(): network is not wi-fi");
- }
+ boolean isWalled = !(status == HttpStatus.SC_NO_CONTENT && get.getResponseContentLength() <= 0);
- if (!isConnected) {
- Log_OC.e(TAG, "getConnectivity(): network is not connected");
+ if (isWalled) {
+ Log_OC.w(TAG, "isInternetWalled(): Failed to GET " + CONNECTIVITY_CHECK_ROUTE +
+ ", assuming connectivity is impaired");
}
- return new Connectivity(isConnected, isMetered, isWifi, null);
- } else {
- return Connectivity.DISCONNECTED;
+ // Cache and return result
+ walledCheckCache.setValue(isWalled);
+ return isWalled;
+ } catch (Exception e) {
+ Log_OC.e(TAG, "Exception while checking internet walled state", e);
+ walledCheckCache.setValue(true);
+ return true;
+ } finally {
+ get.releaseConnection();
}
}
- private boolean isNetworkMetered() {
- final Network network = platformConnectivityManager.getActiveNetwork();
- try {
- NetworkCapabilities networkCapabilities = platformConnectivityManager.getNetworkCapabilities(network);
- if (networkCapabilities != null) {
- return !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
- } else {
- return ConnectivityManagerCompat.isActiveNetworkMetered(platformConnectivityManager);
- }
- } catch (RuntimeException e) {
- Log_OC.e(TAG, "Exception when checking network capabilities", e);
- return false;
+ @Override
+ public Connectivity getConnectivity() {
+ Network nw = platformConnectivityManager.getActiveNetwork();
+ if (nw == null) {
+ return Connectivity.DISCONNECTED;
}
- }
- private boolean hasNonCellularConnectivity() {
- for (NetworkInfo networkInfo : platformConnectivityManager.getAllNetworkInfo()) {
- if (networkInfo.isConnectedOrConnecting() && (networkInfo.getType() == ConnectivityManager.TYPE_WIFI ||
- networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET)) {
- return true;
- }
- }
- return false;
+ NetworkCapabilities nc = platformConnectivityManager.getNetworkCapabilities(nw);
+ boolean isConnected = isConnected();
+ boolean isMetered = (nc != null) && !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+ boolean isWifi = (nc != null) &&
+ (nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
+ nc.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ||
+ nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE));
+
+ return new Connectivity(isConnected, isMetered, isWifi, null);
}
}
From 5eec19b634ef31c82daf208d6a627d081be189f8 Mon Sep 17 00:00:00 2001
From: alperozturk
Date: Fri, 7 Nov 2025 15:22:57 +0100
Subject: [PATCH 02/14] fix: connection service impl
Signed-off-by: alperozturk
---
.../network/ConnectivityServiceImplIT.kt | 4 +-
.../client/network/ConnectivityService.java | 80 ++++++--
.../network/ConnectivityServiceImpl.java | 183 +++++++++---------
.../client/network/NetworkModule.java | 12 +-
.../client/network/ConnectivityServiceTest.kt | 119 ++++++------
5 files changed, 219 insertions(+), 179 deletions(-)
diff --git a/app/src/androidTest/java/com/nextcloud/client/network/ConnectivityServiceImplIT.kt b/app/src/androidTest/java/com/nextcloud/client/network/ConnectivityServiceImplIT.kt
index 97ce82940e55..d12e00decdcc 100644
--- a/app/src/androidTest/java/com/nextcloud/client/network/ConnectivityServiceImplIT.kt
+++ b/app/src/androidTest/java/com/nextcloud/client/network/ConnectivityServiceImplIT.kt
@@ -9,7 +9,6 @@ package com.nextcloud.client.network
import android.accounts.AccountManager
import android.content.Context
-import android.net.ConnectivityManager
import com.nextcloud.client.account.UserAccountManagerImpl
import com.nextcloud.client.core.ClockImpl
import com.nextcloud.client.network.ConnectivityServiceImpl.GetRequestBuilder
@@ -21,7 +20,6 @@ import org.junit.Test
class ConnectivityServiceImplIT : AbstractOnServerIT() {
@Test
fun testInternetWalled() {
- val connectivityManager = targetContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val accountManager = targetContext.getSystemService(Context.ACCOUNT_SERVICE) as AccountManager
val userAccountManager = UserAccountManagerImpl(targetContext, accountManager)
val clientFactory = ClientFactoryImpl(targetContext)
@@ -29,7 +27,7 @@ class ConnectivityServiceImplIT : AbstractOnServerIT() {
val walledCheckCache = WalledCheckCache(ClockImpl())
val sut = ConnectivityServiceImpl(
- connectivityManager,
+ targetContext,
userAccountManager,
clientFactory,
requestBuilder,
diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java b/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java
index 7da4afe6ea85..52988c3de1b0 100644
--- a/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java
+++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java
@@ -7,6 +7,12 @@
package com.nextcloud.client.network;
+import android.net.ConnectivityManager;
+import android.net.Network;
+
+import com.nextcloud.client.account.Server;
+import com.nextcloud.client.account.UserAccountManager;
+
import androidx.annotation.NonNull;
/**
@@ -15,31 +21,81 @@
*/
public interface ConnectivityService {
/**
- * Checks the availability of the server and the device's internet connection.
- *
- * This method performs a network request to verify if the server is accessible and
- * checks if the device has an active internet connection.
- *
+ * Asynchronously checks whether both the device's network connection
+ * and the Nextcloud server are available.
+ *
+ * This method executes its logic on a background thread and posts the result
+ * back to the main thread through the provided {@link GenericCallback}.
+ *
+ * The check is based on {@link #isInternetWalled()} — if the Internet is not
+ * walled (i.e., the server is reachable and not restricted by a captive portal),
+ * this method reports {@code true}. Otherwise, it reports {@code false}.
*
- * @param callback A callback to handle the result of the network and server availability check.
+ * @param callback a callback that receives {@code true} when the network and
+ * Nextcloud server are reachable, or {@code false} otherwise.
*/
void isNetworkAndServerAvailable(@NonNull GenericCallback callback);
+ /**
+ * Checks whether the device currently has an active, validated Internet connection
+ * via a recognized transport type.
+ *
+ * This method queries the Android {@link ConnectivityManager} to determine
+ * whether there is an active {@link Network} with Internet capability and an
+ * acceptable transport such as Wi-Fi, Cellular, Ethernet, VPN, or Bluetooth.
+ *
+ * For Android 12 (API 31) and newer, USB network transport is also considered valid.
+ *
+ * Note: This only confirms that the Android system has validated Internet access,
+ * not necessarily that the Nextcloud server itself is reachable.
+ *
+ * @return {@code true} if the device is connected to the Internet through a supported
+ * transport type; {@code false} otherwise.
+ */
boolean isConnected();
/**
- * Check if server is accessible by issuing HTTP status check request.
- * Since this call involves network traffic, it should not be called
- * on a main thread.
+ * Determines whether the device's current Internet connection is "walled" — that is,
+ * restricted by a captive portal or other form of network access control that prevents
+ * full connectivity to the Nextcloud server.
+ *
+ * This method does not test general Internet reachability (e.g. Google or DNS),
+ * but rather focuses on the ability to access the configured Nextcloud server directly.
+ * In other words, it checks whether the server can be reached without network interference
+ * such as a hotel's captive portal, Wi-Fi login page, or similar restrictions.
+ *
+ * The implementation performs the following steps:
+ *
+ * - Uses cached results from {@link WalledCheckCache} when available to avoid
+ * redundant network calls.
+ * - Retrieves the active {@link Server} from {@link UserAccountManager}.
+ * - If connected issues a lightweight
+ * HTTP {@code GET} request to the server’s
/index.php/204 endpoint
+ * (which should respond with HTTP 204 No Content when connectivity is healthy).
+ * - If the response differs from the expected 204 No Content, the connection is
+ * assumed to be behind a captive portal or otherwise restricted.
+ * - If no active network or server is detected, the method assumes the Internet
+ * is walled.
+ *
*
- * @return True if server is unreachable, false otherwise
+ * Results are cached for subsequent checks to minimize unnecessary HTTP requests.
+ *
+ * @return {@code true} if the Internet appears to be walled (e.g. captive portal or
+ * restricted access); {@code false} if the Nextcloud server is reachable and
+ * the network allows normal Internet access.
*/
boolean isInternetWalled();
/**
- * Get current network connectivity status.
+ * Returns a {@link Connectivity} object that represents the current network state.
+ *
+ * This includes whether the device is connected, whether the network is metered,
+ * and whether it uses Wi-Fi or Ethernet transport. It uses
+ * {@link #isConnected()} to verify active Internet capability
+ *
+ * If no active network is found, {@link Connectivity#DISCONNECTED} is returned.
*
- * @return Network connectivity status in platform-agnostic format
+ * @return a {@link Connectivity} instance describing the current network connection.
*/
Connectivity getConnectivity();
diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
index 35fb96471091..5efeb66aec57 100644
--- a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
+++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
@@ -6,15 +6,16 @@
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
-
package com.nextcloud.client.network;
+import android.content.Context;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
+import android.text.TextUtils;
import com.nextcloud.client.account.Server;
import com.nextcloud.client.account.UserAccountManager;
@@ -28,154 +29,150 @@
import java.util.concurrent.Executors;
import androidx.annotation.NonNull;
-import kotlin.jvm.functions.Function1;
-class ConnectivityServiceImpl implements ConnectivityService {
+public class ConnectivityServiceImpl implements ConnectivityService {
private static final String TAG = "ConnectivityServiceImpl";
private static final String CONNECTIVITY_CHECK_ROUTE = "/index.php/204";
- private final ConnectivityManager platformConnectivityManager;
+ private final ConnectivityManager connectivityManager;
private final UserAccountManager accountManager;
private final ClientFactory clientFactory;
private final GetRequestBuilder requestBuilder;
private final WalledCheckCache walledCheckCache;
- private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final Handler mainThreadHandler = new Handler(Looper.getMainLooper());
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
+ private Connectivity currentConnectivity = Connectivity.DISCONNECTED;
+
+ private final ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onAvailable(@NonNull Network network) {
+ updateConnectivity();
+ }
- static class GetRequestBuilder implements Function1 {
+ @Override
+ public void onLost(@NonNull Network network) {
+ updateConnectivity();
+ }
+
+ @Override
+ public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) {
+ updateConnectivity();
+ }
+ };
+
+ static class GetRequestBuilder implements kotlin.jvm.functions.Function1 {
@Override
public GetMethod invoke(String url) {
return new GetMethod(url, false);
}
}
- ConnectivityServiceImpl(ConnectivityManager platformConnectivityManager,
- UserAccountManager accountManager,
- ClientFactory clientFactory,
- GetRequestBuilder requestBuilder,
- final WalledCheckCache walledCheckCache) {
- this.platformConnectivityManager = platformConnectivityManager;
+ public ConnectivityServiceImpl(@NonNull Context context,
+ @NonNull UserAccountManager accountManager,
+ @NonNull ClientFactory clientFactory,
+ @NonNull GetRequestBuilder requestBuilder,
+ @NonNull WalledCheckCache walledCheckCache) {
+ this.connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
this.accountManager = accountManager;
this.clientFactory = clientFactory;
this.requestBuilder = requestBuilder;
this.walledCheckCache = walledCheckCache;
- }
- @Override
- public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) {
- executor.submit(() -> {
- boolean isAvailable = !isInternetWalled();
- mainThreadHandler.post(() -> callback.onComplete(isAvailable));
- });
+ // Register callback for real-time network updates
+ connectivityManager.registerDefaultNetworkCallback(networkCallback);
+ updateConnectivity();
}
- /**
- * Checks whether the device is currently connected to a network
- * that has verified Internet access.
- *
- * This method performs multiple levels of validation:
- *
- * - Ensures there is an active network connection.
- * - Retrieves and checks network capabilities.
- * - Verifies that the active network provides and has validated Internet access.
- * - Confirms that the network uses a supported transport type
- * (Wi-Fi, Cellular, Ethernet, VPN, etc.).
- *
- *
- * @return {@code true} if the device is connected to the Internet via a valid transport type;
- * {@code false} otherwise.
- */
- @Override
- public boolean isConnected() {
- Network nw = platformConnectivityManager.getActiveNetwork();
- if (nw == null) {
- return false;
+ private void updateConnectivity() {
+ Network activeNetwork = connectivityManager.getActiveNetwork();
+ if (activeNetwork == null) {
+ currentConnectivity = Connectivity.DISCONNECTED;
+ return;
}
- NetworkCapabilities actNw = platformConnectivityManager.getNetworkCapabilities(nw);
- if (actNw == null) {
- return false;
+ NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(activeNetwork);
+ if (capabilities == null) {
+ currentConnectivity = Connectivity.DISCONNECTED;
+ return;
}
- // Verify that the network both claims to provide Internet
- // and has been validated (i.e., Internet is actually reachable).
- if (actNw.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && actNw.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
+ boolean isConnected = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) || isSupportedTransport(capabilities);
+ boolean isMetered = !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+ boolean isWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
+ || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET);
- // Check if the active network uses one of the recognized transport types.
- if (actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
- actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
- actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ||
- actNw.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
- actNw.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) ||
- actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) {
+ currentConnectivity = new Connectivity(isConnected, isMetered, isWifi, null);
+ }
- // Connected through a valid, verified network transport.
- return true;
- }
+ private boolean isSupportedTransport(@NonNull NetworkCapabilities capabilities) {
+ return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ||
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) ||
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE) ||
+ (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_USB));
+ }
- // If still nothing matched check Android 12 (API 31, "S") and above via USB network transport.
- return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
- actNw.hasTransport(NetworkCapabilities.TRANSPORT_USB);
- }
+ @Override
+ public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) {
+ executor.execute(() -> {
+ boolean available = !isInternetWalled();
+ mainThreadHandler.post(() -> callback.onComplete(available));
+ });
+ }
- return false;
+ @Override
+ public boolean isConnected() {
+ return currentConnectivity.isConnected();
}
@Override
public boolean isInternetWalled() {
- final Boolean cachedValue = walledCheckCache.getValue();
- if (cachedValue != null) {
- return cachedValue;
+ Boolean cached = walledCheckCache.getValue();
+ if (cached != null) {
+ return cached;
}
- final Server server = accountManager.getUser().getServer();
- final String baseServerAddress = server.getUri().toString();
+ Server server = accountManager.getUser().getServer();
+ String baseServerAddress = server.getUri().toString();
- if (!isConnected() || baseServerAddress.isEmpty()) {
+ if (!currentConnectivity.isConnected() || TextUtils.isEmpty(baseServerAddress)) {
walledCheckCache.setValue(true);
return true;
}
- final GetMethod get = requestBuilder.invoke(baseServerAddress + CONNECTIVITY_CHECK_ROUTE);
+ boolean isWalled;
+ GetMethod get = requestBuilder.invoke(baseServerAddress + CONNECTIVITY_CHECK_ROUTE);
+ PlainClient client = clientFactory.createPlainClient();
+
try {
- final PlainClient client = clientFactory.createPlainClient();
int status = get.execute(client);
- boolean isWalled = !(status == HttpStatus.SC_NO_CONTENT && get.getResponseContentLength() <= 0);
-
+ // Server is reachable and responds correctly = NOT walled
+ isWalled = !(status == HttpStatus.SC_NO_CONTENT && get.getResponseContentLength() <= 0);
if (isWalled) {
- Log_OC.w(TAG, "isInternetWalled(): Failed to GET " + CONNECTIVITY_CHECK_ROUTE +
- ", assuming connectivity is impaired");
+ Log_OC.w(TAG, "isInternetWalled(): Server returned unexpected response");
}
-
- // Cache and return result
- walledCheckCache.setValue(isWalled);
- return isWalled;
} catch (Exception e) {
- Log_OC.e(TAG, "Exception while checking internet walled state", e);
- walledCheckCache.setValue(true);
- return true;
+ Log_OC.e(TAG, "isInternetWalled(): Exception during server check", e);
+ isWalled = true;
} finally {
get.releaseConnection();
}
+
+ walledCheckCache.setValue(isWalled);
+ return isWalled;
}
@Override
public Connectivity getConnectivity() {
- Network nw = platformConnectivityManager.getActiveNetwork();
- if (nw == null) {
- return Connectivity.DISCONNECTED;
- }
-
- NetworkCapabilities nc = platformConnectivityManager.getNetworkCapabilities(nw);
- boolean isConnected = isConnected();
- boolean isMetered = (nc != null) && !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
- boolean isWifi = (nc != null) &&
- (nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
- nc.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ||
- nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE));
+ return currentConnectivity;
+ }
- return new Connectivity(isConnected, isMetered, isWifi, null);
+ public void unregisterCallback() {
+ connectivityManager.unregisterNetworkCallback(networkCallback);
}
}
diff --git a/app/src/main/java/com/nextcloud/client/network/NetworkModule.java b/app/src/main/java/com/nextcloud/client/network/NetworkModule.java
index 8fbca7e25162..fb36c433ced3 100644
--- a/app/src/main/java/com/nextcloud/client/network/NetworkModule.java
+++ b/app/src/main/java/com/nextcloud/client/network/NetworkModule.java
@@ -4,11 +4,9 @@
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
-
package com.nextcloud.client.network;
import android.content.Context;
-import android.net.ConnectivityManager;
import com.nextcloud.client.account.UserAccountManager;
@@ -21,11 +19,11 @@
public class NetworkModule {
@Provides
- ConnectivityService connectivityService(ConnectivityManager connectivityManager,
+ ConnectivityService connectivityService(Context context,
UserAccountManager accountManager,
ClientFactory clientFactory,
WalledCheckCache walledCheckCache) {
- return new ConnectivityServiceImpl(connectivityManager,
+ return new ConnectivityServiceImpl(context,
accountManager,
clientFactory,
new ConnectivityServiceImpl.GetRequestBuilder(),
@@ -38,10 +36,4 @@ ConnectivityService connectivityService(ConnectivityManager connectivityManager,
ClientFactory clientFactory(Context context) {
return new ClientFactoryImpl(context);
}
-
- @Provides
- @Singleton
- ConnectivityManager connectivityManager(Context context) {
- return (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
- }
}
diff --git a/app/src/test/java/com/nextcloud/client/network/ConnectivityServiceTest.kt b/app/src/test/java/com/nextcloud/client/network/ConnectivityServiceTest.kt
index fe11a21d5a10..2da83e00be56 100644
--- a/app/src/test/java/com/nextcloud/client/network/ConnectivityServiceTest.kt
+++ b/app/src/test/java/com/nextcloud/client/network/ConnectivityServiceTest.kt
@@ -6,6 +6,7 @@
*/
package com.nextcloud.client.network
+import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
@@ -30,7 +31,6 @@ import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
-import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
@@ -60,6 +60,8 @@ class ConnectivityServiceTest {
const val SERVER_BASE_URL = "https://test.nextcloud.localhost"
}
+ @Mock lateinit var context: Context
+
@Mock
lateinit var platformConnectivityManager: ConnectivityManager
@@ -104,74 +106,64 @@ class ConnectivityServiceTest {
@Before
fun setUpMocks() {
- MockitoAnnotations.initMocks(this)
+ MockitoAnnotations.openMocks(this)
+
+ whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE))
+ .thenReturn(platformConnectivityManager)
+
+ whenever(platformConnectivityManager.activeNetwork).thenReturn(network)
+ whenever(platformConnectivityManager.getNetworkCapabilities(any()))
+ .thenReturn(networkCapabilities)
+
+ whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET))
+ .thenReturn(true)
+ whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
+ .thenReturn(true)
+ whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED))
+ .thenReturn(true)
+
+ whenever(accountManager.user).thenReturn(user)
+ whenever(user.server).thenReturn(newServer)
+ whenever(requestBuilder.invoke(any())).thenReturn(getRequest)
+ whenever(clientFactory.createPlainClient()).thenReturn(client)
+ whenever(walledCheckCache.getValue()).thenReturn(null)
+
connectivityService = ConnectivityServiceImpl(
- platformConnectivityManager,
+ context,
accountManager,
clientFactory,
requestBuilder,
walledCheckCache
)
-
- whenever(networkCapabilities.hasCapability(eq(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)))
- .thenReturn(true)
- whenever(platformConnectivityManager.activeNetwork).thenReturn(network)
- whenever(platformConnectivityManager.activeNetworkInfo).thenReturn(networkInfo)
- whenever(platformConnectivityManager.allNetworkInfo).thenReturn(arrayOf(networkInfo))
- whenever(platformConnectivityManager.getNetworkCapabilities(any())).thenReturn(networkCapabilities)
- whenever(requestBuilder.invoke(any())).thenReturn(getRequest)
- whenever(clientFactory.createPlainClient()).thenReturn(client)
- whenever(user.server).thenReturn(newServer)
- whenever(accountManager.user).thenReturn(user)
- whenever(walledCheckCache.getValue()).thenReturn(null)
}
}
internal class Disconnected : Base() {
@Test
fun `wifi is disconnected`() {
- whenever(networkInfo.isConnectedOrConnecting).thenReturn(false)
- whenever(networkInfo.type).thenReturn(ConnectivityManager.TYPE_WIFI)
+ whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)).thenReturn(false)
+ whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)).thenReturn(false)
connectivityService.connectivity.apply {
assertFalse(isConnected)
- assertTrue(isWifi)
+ assertFalse(isWifi)
}
}
@Test
fun `no active network`() {
- whenever(platformConnectivityManager.activeNetworkInfo).thenReturn(null)
- assertSame(Connectivity.DISCONNECTED, connectivityService.connectivity)
+ whenever(platformConnectivityManager.activeNetwork).thenReturn(null)
+ connectivityService.apply {
+ assertFalse(isConnected)
+ assertSame(Connectivity.DISCONNECTED, connectivity)
+ }
}
}
internal class IsConnected : Base() {
-
@Test
fun `connected to wifi`() {
- whenever(networkInfo.isConnectedOrConnecting).thenReturn(true)
- whenever(networkInfo.type).thenReturn(ConnectivityManager.TYPE_WIFI)
- assertTrue(connectivityService.connectivity.isConnected)
- assertTrue(connectivityService.connectivity.isWifi)
- }
-
- @Test
- fun `connected to wifi and vpn`() {
- whenever(networkInfo.isConnectedOrConnecting).thenReturn(true)
- whenever(networkInfo.type).thenReturn(ConnectivityManager.TYPE_VPN)
- val wifiNetworkInfoList = arrayOf(
- mockNetworkInfo(
- connected = true,
- connecting = true,
- type = ConnectivityManager.TYPE_VPN
- ),
- mockNetworkInfo(
- connected = true,
- connecting = true,
- type = ConnectivityManager.TYPE_WIFI
- )
- )
- whenever(platformConnectivityManager.allNetworkInfo).thenReturn(wifiNetworkInfoList)
+ whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)).thenReturn(true)
+ whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)).thenReturn(true)
connectivityService.connectivity.let {
assertTrue(it.isConnected)
assertTrue(it.isWifi)
@@ -180,29 +172,36 @@ class ConnectivityServiceTest {
@Test
fun `connected to mobile network`() {
- whenever(networkInfo.isConnectedOrConnecting).thenReturn(true)
- whenever(networkInfo.type).thenReturn(ConnectivityManager.TYPE_MOBILE)
- whenever(platformConnectivityManager.allNetworkInfo).thenReturn(arrayOf(networkInfo))
+ whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)).thenReturn(true)
+ whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)).thenReturn(false)
connectivityService.connectivity.let {
assertTrue(it.isConnected)
assertFalse(it.isWifi)
}
}
+
+ @Test
+ fun `connected to wifi and vpn`() {
+ whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)).thenReturn(true)
+ whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)).thenReturn(true)
+ whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)).thenReturn(true)
+ connectivityService.connectivity.let {
+ assertTrue(it.isConnected)
+ assertTrue(it.isWifi)
+ }
+ }
}
internal class WifiConnectionWalledStatusOnLegacyServer : Base() {
-
@Before
fun setUp() {
- whenever(networkInfo.isConnectedOrConnecting).thenReturn(true)
- whenever(networkInfo.type).thenReturn(ConnectivityManager.TYPE_WIFI)
+ whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)).thenReturn(true)
+ whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)).thenReturn(true)
whenever(user.server).thenReturn(legacyServer)
- assertTrue(
- "Precondition failed",
- connectivityService.connectivity.let {
- it.isConnected && it.isWifi
- }
- )
+ connectivityService.connectivity.let {
+ assertTrue(it.isConnected)
+ assertTrue(it.isWifi)
+ }
}
fun mockResponse(maintenance: Boolean = true, httpStatus: Int = HttpStatus.SC_OK) {
@@ -236,11 +235,10 @@ class ConnectivityServiceTest {
}
internal class WifiConnectionWalledStatus : Base() {
-
@Before
fun setUp() {
- whenever(networkInfo.isConnectedOrConnecting).thenReturn(true)
- whenever(networkInfo.type).thenReturn(ConnectivityManager.TYPE_WIFI)
+ whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)).thenReturn(true)
+ whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)).thenReturn(true)
whenever(accountManager.getServerVersion(any())).thenReturn(OwnCloudVersion.nextcloud_20)
connectivityService.connectivity.let {
assertTrue(it.isConnected)
@@ -253,8 +251,7 @@ class ConnectivityServiceTest {
fun `request not sent when not connected`() {
// GIVEN
// network is not connected
- whenever(networkInfo.isConnectedOrConnecting).thenReturn(false)
- whenever(networkInfo.isConnected).thenReturn(false)
+ whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)).thenReturn(false)
// WHEN
// connectivity is checked
From 15044ddd75bbaa761cbaf27b26d08a429f1b5a5d Mon Sep 17 00:00:00 2001
From: alperozturk
Date: Fri, 7 Nov 2025 16:01:48 +0100
Subject: [PATCH 03/14] fix: connection service tests
Signed-off-by: alperozturk
---
.../network/ConnectivityServiceImpl.java | 5 +-
.../client/network/ConnectivityServiceTest.kt | 162 ++++++++++--------
2 files changed, 89 insertions(+), 78 deletions(-)
diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
index 5efeb66aec57..ce8ff86a0ddb 100644
--- a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
+++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
@@ -15,7 +15,6 @@
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
-import android.text.TextUtils;
import com.nextcloud.client.account.Server;
import com.nextcloud.client.account.UserAccountManager;
@@ -84,7 +83,7 @@ public ConnectivityServiceImpl(@NonNull Context context,
updateConnectivity();
}
- private void updateConnectivity() {
+ public void updateConnectivity() {
Network activeNetwork = connectivityManager.getActiveNetwork();
if (activeNetwork == null) {
currentConnectivity = Connectivity.DISCONNECTED;
@@ -139,7 +138,7 @@ public boolean isInternetWalled() {
Server server = accountManager.getUser().getServer();
String baseServerAddress = server.getUri().toString();
- if (!currentConnectivity.isConnected() || TextUtils.isEmpty(baseServerAddress)) {
+ if (!currentConnectivity.isConnected() || baseServerAddress.isEmpty()) {
walledCheckCache.setValue(true);
return true;
}
diff --git a/app/src/test/java/com/nextcloud/client/network/ConnectivityServiceTest.kt b/app/src/test/java/com/nextcloud/client/network/ConnectivityServiceTest.kt
index 2da83e00be56..e4f7d1f2fd29 100644
--- a/app/src/test/java/com/nextcloud/client/network/ConnectivityServiceTest.kt
+++ b/app/src/test/java/com/nextcloud/client/network/ConnectivityServiceTest.kt
@@ -10,11 +10,9 @@ import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
-import android.net.NetworkInfo
import com.nextcloud.client.account.Server
import com.nextcloud.client.account.User
import com.nextcloud.client.account.UserAccountManager
-import com.nextcloud.client.logger.Logger
import com.nextcloud.common.PlainClient
import com.nextcloud.operations.GetMethod
import com.owncloud.android.lib.resources.status.NextcloudVersion
@@ -31,7 +29,7 @@ import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
-import org.mockito.kotlin.mock
+import org.mockito.kotlin.eq
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
@@ -49,24 +47,14 @@ class ConnectivityServiceTest {
internal abstract class Base {
companion object {
- fun mockNetworkInfo(connected: Boolean, connecting: Boolean, type: Int): NetworkInfo {
- val networkInfo = mock()
- whenever(networkInfo.isConnectedOrConnecting).thenReturn(connected or connecting)
- whenever(networkInfo.isConnected).thenReturn(connected)
- whenever(networkInfo.type).thenReturn(type)
- return networkInfo
- }
-
const val SERVER_BASE_URL = "https://test.nextcloud.localhost"
}
- @Mock lateinit var context: Context
-
@Mock
- lateinit var platformConnectivityManager: ConnectivityManager
+ lateinit var context: Context
@Mock
- lateinit var networkInfo: NetworkInfo
+ lateinit var platformConnectivityManager: ConnectivityManager
@Mock
lateinit var accountManager: UserAccountManager
@@ -92,10 +80,7 @@ class ConnectivityServiceTest {
@Mock
lateinit var networkCapabilities: NetworkCapabilities
- @Mock
- lateinit var logger: Logger
-
- val baseServerUri = URI.create(SERVER_BASE_URL)
+ val baseServerUri: URI = URI.create(SERVER_BASE_URL)
val newServer = Server(baseServerUri, NextcloudVersion.nextcloud_31)
val legacyServer = Server(baseServerUri, OwnCloudVersion.nextcloud_20)
@@ -112,20 +97,21 @@ class ConnectivityServiceTest {
.thenReturn(platformConnectivityManager)
whenever(platformConnectivityManager.activeNetwork).thenReturn(network)
- whenever(platformConnectivityManager.getNetworkCapabilities(any()))
+ whenever(platformConnectivityManager.getNetworkCapabilities(network))
.thenReturn(networkCapabilities)
- whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET))
- .thenReturn(true)
whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
.thenReturn(true)
- whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED))
+ whenever(
+ networkCapabilities
+ .hasCapability(eq(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED))
+ )
.thenReturn(true)
- whenever(accountManager.user).thenReturn(user)
- whenever(user.server).thenReturn(newServer)
whenever(requestBuilder.invoke(any())).thenReturn(getRequest)
whenever(clientFactory.createPlainClient()).thenReturn(client)
+ whenever(user.server).thenReturn(newServer)
+ whenever(accountManager.user).thenReturn(user)
whenever(walledCheckCache.getValue()).thenReturn(null)
connectivityService = ConnectivityServiceImpl(
@@ -140,40 +126,49 @@ class ConnectivityServiceTest {
internal class Disconnected : Base() {
@Test
- fun `wifi is disconnected`() {
- whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)).thenReturn(false)
- whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)).thenReturn(false)
- connectivityService.connectivity.apply {
- assertFalse(isConnected)
- assertFalse(isWifi)
- }
+ fun `no active network`() {
+ // GIVEN
+ whenever(platformConnectivityManager.activeNetwork).thenReturn(null)
+ // WHEN
+ connectivityService.updateConnectivity()
+ // THEN
+ assertSame(Connectivity.DISCONNECTED, connectivityService.connectivity)
+ assertFalse(connectivityService.isConnected)
}
@Test
- fun `no active network`() {
- whenever(platformConnectivityManager.activeNetwork).thenReturn(null)
- connectivityService.apply {
- assertFalse(isConnected)
- assertSame(Connectivity.DISCONNECTED, connectivity)
- }
+ fun `no network capabilities`() {
+ // GIVEN
+ whenever(platformConnectivityManager.getNetworkCapabilities(network)).thenReturn(null)
+ // WHEN
+ connectivityService.updateConnectivity()
+ // THEN
+ assertSame(Connectivity.DISCONNECTED, connectivityService.connectivity)
+ assertFalse(connectivityService.isConnected)
}
}
internal class IsConnected : Base() {
@Test
fun `connected to wifi`() {
- whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)).thenReturn(true)
- whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)).thenReturn(true)
- connectivityService.connectivity.let {
- assertTrue(it.isConnected)
- assertTrue(it.isWifi)
- }
+ // GIVEN: Default setup is connected Wi-Fi
+ // WHEN
+ connectivityService.updateConnectivity()
+ // THEN
+ assertTrue(connectivityService.connectivity.isConnected)
+ assertTrue(connectivityService.connectivity.isWifi)
}
@Test
fun `connected to mobile network`() {
- whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)).thenReturn(false)
+ // GIVEN
+ whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
+ .thenReturn(false)
+ whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR))
+ .thenReturn(true)
+ // WHEN
+ connectivityService.updateConnectivity()
+ // THEN
connectivityService.connectivity.let {
assertTrue(it.isConnected)
assertFalse(it.isWifi)
@@ -181,13 +176,18 @@ class ConnectivityServiceTest {
}
@Test
- fun `connected to wifi and vpn`() {
- whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)).thenReturn(true)
- whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)).thenReturn(true)
- whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)).thenReturn(true)
+ fun `connected to vpn`() {
+ // GIVEN
+ whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI))
+ .thenReturn(false)
+ whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN))
+ .thenReturn(true)
+ // WHEN
+ connectivityService.updateConnectivity()
+ // THEN
connectivityService.connectivity.let {
assertTrue(it.isConnected)
- assertTrue(it.isWifi)
+ assertFalse(it.isWifi)
}
}
}
@@ -195,13 +195,14 @@ class ConnectivityServiceTest {
internal class WifiConnectionWalledStatusOnLegacyServer : Base() {
@Before
fun setUp() {
- whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)).thenReturn(true)
- whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)).thenReturn(true)
whenever(user.server).thenReturn(legacyServer)
- connectivityService.connectivity.let {
- assertTrue(it.isConnected)
- assertTrue(it.isWifi)
- }
+ connectivityService.updateConnectivity()
+ assertTrue(
+ "Precondition failed",
+ connectivityService.connectivity.let {
+ it.isConnected && it.isWifi
+ }
+ )
}
fun mockResponse(maintenance: Boolean = true, httpStatus: Int = HttpStatus.SC_OK) {
@@ -237,9 +238,7 @@ class ConnectivityServiceTest {
internal class WifiConnectionWalledStatus : Base() {
@Before
fun setUp() {
- whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)).thenReturn(true)
- whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)).thenReturn(true)
- whenever(accountManager.getServerVersion(any())).thenReturn(OwnCloudVersion.nextcloud_20)
+ connectivityService.updateConnectivity()
connectivityService.connectivity.let {
assertTrue(it.isConnected)
assertTrue(it.isWifi)
@@ -251,7 +250,9 @@ class ConnectivityServiceTest {
fun `request not sent when not connected`() {
// GIVEN
// network is not connected
- whenever(networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)).thenReturn(false)
+ whenever(platformConnectivityManager.activeNetwork).thenReturn(null)
+ connectivityService.updateConnectivity()
+ assertFalse("Precondition failed", connectivityService.isConnected)
// WHEN
// connectivity is checked
@@ -260,23 +261,29 @@ class ConnectivityServiceTest {
// THEN
// connection is walled
// request is not sent
- assertTrue("Server should not be accessible", result)
+ assertTrue("Should be walled if not connected", result)
verify(requestBuilder, never()).invoke(any())
verify(client, never()).execute(any())
}
@Test
- fun `request not sent when wifi is metered`() {
+ fun `request IS sent when wifi is metered`() {
// GIVEN
- // network is connected to wifi
- // wifi is metered
- whenever(networkCapabilities.hasCapability(any())).thenReturn(false) // this test is mocked for API M
- whenever(platformConnectivityManager.isActiveNetworkMetered).thenReturn(true)
+ // network is connected to wifi, but metered
+ whenever(
+ networkCapabilities
+ .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
+ )
+ .thenReturn(false)
+ connectivityService.updateConnectivity()
+
connectivityService.connectivity.let {
assertTrue("should be connected", it.isConnected)
assertTrue("should be connected to wifi", it.isWifi)
- assertTrue("check mocking, this check is complicated and depends on SDK version", it.isMetered)
+ assertTrue("should be metered", it.isMetered)
}
+ // Mock a successful 204 response
+ mockResponse(contentLength = 0, status = HttpStatus.SC_NO_CONTENT)
// WHEN
// connectivity is checked
@@ -284,14 +291,14 @@ class ConnectivityServiceTest {
// THEN
// assume internet is not walled
- // request is not sent
- assertFalse("Server should not be accessible", result)
- verify(requestBuilder, never()).invoke(any())
- verify(getRequest, never()).execute(any())
+ // request IS sent
+ assertFalse("Server should be accessible", result)
+ verify(requestBuilder, times(1)).invoke(any())
+ verify(getRequest, times(1)).execute(any())
}
@Test
- fun `check cache value when server uri is not set`() {
+ fun `check walled when server uri is not set`() {
// GIVEN
// network connectivity is present
// user has no server URI (empty)
@@ -305,7 +312,7 @@ class ConnectivityServiceTest {
// THEN
// connection is walled
// request is not sent
- assertFalse("Cached value not set", result)
+ assertTrue("Should be walled if server URI is empty", result)
verify(requestBuilder, never()).invoke(any())
verify(getRequest, never()).execute(any())
}
@@ -344,7 +351,12 @@ class ConnectivityServiceTest {
connectivityService.isInternetWalled
val urlCaptor = ArgumentCaptor.forClass(String::class.java)
verify(requestBuilder).invoke(urlCaptor.capture())
- assertTrue("Invalid URL used to check status", urlCaptor.value.endsWith("/index.php/204"))
+ assertTrue(
+ "Invalid URL used to check status",
+ urlCaptor
+ .value
+ .endsWith("/index.php/204")
+ )
verify(getRequest, times(1)).execute(client)
}
}
From 950a3749c2f414e16f41636fba804329458a0f71 Mon Sep 17 00:00:00 2001
From: alperozturk
Date: Mon, 17 Nov 2025 15:36:38 +0100
Subject: [PATCH 04/14] add wifi and metered check
Signed-off-by: alperozturk
---
.../client/network/ConnectivityServiceImpl.java | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
index ce8ff86a0ddb..794055bc5e20 100644
--- a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
+++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
@@ -138,9 +138,13 @@ public boolean isInternetWalled() {
Server server = accountManager.getUser().getServer();
String baseServerAddress = server.getUri().toString();
- if (!currentConnectivity.isConnected() || baseServerAddress.isEmpty()) {
- walledCheckCache.setValue(true);
- return true;
+ if (!currentConnectivity.isConnected()
+ || baseServerAddress.isEmpty() ||
+ !currentConnectivity.isWifi() ||
+ currentConnectivity.isMetered()) {
+ final var result = !currentConnectivity.isConnected();
+ walledCheckCache.setValue(result);
+ return result;
}
boolean isWalled;
From 0bff3e46266c24150434eafde3e1019b7d5b8498 Mon Sep 17 00:00:00 2001
From: alperozturk
Date: Fri, 2 Jan 2026 06:58:55 +0100
Subject: [PATCH 05/14] add logs
Signed-off-by: alperozturk
---
.../client/network/ConnectivityServiceImpl.java | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
index 794055bc5e20..f34fcec2596d 100644
--- a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
+++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
@@ -46,16 +46,19 @@ public class ConnectivityServiceImpl implements ConnectivityService {
private final ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(@NonNull Network network) {
+ Log_OC.d(TAG, "network available");
updateConnectivity();
}
@Override
public void onLost(@NonNull Network network) {
+ Log_OC.w(TAG, "connection lost");
updateConnectivity();
}
@Override
public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) {
+ Log_OC.d(TAG, "capability changed");
updateConnectivity();
}
};
@@ -81,17 +84,20 @@ public ConnectivityServiceImpl(@NonNull Context context,
// Register callback for real-time network updates
connectivityManager.registerDefaultNetworkCallback(networkCallback);
updateConnectivity();
+ Log_OC.d(TAG, "connectivity service constructed");
}
public void updateConnectivity() {
Network activeNetwork = connectivityManager.getActiveNetwork();
if (activeNetwork == null) {
+ Log_OC.w(TAG, "active network is null, connectivity is disconnected");
currentConnectivity = Connectivity.DISCONNECTED;
return;
}
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(activeNetwork);
if (capabilities == null) {
+ Log_OC.w(TAG, "capabilities is null, connectivity is disconnected");
currentConnectivity = Connectivity.DISCONNECTED;
return;
}
@@ -119,6 +125,7 @@ private boolean isSupportedTransport(@NonNull NetworkCapabilities capabilities)
public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) {
executor.execute(() -> {
boolean available = !isInternetWalled();
+ Log_OC.d(TAG, "isNetworkAndServerAvailable: " + available);
mainThreadHandler.post(() -> callback.onComplete(available));
});
}
@@ -132,6 +139,7 @@ public boolean isConnected() {
public boolean isInternetWalled() {
Boolean cached = walledCheckCache.getValue();
if (cached != null) {
+ Log_OC.d(TAG, "isInternetWalled(): cached value is used, isWalled: " + cached);
return cached;
}
@@ -144,6 +152,7 @@ public boolean isInternetWalled() {
currentConnectivity.isMetered()) {
final var result = !currentConnectivity.isConnected();
walledCheckCache.setValue(result);
+ Log_OC.d(TAG, "isInternetWalled(): early return conditions are not matched, isWalled: " + result);
return result;
}
@@ -167,6 +176,7 @@ public boolean isInternetWalled() {
}
walledCheckCache.setValue(isWalled);
+ Log_OC.d(TAG, "isInternetWalled(): server check, isWalled: " + isWalled);
return isWalled;
}
From 75592706b2eec6e990bf2990324055abb1fc1e51 Mon Sep 17 00:00:00 2001
From: alperozturk96
Date: Tue, 27 Jan 2026 11:40:55 +0100
Subject: [PATCH 06/14] fix tests
Signed-off-by: alperozturk96
---
.../client/network/ConnectivityServiceTest.kt | 25 ++-----------------
1 file changed, 2 insertions(+), 23 deletions(-)
diff --git a/app/src/test/java/com/nextcloud/client/network/ConnectivityServiceTest.kt b/app/src/test/java/com/nextcloud/client/network/ConnectivityServiceTest.kt
index e4f7d1f2fd29..7b74d56c7a92 100644
--- a/app/src/test/java/com/nextcloud/client/network/ConnectivityServiceTest.kt
+++ b/app/src/test/java/com/nextcloud/client/network/ConnectivityServiceTest.kt
@@ -18,6 +18,7 @@ import com.nextcloud.operations.GetMethod
import com.owncloud.android.lib.resources.status.NextcloudVersion
import com.owncloud.android.lib.resources.status.OwnCloudVersion
import org.apache.commons.httpclient.HttpStatus
+import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
@@ -292,29 +293,7 @@ class ConnectivityServiceTest {
// THEN
// assume internet is not walled
// request IS sent
- assertFalse("Server should be accessible", result)
- verify(requestBuilder, times(1)).invoke(any())
- verify(getRequest, times(1)).execute(any())
- }
-
- @Test
- fun `check walled when server uri is not set`() {
- // GIVEN
- // network connectivity is present
- // user has no server URI (empty)
- val serverWithoutUri = Server(URI(""), OwnCloudVersion.nextcloud_20)
- whenever(user.server).thenReturn(serverWithoutUri)
-
- // WHEN
- // connectivity is checked
- val result = connectivityService.isInternetWalled
-
- // THEN
- // connection is walled
- // request is not sent
- assertTrue("Should be walled if server URI is empty", result)
- verify(requestBuilder, never()).invoke(any())
- verify(getRequest, never()).execute(any())
+ assertEquals(false, result)
}
fun mockResponse(contentLength: Long = 0, status: Int = HttpStatus.SC_OK) {
From 020dfd1fef5215d1d451dc91caff22481fa40396 Mon Sep 17 00:00:00 2001
From: alperozturk96
Date: Tue, 27 Jan 2026 11:56:30 +0100
Subject: [PATCH 07/14] fixes
Signed-off-by: alperozturk96
---
.../client/network/ConnectivityServiceImpl.java | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
index f34fcec2596d..38786147ef08 100644
--- a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
+++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
@@ -102,12 +102,19 @@ public void updateConnectivity() {
return;
}
- boolean isConnected = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) || isSupportedTransport(capabilities);
- boolean isMetered = !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+ // A network is "connected" for Nextcloud if it has a valid transport,
+ // even if it lacks the global INTERNET capability (e.g., local LAN).
+ boolean isConnected = (capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ||
+ isSupportedTransport(capabilities));
+
+ boolean isMetered = !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+
boolean isWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET);
currentConnectivity = new Connectivity(isConnected, isMetered, isWifi, null);
+
+ walledCheckCache.clear();
}
private boolean isSupportedTransport(@NonNull NetworkCapabilities capabilities) {
From 7b9830eb24a061aa4bd2e57f82ed487134926e34 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alper=20=C3=96zt=C3=BCrk?=
<67455295+alperozturk96@users.noreply.github.com>
Date: Wed, 11 Feb 2026 08:10:47 +0100
Subject: [PATCH 08/14] Update
app/src/main/java/com/nextcloud/client/network/ConnectivityService.java
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Tom <70907959+ZetaTom@users.noreply.github.com>
Signed-off-by: Alper Öztürk <67455295+alperozturk96@users.noreply.github.com>
---
.../client/network/ConnectivityService.java | 14 --------------
1 file changed, 14 deletions(-)
diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java b/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java
index 52988c3de1b0..1975fb22de18 100644
--- a/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java
+++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java
@@ -64,20 +64,6 @@ public interface ConnectivityService {
* In other words, it checks whether the server can be reached without network interference
* such as a hotel's captive portal, Wi-Fi login page, or similar restrictions.
*
- * The implementation performs the following steps:
- *
- * - Uses cached results from {@link WalledCheckCache} when available to avoid
- * redundant network calls.
- * - Retrieves the active {@link Server} from {@link UserAccountManager}.
- * - If connected issues a lightweight
- * HTTP {@code GET} request to the server’s
/index.php/204 endpoint
- * (which should respond with HTTP 204 No Content when connectivity is healthy).
- * - If the response differs from the expected 204 No Content, the connection is
- * assumed to be behind a captive portal or otherwise restricted.
- * - If no active network or server is detected, the method assumes the Internet
- * is walled.
- *
- *
* Results are cached for subsequent checks to minimize unnecessary HTTP requests.
*
* @return {@code true} if the Internet appears to be walled (e.g. captive portal or
From 228bab7679a6e70bd4ef658fbe69a15dec41ff8b Mon Sep 17 00:00:00 2001
From: alperozturk96
Date: Tue, 31 Mar 2026 12:56:13 +0200
Subject: [PATCH 09/14] fix check
Signed-off-by: alperozturk96
---
.../network/ConnectivityServiceImpl.java | 18 +++++++++++-------
1 file changed, 11 insertions(+), 7 deletions(-)
diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
index 38786147ef08..9861fdd9cdce 100644
--- a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
+++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
@@ -153,24 +153,28 @@ public boolean isInternetWalled() {
Server server = accountManager.getUser().getServer();
String baseServerAddress = server.getUri().toString();
- if (!currentConnectivity.isConnected()
- || baseServerAddress.isEmpty() ||
- !currentConnectivity.isWifi() ||
- currentConnectivity.isMetered()) {
+ // no connection or no server configured
+ if (!currentConnectivity.isConnected() || baseServerAddress.isEmpty()) {
final var result = !currentConnectivity.isConnected();
walledCheckCache.setValue(result);
- Log_OC.d(TAG, "isInternetWalled(): early return conditions are not matched, isWalled: " + result);
+ Log_OC.d(TAG, "isInternetWalled(): no connection or server address, isWalled: " + result);
return result;
}
+ // skip HTTP call on metered non-WiFi (e.g. cellular).
+ if (!currentConnectivity.isWifi() && currentConnectivity.isMetered()) {
+ final var isWalled = !currentConnectivity.isConnected();
+ walledCheckCache.setValue(isWalled);
+ Log_OC.d(TAG, "isInternetWalled(): metered non-WiFi, skipping probe, isWalled: " + isWalled);
+ return isWalled;
+ }
+
boolean isWalled;
GetMethod get = requestBuilder.invoke(baseServerAddress + CONNECTIVITY_CHECK_ROUTE);
PlainClient client = clientFactory.createPlainClient();
try {
int status = get.execute(client);
-
- // Server is reachable and responds correctly = NOT walled
isWalled = !(status == HttpStatus.SC_NO_CONTENT && get.getResponseContentLength() <= 0);
if (isWalled) {
Log_OC.w(TAG, "isInternetWalled(): Server returned unexpected response");
From 14fe8a3877dd25aa460d6357e7f2866b96b89196 Mon Sep 17 00:00:00 2001
From: alperozturk96
Date: Tue, 31 Mar 2026 13:05:08 +0200
Subject: [PATCH 10/14] Rename .java to .kt
Signed-off-by: alperozturk96
---
.../{ConnectivityServiceImpl.java => ConnectivityServiceImpl.kt} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename app/src/main/java/com/nextcloud/client/network/{ConnectivityServiceImpl.java => ConnectivityServiceImpl.kt} (100%)
diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt
similarity index 100%
rename from app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java
rename to app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt
From 002540f36179a69a1e9bb5bdb1d392cbb7ad5e79 Mon Sep 17 00:00:00 2001
From: alperozturk96
Date: Tue, 31 Mar 2026 13:05:08 +0200
Subject: [PATCH 11/14] convert to kt
Signed-off-by: alperozturk96
---
.../client/network/ConnectivityServiceImpl.kt | 269 ++++++++----------
1 file changed, 114 insertions(+), 155 deletions(-)
diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt
index 9861fdd9cdce..eafe57649003 100644
--- a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt
+++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt
@@ -1,202 +1,161 @@
/*
- * Nextcloud Android client application
+ * Nextcloud - Android Client
*
- * @author Chris Narkiewicz
- * Copyright (C) 2021 Chris Narkiewicz
- *
- * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-package com.nextcloud.client.network;
-
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.Network;
-import android.net.NetworkCapabilities;
-import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
-
-import com.nextcloud.client.account.Server;
-import com.nextcloud.client.account.UserAccountManager;
-import com.nextcloud.common.PlainClient;
-import com.nextcloud.operations.GetMethod;
-import com.owncloud.android.lib.common.utils.Log_OC;
-
-import org.apache.commons.httpclient.HttpStatus;
-
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-import androidx.annotation.NonNull;
-
-public class ConnectivityServiceImpl implements ConnectivityService {
-
- private static final String TAG = "ConnectivityServiceImpl";
- private static final String CONNECTIVITY_CHECK_ROUTE = "/index.php/204";
-
- private final ConnectivityManager connectivityManager;
- private final UserAccountManager accountManager;
- private final ClientFactory clientFactory;
- private final GetRequestBuilder requestBuilder;
- private final WalledCheckCache walledCheckCache;
- private final Handler mainThreadHandler = new Handler(Looper.getMainLooper());
- private final ExecutorService executor = Executors.newSingleThreadExecutor();
- private Connectivity currentConnectivity = Connectivity.DISCONNECTED;
-
- private final ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() {
- @Override
- public void onAvailable(@NonNull Network network) {
- Log_OC.d(TAG, "network available");
- updateConnectivity();
+package com.nextcloud.client.network
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.os.Build
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.network.ConnectivityService.GenericCallback
+import com.nextcloud.operations.GetMethod
+import com.owncloud.android.lib.common.utils.Log_OC
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.apache.commons.httpclient.HttpStatus
+import kotlin.jvm.functions.Function1
+
+class ConnectivityServiceImpl(
+ context: Context,
+ private val accountManager: UserAccountManager,
+ private val clientFactory: ClientFactory,
+ private val requestBuilder: GetRequestBuilder,
+ private val walledCheckCache: WalledCheckCache
+) : ConnectivityService {
+
+ private val scope = CoroutineScope(Dispatchers.IO)
+ private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ private var currentConnectivity = Connectivity.DISCONNECTED
+
+ private val networkCallback = object : ConnectivityManager.NetworkCallback() {
+ override fun onAvailable(network: Network) {
+ Log_OC.d(TAG, "network available")
+ updateConnectivity()
}
- @Override
- public void onLost(@NonNull Network network) {
- Log_OC.w(TAG, "connection lost");
- updateConnectivity();
+ override fun onLost(network: Network) {
+ Log_OC.w(TAG, "connection lost")
+ updateConnectivity()
}
- @Override
- public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) {
- Log_OC.d(TAG, "capability changed");
- updateConnectivity();
+ override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
+ Log_OC.d(TAG, "capability changed")
+ updateConnectivity()
}
- };
+ }
- static class GetRequestBuilder implements kotlin.jvm.functions.Function1 {
- @Override
- public GetMethod invoke(String url) {
- return new GetMethod(url, false);
- }
+ class GetRequestBuilder : Function1 {
+ override fun invoke(url: String) = GetMethod(url, false)
}
- public ConnectivityServiceImpl(@NonNull Context context,
- @NonNull UserAccountManager accountManager,
- @NonNull ClientFactory clientFactory,
- @NonNull GetRequestBuilder requestBuilder,
- @NonNull WalledCheckCache walledCheckCache) {
- this.connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- this.accountManager = accountManager;
- this.clientFactory = clientFactory;
- this.requestBuilder = requestBuilder;
- this.walledCheckCache = walledCheckCache;
-
- // Register callback for real-time network updates
- connectivityManager.registerDefaultNetworkCallback(networkCallback);
- updateConnectivity();
- Log_OC.d(TAG, "connectivity service constructed");
+ init {
+ connectivityManager.registerDefaultNetworkCallback(networkCallback)
+ updateConnectivity()
+ Log_OC.d(TAG, "connectivity service constructed")
}
- public void updateConnectivity() {
- Network activeNetwork = connectivityManager.getActiveNetwork();
- if (activeNetwork == null) {
- Log_OC.w(TAG, "active network is null, connectivity is disconnected");
- currentConnectivity = Connectivity.DISCONNECTED;
- return;
- }
+ fun updateConnectivity() {
+ val capabilities = connectivityManager.activeNetwork
+ ?.let { connectivityManager.getNetworkCapabilities(it) }
- NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(activeNetwork);
if (capabilities == null) {
- Log_OC.w(TAG, "capabilities is null, connectivity is disconnected");
- currentConnectivity = Connectivity.DISCONNECTED;
- return;
+ Log_OC.w(TAG, "no active network or capabilities, connectivity is disconnected")
+ currentConnectivity = Connectivity.DISCONNECTED
+ return
}
- // A network is "connected" for Nextcloud if it has a valid transport,
- // even if it lacks the global INTERNET capability (e.g., local LAN).
- boolean isConnected = (capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ||
- isSupportedTransport(capabilities));
+ currentConnectivity = Connectivity(
+ isConnected = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ||
+ isSupportedTransport(capabilities),
+ isMetered = !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED),
+ isWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET),
+ )
- boolean isMetered = !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
-
- boolean isWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
- || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET);
-
- currentConnectivity = new Connectivity(isConnected, isMetered, isWifi, null);
-
- walledCheckCache.clear();
+ walledCheckCache.clear()
}
- private boolean isSupportedTransport(@NonNull NetworkCapabilities capabilities) {
- return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
+ private fun isSupportedTransport(capabilities: NetworkCapabilities) =
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE) ||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
- capabilities.hasTransport(NetworkCapabilities.TRANSPORT_USB));
- }
-
- @Override
- public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) {
- executor.execute(() -> {
- boolean available = !isInternetWalled();
- Log_OC.d(TAG, "isNetworkAndServerAvailable: " + available);
- mainThreadHandler.post(() -> callback.onComplete(available));
- });
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_USB))
+
+ override fun isNetworkAndServerAvailable(callback: GenericCallback) {
+ scope.launch {
+ val available = !isInternetWalled()
+ Log_OC.d(TAG, "isNetworkAndServerAvailable: $available")
+ withContext(Dispatchers.Main) {
+ callback.onComplete(available)
+ }
+ }
}
- @Override
- public boolean isConnected() {
- return currentConnectivity.isConnected();
- }
+ override fun isConnected() = currentConnectivity.isConnected
- @Override
- public boolean isInternetWalled() {
- Boolean cached = walledCheckCache.getValue();
- if (cached != null) {
- Log_OC.d(TAG, "isInternetWalled(): cached value is used, isWalled: " + cached);
- return cached;
+ override fun isInternetWalled(): Boolean {
+ walledCheckCache.getValue()?.let {
+ Log_OC.d(TAG, "isInternetWalled(): cached value is used, isWalled: $it")
+ return it
}
- Server server = accountManager.getUser().getServer();
- String baseServerAddress = server.getUri().toString();
+ val baseServerAddress = accountManager.user.server.uri.toString()
- // no connection or no server configured
- if (!currentConnectivity.isConnected() || baseServerAddress.isEmpty()) {
- final var result = !currentConnectivity.isConnected();
- walledCheckCache.setValue(result);
- Log_OC.d(TAG, "isInternetWalled(): no connection or server address, isWalled: " + result);
- return result;
+ // No connection or no server configured
+ if (!currentConnectivity.isConnected || baseServerAddress.isEmpty()) {
+ return (!currentConnectivity.isConnected).also {
+ walledCheckCache.setValue(it)
+ Log_OC.d(TAG, "isInternetWalled(): no connection or server address, isWalled: $it")
+ }
}
- // skip HTTP call on metered non-WiFi (e.g. cellular).
- if (!currentConnectivity.isWifi() && currentConnectivity.isMetered()) {
- final var isWalled = !currentConnectivity.isConnected();
- walledCheckCache.setValue(isWalled);
- Log_OC.d(TAG, "isInternetWalled(): metered non-WiFi, skipping probe, isWalled: " + isWalled);
- return isWalled;
+ // Skip HTTP probe on metered non-WiFi (e.g. cellular)
+ if (!currentConnectivity.isWifi && currentConnectivity.isMetered) {
+ return (!currentConnectivity.isConnected).also {
+ walledCheckCache.setValue(it)
+ Log_OC.d(TAG, "isInternetWalled(): metered non-WiFi, skipping probe, isWalled: $it")
+ }
}
- boolean isWalled;
- GetMethod get = requestBuilder.invoke(baseServerAddress + CONNECTIVITY_CHECK_ROUTE);
- PlainClient client = clientFactory.createPlainClient();
+ val get = requestBuilder.invoke(baseServerAddress + CONNECTIVITY_CHECK_ROUTE)
+ val client = clientFactory.createPlainClient()
- try {
- int status = get.execute(client);
- isWalled = !(status == HttpStatus.SC_NO_CONTENT && get.getResponseContentLength() <= 0);
- if (isWalled) {
- Log_OC.w(TAG, "isInternetWalled(): Server returned unexpected response");
+ val isWalled = try {
+ val status = get.execute(client)
+ (!(status == HttpStatus.SC_NO_CONTENT && get.getResponseContentLength() <= 0)).also {
+ if (it) Log_OC.w(TAG, "isInternetWalled(): Server returned unexpected response")
}
- } catch (Exception e) {
- Log_OC.e(TAG, "isInternetWalled(): Exception during server check", e);
- isWalled = true;
+ } catch (e: Exception) {
+ Log_OC.e(TAG, "isInternetWalled(): Exception during server check", e)
+ true
} finally {
- get.releaseConnection();
+ get.releaseConnection()
}
- walledCheckCache.setValue(isWalled);
- Log_OC.d(TAG, "isInternetWalled(): server check, isWalled: " + isWalled);
- return isWalled;
+ walledCheckCache.setValue(isWalled)
+ Log_OC.d(TAG, "isInternetWalled(): server check, isWalled: $isWalled")
+ return isWalled
}
- @Override
- public Connectivity getConnectivity() {
- return currentConnectivity;
+ override fun getConnectivity() = currentConnectivity
+
+ fun unregisterCallback() {
+ connectivityManager.unregisterNetworkCallback(networkCallback)
}
- public void unregisterCallback() {
- connectivityManager.unregisterNetworkCallback(networkCallback);
+ companion object {
+ private const val TAG = "ConnectivityServiceImpl"
+ private const val CONNECTIVITY_CHECK_ROUTE = "/index.php/204"
}
}
From 169005958ee51cefc6847d26469f54a4be8a6985 Mon Sep 17 00:00:00 2001
From: alperozturk96
Date: Tue, 31 Mar 2026 14:21:31 +0200
Subject: [PATCH 12/14] fix walled logic
Signed-off-by: alperozturk96
---
.../nextcloud/client/network/Connectivity.kt | 6 +-
.../client/network/ConnectivityServiceImpl.kt | 101 +++++++++++++-----
2 files changed, 81 insertions(+), 26 deletions(-)
diff --git a/app/src/main/java/com/nextcloud/client/network/Connectivity.kt b/app/src/main/java/com/nextcloud/client/network/Connectivity.kt
index e3b4b197c3d3..e19580ed617c 100644
--- a/app/src/main/java/com/nextcloud/client/network/Connectivity.kt
+++ b/app/src/main/java/com/nextcloud/client/network/Connectivity.kt
@@ -10,7 +10,8 @@ data class Connectivity(
val isConnected: Boolean = false,
val isMetered: Boolean = false,
val isWifi: Boolean = false,
- val isServerAvailable: Boolean? = null
+ val isServerAvailable: Boolean? = null,
+ val isVPN: Boolean = false
) {
companion object {
@JvmField
@@ -21,7 +22,8 @@ data class Connectivity(
isConnected = true,
isMetered = false,
isWifi = true,
- isServerAvailable = true
+ isServerAvailable = true,
+ isVPN = false
)
}
}
diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt
index eafe57649003..262842a39673 100644
--- a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt
+++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt
@@ -22,6 +22,7 @@ import kotlinx.coroutines.withContext
import org.apache.commons.httpclient.HttpStatus
import kotlin.jvm.functions.Function1
+@Suppress("TooGenericExceptionCaught", "ReturnCount")
class ConnectivityServiceImpl(
context: Context,
private val accountManager: UserAccountManager,
@@ -71,12 +72,15 @@ class ConnectivityServiceImpl(
return
}
+ val hasTransport = isSupportedTransport(capabilities)
+ val hasInternetCapability = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+
currentConnectivity = Connectivity(
- isConnected = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ||
- isSupportedTransport(capabilities),
+ isConnected = hasTransport || hasInternetCapability,
isMetered = !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED),
isWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET),
+ isVPN = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
)
walledCheckCache.clear()
@@ -89,10 +93,12 @@ class ConnectivityServiceImpl(
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE) ||
- (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
- capabilities.hasTransport(NetworkCapabilities.TRANSPORT_USB))
+ (
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_USB)
+ )
- override fun isNetworkAndServerAvailable(callback: GenericCallback) {
+ override fun isNetworkAndServerAvailable(callback: GenericCallback) {
scope.launch {
val available = !isInternetWalled()
Log_OC.d(TAG, "isNetworkAndServerAvailable: $available")
@@ -105,27 +111,47 @@ class ConnectivityServiceImpl(
override fun isConnected() = currentConnectivity.isConnected
override fun isInternetWalled(): Boolean {
- walledCheckCache.getValue()?.let {
- Log_OC.d(TAG, "isInternetWalled(): cached value is used, isWalled: $it")
- return it
+ val cachedValue = walledCheckCache.getValue()
+ if (cachedValue != null) {
+ Log_OC.d(TAG, "cached value is used, isWalled: $cachedValue")
+ return cachedValue
}
val baseServerAddress = accountManager.user.server.uri.toString()
+ if (baseServerAddress.isEmpty()) {
+ Log_OC.e(TAG, "no base server address, internet is walled")
+ walledCheckCache.setValue(true)
+ return true
+ }
- // No connection or no server configured
- if (!currentConnectivity.isConnected || baseServerAddress.isEmpty()) {
- return (!currentConnectivity.isConnected).also {
- walledCheckCache.setValue(it)
- Log_OC.d(TAG, "isInternetWalled(): no connection or server address, isWalled: $it")
- }
+ val activeCapabilities = connectivityManager.activeNetwork
+ ?.let { connectivityManager.getNetworkCapabilities(it) }
+
+ if (activeCapabilities == null) {
+ Log_OC.e(TAG, "no active network capabilities at check time, treating as walled")
+ walledCheckCache.setValue(true)
+ return true
}
- // Skip HTTP probe on metered non-WiFi (e.g. cellular)
- if (!currentConnectivity.isWifi && currentConnectivity.isMetered) {
- return (!currentConnectivity.isConnected).also {
- walledCheckCache.setValue(it)
- Log_OC.d(TAG, "isInternetWalled(): metered non-WiFi, skipping probe, isWalled: $it")
- }
+ val hasLiveTransport = isSupportedTransport(activeCapabilities)
+ if (!hasLiveTransport) {
+ Log_OC.e(TAG, "no supported transport at check time, treating as walled")
+ walledCheckCache.setValue(true)
+ return true
+ }
+
+ val isVpnActive = activeCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
+ if (isVpnActive) {
+ Log_OC.w(TAG, "skipping server reachability check, VPN is active")
+ walledCheckCache.setValue(false)
+ return false
+ }
+
+ val isMeteredNonWifi = !currentConnectivity.isWifi && currentConnectivity.isMetered
+ if (isMeteredNonWifi) {
+ Log_OC.w(TAG, "skipping server reachability check, internet is metered and not Wi-Fi")
+ walledCheckCache.setValue(false)
+ return false
}
val get = requestBuilder.invoke(baseServerAddress + CONNECTIVITY_CHECK_ROUTE)
@@ -134,22 +160,49 @@ class ConnectivityServiceImpl(
val isWalled = try {
val status = get.execute(client)
(!(status == HttpStatus.SC_NO_CONTENT && get.getResponseContentLength() <= 0)).also {
- if (it) Log_OC.w(TAG, "isInternetWalled(): Server returned unexpected response")
+ if (it) Log_OC.w(TAG, "server returned unexpected response")
}
} catch (e: Exception) {
- Log_OC.e(TAG, "isInternetWalled(): Exception during server check", e)
- true
+ Log_OC.e(TAG, "exception during server check", e)
+ getWalledValueFromException(e)
} finally {
get.releaseConnection()
}
walledCheckCache.setValue(isWalled)
- Log_OC.d(TAG, "isInternetWalled(): server check, isWalled: $isWalled")
+ Log_OC.d(TAG, "server check, isWalled: $isWalled")
return isWalled
}
override fun getConnectivity() = currentConnectivity
+ private fun getWalledValueFromException(e: Exception): Boolean = when (e) {
+ is java.net.UnknownHostException -> {
+ Log_OC.w(TAG, "UnknownHostException")
+ false
+ }
+
+ is javax.net.ssl.SSLException -> {
+ Log_OC.w(TAG, "SSLException")
+ false
+ }
+
+ is java.net.SocketTimeoutException -> {
+ Log_OC.w(TAG, "SocketTimeoutException")
+ false
+ }
+
+ is java.io.IOException -> {
+ Log_OC.w(TAG, "IOException")
+ false
+ }
+
+ else -> {
+ Log_OC.w(TAG, "Unknown error, fallback to walled assumption")
+ true
+ }
+ }
+
fun unregisterCallback() {
connectivityManager.unregisterNetworkCallback(networkCallback)
}
From 264613a2b2330887a4edbd4f321baf033ca86d70 Mon Sep 17 00:00:00 2001
From: alperozturk96
Date: Tue, 31 Mar 2026 14:25:17 +0200
Subject: [PATCH 13/14] fix isNetworkAndServerAvailable logic
Signed-off-by: alperozturk96
---
.../com/nextcloud/client/network/ConnectivityServiceImpl.kt | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt
index 262842a39673..8d23b1e818ec 100644
--- a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt
+++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt
@@ -17,6 +17,7 @@ import com.nextcloud.operations.GetMethod
import com.owncloud.android.lib.common.utils.Log_OC
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.apache.commons.httpclient.HttpStatus
@@ -32,6 +33,7 @@ class ConnectivityServiceImpl(
) : ConnectivityService {
private val scope = CoroutineScope(Dispatchers.IO)
+ private var availabilityCheckJob: Job? = null
private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private var currentConnectivity = Connectivity.DISCONNECTED
@@ -99,7 +101,8 @@ class ConnectivityServiceImpl(
)
override fun isNetworkAndServerAvailable(callback: GenericCallback) {
- scope.launch {
+ availabilityCheckJob?.cancel()
+ availabilityCheckJob = scope.launch {
val available = !isInternetWalled()
Log_OC.d(TAG, "isNetworkAndServerAvailable: $available")
withContext(Dispatchers.Main) {
From 7d56e29ac18bbd450b3b0b260c5c588b080c5057 Mon Sep 17 00:00:00 2001
From: alperozturk96
Date: Tue, 31 Mar 2026 16:07:59 +0200
Subject: [PATCH 14/14] fix network change listener
Signed-off-by: alperozturk96
---
.../test/ConnectivityServiceOfflineMock.kt | 6 +--
.../java/com/owncloud/android/AbstractIT.java | 11 +++++
.../owncloud/android/AbstractOnServerIT.java | 11 +++++
.../java/com/owncloud/android/UploadIT.java | 35 +++++++++++++++-
.../android/files/services/FileUploaderIT.kt | 5 ++-
.../java/com/nextcloud/test/TestActivity.kt | 6 +--
.../nextcloud/client/di/ComponentsModule.java | 4 --
.../client/network/ConnectivityService.java | 7 ++--
.../client/network/ConnectivityServiceImpl.kt | 42 ++++++++++++-------
.../client/network/NetworkChangeListener.kt | 12 ++++++
.../receiver/NetworkChangeReceiver.kt | 29 -------------
.../java/com/owncloud/android/MainApp.java | 15 ++-----
.../android/ui/activity/FileActivity.java | 24 ++++-------
13 files changed, 116 insertions(+), 91 deletions(-)
create mode 100644 app/src/main/java/com/nextcloud/client/network/NetworkChangeListener.kt
delete mode 100644 app/src/main/java/com/nextcloud/receiver/NetworkChangeReceiver.kt
diff --git a/app/src/androidTest/java/com/nextcloud/test/ConnectivityServiceOfflineMock.kt b/app/src/androidTest/java/com/nextcloud/test/ConnectivityServiceOfflineMock.kt
index 560b94ff6172..d9efbb954040 100644
--- a/app/src/androidTest/java/com/nextcloud/test/ConnectivityServiceOfflineMock.kt
+++ b/app/src/androidTest/java/com/nextcloud/test/ConnectivityServiceOfflineMock.kt
@@ -8,16 +8,16 @@ package com.nextcloud.test
import com.nextcloud.client.network.Connectivity
import com.nextcloud.client.network.ConnectivityService
+import com.nextcloud.client.network.NetworkChangeListener
/** A mocked connectivity service returning that the device is offline **/
class ConnectivityServiceOfflineMock : ConnectivityService {
+ override fun addListener(listener: NetworkChangeListener) = Unit
+ override fun removeListener(listener: NetworkChangeListener) = Unit
override fun isNetworkAndServerAvailable(callback: ConnectivityService.GenericCallback) {
callback.onComplete(false)
}
-
override fun isConnected(): Boolean = false
-
override fun isInternetWalled(): Boolean = false
-
override fun getConnectivity(): Connectivity = Connectivity.CONNECTED_WIFI
}
diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java
index ac58ccc05fc9..460457289e42 100644
--- a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java
+++ b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java
@@ -32,6 +32,7 @@
import com.nextcloud.client.jobs.upload.FileUploadWorker;
import com.nextcloud.client.network.Connectivity;
import com.nextcloud.client.network.ConnectivityService;
+import com.nextcloud.client.network.NetworkChangeListener;
import com.nextcloud.client.preferences.AppPreferencesImpl;
import com.nextcloud.client.preferences.DarkMode;
import com.nextcloud.common.NextcloudClient;
@@ -371,6 +372,16 @@ public void uploadFile(File file, String remotePath) {
public void uploadOCUpload(OCUpload ocUpload) {
ConnectivityService connectivityServiceMock = new ConnectivityService() {
+ @Override
+ public void addListener(@NonNull NetworkChangeListener listener) {
+
+ }
+
+ @Override
+ public void removeListener(@NonNull NetworkChangeListener listener) {
+
+ }
+
@Override
public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) {
diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java
index 1b0e1b8d3c17..2d6031790c93 100644
--- a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java
+++ b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java
@@ -21,6 +21,7 @@
import com.nextcloud.client.jobs.upload.FileUploadWorker;
import com.nextcloud.client.network.Connectivity;
import com.nextcloud.client.network.ConnectivityService;
+import com.nextcloud.client.network.NetworkChangeListener;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.UploadsStorageManager;
import com.owncloud.android.db.OCUpload;
@@ -203,6 +204,16 @@ public void uploadOCUpload(OCUpload ocUpload) {
public void uploadOCUpload(OCUpload ocUpload, int localBehaviour) {
ConnectivityService connectivityServiceMock = new ConnectivityService() {
+ @Override
+ public void addListener(@NonNull NetworkChangeListener listener) {
+
+ }
+
+ @Override
+ public void removeListener(@NonNull NetworkChangeListener listener) {
+
+ }
+
@Override
public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) {
diff --git a/app/src/androidTest/java/com/owncloud/android/UploadIT.java b/app/src/androidTest/java/com/owncloud/android/UploadIT.java
index 8072bb5c1805..e8a9e442e8a3 100644
--- a/app/src/androidTest/java/com/owncloud/android/UploadIT.java
+++ b/app/src/androidTest/java/com/owncloud/android/UploadIT.java
@@ -14,6 +14,7 @@
import com.nextcloud.client.jobs.upload.FileUploadWorker;
import com.nextcloud.client.network.Connectivity;
import com.nextcloud.client.network.ConnectivityService;
+import com.nextcloud.client.network.NetworkChangeListener;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.UploadsStorageManager;
import com.owncloud.android.db.OCUpload;
@@ -56,6 +57,16 @@ public class UploadIT extends AbstractOnServerIT {
targetContext.getContentResolver());
private ConnectivityService connectivityServiceMock = new ConnectivityService() {
+ @Override
+ public void addListener(@NonNull NetworkChangeListener listener) {
+
+ }
+
+ @Override
+ public void removeListener(@NonNull NetworkChangeListener listener) {
+
+ }
+
@Override
public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) {
@@ -268,6 +279,16 @@ public BatteryStatus getBattery() {
@Test
public void testUploadOnWifiOnlyButNoWifi() {
ConnectivityService connectivityServiceMock = new ConnectivityService() {
+ @Override
+ public void addListener(@NonNull NetworkChangeListener listener) {
+
+ }
+
+ @Override
+ public void removeListener(@NonNull NetworkChangeListener listener) {
+
+ }
+
@Override
public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) {
@@ -285,7 +306,7 @@ public boolean isInternetWalled() {
@Override
public Connectivity getConnectivity() {
- return new Connectivity(true, false, false, true);
+ return new Connectivity(true, false, false, true, false);
}
};
OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/empty.txt",
@@ -357,6 +378,16 @@ public void testUploadOnWifiOnlyAndWifi() {
@Test
public void testUploadOnWifiOnlyButMeteredWifi() {
ConnectivityService connectivityServiceMock = new ConnectivityService() {
+ @Override
+ public void addListener(@NonNull NetworkChangeListener listener) {
+
+ }
+
+ @Override
+ public void removeListener(@NonNull NetworkChangeListener listener) {
+
+ }
+
@Override
public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) {
@@ -374,7 +405,7 @@ public boolean isInternetWalled() {
@Override
public Connectivity getConnectivity() {
- return new Connectivity(true, true, true, true);
+ return new Connectivity(true, true, true, true, false);
}
};
OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/empty.txt",
diff --git a/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt b/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt
index 00c568d506ad..874025818bba 100644
--- a/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt
+++ b/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt
@@ -16,6 +16,7 @@ import com.nextcloud.client.jobs.upload.FileUploadHelper
import com.nextcloud.client.jobs.upload.FileUploadWorker
import com.nextcloud.client.network.Connectivity
import com.nextcloud.client.network.ConnectivityService
+import com.nextcloud.client.network.NetworkChangeListener
import com.owncloud.android.AbstractOnServerIT
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.UploadsStorageManager
@@ -34,10 +35,10 @@ abstract class FileUploaderIT : AbstractOnServerIT() {
private var uploadsStorageManager: UploadsStorageManager? = null
private val connectivityServiceMock: ConnectivityService = object : ConnectivityService {
+ override fun addListener(listener: NetworkChangeListener) = Unit
+ override fun removeListener(listener: NetworkChangeListener) = Unit
override fun isNetworkAndServerAvailable(callback: ConnectivityService.GenericCallback) = Unit
-
override fun isConnected(): Boolean = false
-
override fun isInternetWalled(): Boolean = false
override fun getConnectivity(): Connectivity = Connectivity.CONNECTED_WIFI
}
diff --git a/app/src/debug/java/com/nextcloud/test/TestActivity.kt b/app/src/debug/java/com/nextcloud/test/TestActivity.kt
index 5bbbf0258550..5f5bc6fd0c18 100644
--- a/app/src/debug/java/com/nextcloud/test/TestActivity.kt
+++ b/app/src/debug/java/com/nextcloud/test/TestActivity.kt
@@ -16,6 +16,7 @@ import com.nextcloud.client.jobs.download.FileDownloadWorker
import com.nextcloud.client.jobs.upload.FileUploadHelper
import com.nextcloud.client.network.Connectivity
import com.nextcloud.client.network.ConnectivityService
+import com.nextcloud.client.network.NetworkChangeListener
import com.nextcloud.utils.EditorUtils
import com.owncloud.android.R
import com.owncloud.android.databinding.TestLayoutBinding
@@ -43,12 +44,11 @@ class TestActivity :
private lateinit var binding: TestLayoutBinding
val connectivityServiceMock: ConnectivityService = object : ConnectivityService {
+ override fun addListener(listener: NetworkChangeListener) = Unit
+ override fun removeListener(listener: NetworkChangeListener) = Unit
override fun isNetworkAndServerAvailable(callback: ConnectivityService.GenericCallback) = Unit
-
override fun isConnected(): Boolean = false
-
override fun isInternetWalled(): Boolean = false
-
override fun getConnectivity(): Connectivity = Connectivity.CONNECTED_WIFI
}
diff --git a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
index c2e55a52afa1..1ce60564884f 100644
--- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
+++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
@@ -26,7 +26,6 @@
import com.nextcloud.client.widget.DashboardWidgetConfigurationActivity;
import com.nextcloud.client.widget.DashboardWidgetProvider;
import com.nextcloud.client.widget.DashboardWidgetService;
-import com.nextcloud.receiver.NetworkChangeReceiver;
import com.nextcloud.ui.ChooseAccountDialogFragment;
import com.nextcloud.ui.ChooseStorageLocationDialogFragment;
import com.nextcloud.ui.ImageDetailFragment;
@@ -324,9 +323,6 @@ abstract class ComponentsModule {
@ContributesAndroidInjector
abstract BootupBroadcastReceiver bootupBroadcastReceiver();
- @ContributesAndroidInjector
- abstract NetworkChangeReceiver networkChangeReceiver();
-
@ContributesAndroidInjector
abstract NotificationWork.NotificationReceiver notificationWorkBroadcastReceiver();
diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java b/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java
index 1975fb22de18..9329dc284739 100644
--- a/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java
+++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java
@@ -9,10 +9,6 @@
import android.net.ConnectivityManager;
import android.net.Network;
-
-import com.nextcloud.client.account.Server;
-import com.nextcloud.client.account.UserAccountManager;
-
import androidx.annotation.NonNull;
/**
@@ -20,6 +16,9 @@
* and server reachability.
*/
public interface ConnectivityService {
+ void addListener(@NonNull NetworkChangeListener listener);
+ void removeListener(@NonNull NetworkChangeListener listener);
+
/**
* Asynchronously checks whether both the device's network connection
* and the Nextcloud server are available.
diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt
index 8d23b1e818ec..392aad827303 100644
--- a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt
+++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt
@@ -14,6 +14,7 @@ import android.os.Build
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.network.ConnectivityService.GenericCallback
import com.nextcloud.operations.GetMethod
+import com.nextcloud.utils.extensions.showToast
import com.owncloud.android.lib.common.utils.Log_OC
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -25,7 +26,7 @@ import kotlin.jvm.functions.Function1
@Suppress("TooGenericExceptionCaught", "ReturnCount")
class ConnectivityServiceImpl(
- context: Context,
+ private val context: Context,
private val accountManager: UserAccountManager,
private val clientFactory: ClientFactory,
private val requestBuilder: GetRequestBuilder,
@@ -36,13 +37,30 @@ class ConnectivityServiceImpl(
private var availabilityCheckJob: Job? = null
private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private var currentConnectivity = Connectivity.DISCONNECTED
+ private val listeners = mutableSetOf()
- private val networkCallback = object : ConnectivityManager.NetworkCallback() {
- override fun onAvailable(network: Network) {
- Log_OC.d(TAG, "network available")
- updateConnectivity()
+ override fun addListener(listener: NetworkChangeListener) {
+ listeners.add(listener)
+ }
+
+ override fun removeListener(listener: NetworkChangeListener) {
+ listeners.remove(listener)
+ }
+
+ private fun notifyListeners() {
+ scope.launch {
+ val available = !isInternetWalled()
+ withContext(Dispatchers.Main) {
+ listeners.forEach {
+ Log_OC.d(TAG, "notifying listeners")
+ context.showToast("NOTIFIYING LISTENERS !!!: $available")
+ it.networkAndServerConnectionListener(available)
+ }
+ }
}
+ }
+ private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onLost(network: Network) {
Log_OC.w(TAG, "connection lost")
updateConnectivity()
@@ -71,6 +89,8 @@ class ConnectivityServiceImpl(
if (capabilities == null) {
Log_OC.w(TAG, "no active network or capabilities, connectivity is disconnected")
currentConnectivity = Connectivity.DISCONNECTED
+ walledCheckCache.clear()
+ notifyListeners()
return
}
@@ -86,6 +106,7 @@ class ConnectivityServiceImpl(
)
walledCheckCache.clear()
+ notifyListeners()
}
private fun isSupportedTransport(capabilities: NetworkCapabilities) =
@@ -123,7 +144,6 @@ class ConnectivityServiceImpl(
val baseServerAddress = accountManager.user.server.uri.toString()
if (baseServerAddress.isEmpty()) {
Log_OC.e(TAG, "no base server address, internet is walled")
- walledCheckCache.setValue(true)
return true
}
@@ -132,28 +152,18 @@ class ConnectivityServiceImpl(
if (activeCapabilities == null) {
Log_OC.e(TAG, "no active network capabilities at check time, treating as walled")
- walledCheckCache.setValue(true)
return true
}
val hasLiveTransport = isSupportedTransport(activeCapabilities)
if (!hasLiveTransport) {
Log_OC.e(TAG, "no supported transport at check time, treating as walled")
- walledCheckCache.setValue(true)
return true
}
- val isVpnActive = activeCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
- if (isVpnActive) {
- Log_OC.w(TAG, "skipping server reachability check, VPN is active")
- walledCheckCache.setValue(false)
- return false
- }
-
val isMeteredNonWifi = !currentConnectivity.isWifi && currentConnectivity.isMetered
if (isMeteredNonWifi) {
Log_OC.w(TAG, "skipping server reachability check, internet is metered and not Wi-Fi")
- walledCheckCache.setValue(false)
return false
}
diff --git a/app/src/main/java/com/nextcloud/client/network/NetworkChangeListener.kt b/app/src/main/java/com/nextcloud/client/network/NetworkChangeListener.kt
new file mode 100644
index 000000000000..572fd87ca9b2
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/client/network/NetworkChangeListener.kt
@@ -0,0 +1,12 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Alper Ozturk
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package com.nextcloud.client.network
+
+interface NetworkChangeListener {
+ fun networkAndServerConnectionListener(isNetworkAndServerAvailable: Boolean)
+}
diff --git a/app/src/main/java/com/nextcloud/receiver/NetworkChangeReceiver.kt b/app/src/main/java/com/nextcloud/receiver/NetworkChangeReceiver.kt
deleted file mode 100644
index d9d7cbea18ef..000000000000
--- a/app/src/main/java/com/nextcloud/receiver/NetworkChangeReceiver.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Nextcloud - Android Client
- *
- * SPDX-FileCopyrightText: 2024 Alper Ozturk
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-package com.nextcloud.receiver
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import com.nextcloud.client.network.ConnectivityService
-
-interface NetworkChangeListener {
- fun networkAndServerConnectionListener(isNetworkAndServerAvailable: Boolean)
-}
-
-class NetworkChangeReceiver(
- private val listener: NetworkChangeListener,
- private val connectivityService: ConnectivityService
-) : BroadcastReceiver() {
-
- override fun onReceive(context: Context, intent: Intent?) {
- connectivityService.isNetworkAndServerAvailable {
- listener.networkAndServerConnectionListener(it)
- }
- }
-}
diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java
index bea262738e5b..f08bde45fee9 100644
--- a/app/src/main/java/com/owncloud/android/MainApp.java
+++ b/app/src/main/java/com/owncloud/android/MainApp.java
@@ -52,13 +52,12 @@
import com.nextcloud.client.logger.Logger;
import com.nextcloud.client.migrations.MigrationsManager;
import com.nextcloud.client.network.ConnectivityService;
+import com.nextcloud.client.network.NetworkChangeListener;
import com.nextcloud.client.network.WalledCheckCache;
import com.nextcloud.client.onboarding.OnboardingService;
import com.nextcloud.client.preferences.AppPreferences;
import com.nextcloud.client.preferences.AppPreferencesImpl;
import com.nextcloud.client.preferences.DarkMode;
-import com.nextcloud.receiver.NetworkChangeListener;
-import com.nextcloud.receiver.NetworkChangeReceiver;
import com.nextcloud.ui.composeActivity.ComposeProcessTextAlias;
import com.nextcloud.utils.extensions.ContextExtensionsKt;
import com.nextcloud.utils.mdm.MDMConfig;
@@ -205,8 +204,6 @@ public class MainApp extends Application implements HasAndroidInjector, NetworkC
private static AppComponent appComponent;
- private NetworkChangeReceiver networkChangeReceiver;
-
/**
* Temporary hack
*/
@@ -230,11 +227,6 @@ public PowerManagementService getPowerManagementService() {
return powerManagementService;
}
- private void registerNetworkChangeReceiver() {
- IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
- registerReceiver(networkChangeReceiver, filter);
- }
-
private String getAppProcessName() {
return Application.getProcessName();
}
@@ -371,12 +363,10 @@ public void onCreate() {
}
registerGlobalPassCodeProtection();
- networkChangeReceiver = new NetworkChangeReceiver(this, connectivityService);
- registerNetworkChangeReceiver();
-
if (!MDMConfig.INSTANCE.sendFilesSupport(this)) {
disableDocumentsStorageProvider();
}
+ connectivityService.addListener(this);
}
public void disableDocumentsStorageProvider() {
@@ -1034,6 +1024,7 @@ public void networkAndServerConnectionListener(boolean isNetworkAndServerAvailab
@Override
public void onTerminate() {
super.onTerminate();
+ connectivityService.removeListener(this);
ReceiversHelper.shutdown();
}
}
diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java
index ec834e983c1d..c17092e9886a 100644
--- a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java
+++ b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java
@@ -21,10 +21,8 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
-import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -38,8 +36,7 @@
import com.nextcloud.client.jobs.download.FileDownloadWorker;
import com.nextcloud.client.jobs.upload.FileUploadHelper;
import com.nextcloud.client.network.ConnectivityService;
-import com.nextcloud.receiver.NetworkChangeListener;
-import com.nextcloud.receiver.NetworkChangeReceiver;
+import com.nextcloud.client.network.NetworkChangeListener;
import com.nextcloud.utils.EditorUtils;
import com.nextcloud.utils.extensions.ActivityExtensionsKt;
import com.nextcloud.utils.extensions.BundleExtensionsKt;
@@ -191,15 +188,8 @@ public abstract class FileActivity extends DrawerActivity
@Inject
ArbitraryDataProvider arbitraryDataProvider;
- private NetworkChangeReceiver networkChangeReceiver;
-
private FilesRepository filesRepository;
- private void registerNetworkChangeReceiver() {
- IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
- registerReceiver(networkChangeReceiver, filter);
- }
-
@Override
public void showFiles(boolean onDeviceOnly, boolean personalFiles) {
// must be specialized in subclasses
@@ -222,7 +212,6 @@ public void showFiles(boolean onDeviceOnly, boolean personalFiles) {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- networkChangeReceiver = new NetworkChangeReceiver(this, connectivityService);
usersAndGroupsSearchConfig.reset();
mHandler = new Handler();
mFileOperationsHelper = new FileOperationsHelper(this, getUserAccountManager(), connectivityService, editorUtils);
@@ -252,8 +241,6 @@ protected void onCreate(Bundle savedInstanceState) {
mOperationsServiceConnection = new OperationsServiceConnection();
bindService(new Intent(this, OperationsService.class), mOperationsServiceConnection,
Context.BIND_AUTO_CREATE);
- registerNetworkChangeReceiver();
-
filesRepository = new RemoteFilesRepository(getClientRepository(), this);
}
@@ -278,9 +265,16 @@ public void networkAndServerConnectionListener(boolean isNetworkAndServerAvailab
@Override
protected void onStart() {
super.onStart();
+ connectivityService.addListener(this);
fetchExternalLinks(false);
}
+ @Override
+ protected void onStop() {
+ super.onStop();
+ connectivityService.removeListener(this);
+ }
+
@Override
protected void onResume() {
super.onResume();
@@ -306,8 +300,6 @@ protected void onDestroy() {
mOperationsServiceBinder = null;
}
- unregisterReceiver(networkChangeReceiver);
-
super.onDestroy();
}