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(); }