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/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/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/ConnectivityService.java b/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java index 7da4afe6ea85..9329dc284739 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,8 @@ package com.nextcloud.client.network; +import android.net.ConnectivityManager; +import android.net.Network; import androidx.annotation.NonNull; /** @@ -14,32 +16,71 @@ * and server reachability. */ public interface ConnectivityService { + void addListener(@NonNull NetworkChangeListener listener); + void removeListener(@NonNull NetworkChangeListener listener); + /** - * 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}.

* - * @param callback A callback to handle the result of the network and server availability check. + *

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

* - * @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 deleted file mode 100644 index ad6f07a0456b..000000000000 --- a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Chris Narkiewicz - * Copyright (C) 2021 Chris Narkiewicz - * - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ - -package com.nextcloud.client.network; - -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; - -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 androidx.annotation.NonNull; -import androidx.core.net.ConnectivityManagerCompat; -import kotlin.jvm.functions.Function1; - -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 UserAccountManager accountManager; - private final ClientFactory clientFactory; - private final GetRequestBuilder requestBuilder; - private final WalledCheckCache walledCheckCache; - private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); - - static class GetRequestBuilder implements 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; - this.accountManager = accountManager; - this.clientFactory = clientFactory; - this.requestBuilder = requestBuilder; - this.walledCheckCache = walledCheckCache; - } - - @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(); - } - - @Override - public boolean isConnected() { - Network nw = platformConnectivityManager.getActiveNetwork(); - 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; - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && - actNw.hasTransport(NetworkCapabilities.TRANSPORT_USB)) { - return true; - } - - Log_OC.e(TAG, "network is not connected"); - return false; - } - - @Override - 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"); - } - - walledCheckCache.setValue(result); - return result; - } - } - - @Override - public Connectivity getConnectivity() { - NetworkInfo networkInfo; - 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(); - - if (isMetered) { - Log_OC.w(TAG, "getConnectivity(): network is metered"); - } - - if (!isWifi) { - Log_OC.w(TAG, "getConnectivity(): network is not wi-fi"); - } - - if (!isConnected) { - Log_OC.e(TAG, "getConnectivity(): network is not connected"); - } - - return new Connectivity(isConnected, isMetered, isWifi, null); - } else { - return Connectivity.DISCONNECTED; - } - } - - 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; - } - } - - 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; - } -} diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt new file mode 100644 index 000000000000..392aad827303 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt @@ -0,0 +1,227 @@ +/* + * Nextcloud - Android Client + * + * 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 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 +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.apache.commons.httpclient.HttpStatus +import kotlin.jvm.functions.Function1 + +@Suppress("TooGenericExceptionCaught", "ReturnCount") +class ConnectivityServiceImpl( + private val 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 var availabilityCheckJob: Job? = null + private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + private var currentConnectivity = Connectivity.DISCONNECTED + private val listeners = mutableSetOf() + + 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() + } + + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + Log_OC.d(TAG, "capability changed") + updateConnectivity() + } + } + + class GetRequestBuilder : Function1 { + override fun invoke(url: String) = GetMethod(url, false) + } + + init { + connectivityManager.registerDefaultNetworkCallback(networkCallback) + updateConnectivity() + Log_OC.d(TAG, "connectivity service constructed") + } + + fun updateConnectivity() { + val capabilities = connectivityManager.activeNetwork + ?.let { connectivityManager.getNetworkCapabilities(it) } + + if (capabilities == null) { + Log_OC.w(TAG, "no active network or capabilities, connectivity is disconnected") + currentConnectivity = Connectivity.DISCONNECTED + walledCheckCache.clear() + notifyListeners() + return + } + + val hasTransport = isSupportedTransport(capabilities) + val hasInternetCapability = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + + currentConnectivity = Connectivity( + 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() + notifyListeners() + } + + 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 fun isNetworkAndServerAvailable(callback: GenericCallback) { + availabilityCheckJob?.cancel() + availabilityCheckJob = scope.launch { + val available = !isInternetWalled() + Log_OC.d(TAG, "isNetworkAndServerAvailable: $available") + withContext(Dispatchers.Main) { + callback.onComplete(available) + } + } + } + + override fun isConnected() = currentConnectivity.isConnected + + override fun isInternetWalled(): Boolean { + 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") + return true + } + + 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") + return true + } + + val hasLiveTransport = isSupportedTransport(activeCapabilities) + if (!hasLiveTransport) { + Log_OC.e(TAG, "no supported transport at check time, treating as walled") + return true + } + + val isMeteredNonWifi = !currentConnectivity.isWifi && currentConnectivity.isMetered + if (isMeteredNonWifi) { + Log_OC.w(TAG, "skipping server reachability check, internet is metered and not Wi-Fi") + return false + } + + val get = requestBuilder.invoke(baseServerAddress + CONNECTIVITY_CHECK_ROUTE) + val client = clientFactory.createPlainClient() + + val isWalled = try { + val status = get.execute(client) + (!(status == HttpStatus.SC_NO_CONTENT && get.getResponseContentLength() <= 0)).also { + if (it) Log_OC.w(TAG, "server returned unexpected response") + } + } catch (e: Exception) { + Log_OC.e(TAG, "exception during server check", e) + getWalledValueFromException(e) + } finally { + get.releaseConnection() + } + + walledCheckCache.setValue(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) + } + + companion object { + private const val TAG = "ConnectivityServiceImpl" + private const val CONNECTIVITY_CHECK_ROUTE = "/index.php/204" + } +} 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/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/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(); } 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..7b74d56c7a92 100644 --- a/app/src/test/java/com/nextcloud/client/network/ConnectivityServiceTest.kt +++ b/app/src/test/java/com/nextcloud/client/network/ConnectivityServiceTest.kt @@ -6,19 +6,19 @@ */ package com.nextcloud.client.network +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 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 @@ -31,7 +31,6 @@ 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 import org.mockito.kotlin.verify @@ -49,22 +48,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 platformConnectivityManager: ConnectivityManager + lateinit var context: Context @Mock - lateinit var networkInfo: NetworkInfo + lateinit var platformConnectivityManager: ConnectivityManager @Mock lateinit var accountManager: UserAccountManager @@ -90,10 +81,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) @@ -104,85 +92,100 @@ class ConnectivityServiceTest { @Before fun setUpMocks() { - MockitoAnnotations.initMocks(this) - connectivityService = ConnectivityServiceImpl( - platformConnectivityManager, - accountManager, - clientFactory, - requestBuilder, - walledCheckCache - ) + MockitoAnnotations.openMocks(this) + + whenever(context.getSystemService(Context.CONNECTIVITY_SERVICE)) + .thenReturn(platformConnectivityManager) - 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(platformConnectivityManager.getNetworkCapabilities(network)) + .thenReturn(networkCapabilities) + + whenever(networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) + .thenReturn(true) + whenever( + networkCapabilities + .hasCapability(eq(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)) + ) + .thenReturn(true) + 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( + context, + accountManager, + clientFactory, + requestBuilder, + walledCheckCache + ) } } internal class Disconnected : Base() { @Test - fun `wifi is disconnected`() { - whenever(networkInfo.isConnectedOrConnecting).thenReturn(false) - whenever(networkInfo.type).thenReturn(ConnectivityManager.TYPE_WIFI) - connectivityService.connectivity.apply { - assertFalse(isConnected) - assertTrue(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.activeNetworkInfo).thenReturn(null) + 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(networkInfo.isConnectedOrConnecting).thenReturn(true) - whenever(networkInfo.type).thenReturn(ConnectivityManager.TYPE_WIFI) + // GIVEN: Default setup is connected Wi-Fi + // WHEN + connectivityService.updateConnectivity() + // THEN 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) + fun `connected to mobile network`() { + // 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) - assertTrue(it.isWifi) + assertFalse(it.isWifi) } } @Test - fun `connected to mobile network`() { - whenever(networkInfo.isConnectedOrConnecting).thenReturn(true) - whenever(networkInfo.type).thenReturn(ConnectivityManager.TYPE_MOBILE) - whenever(platformConnectivityManager.allNetworkInfo).thenReturn(arrayOf(networkInfo)) + 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) assertFalse(it.isWifi) @@ -191,12 +194,10 @@ class ConnectivityServiceTest { } internal class WifiConnectionWalledStatusOnLegacyServer : Base() { - @Before fun setUp() { - whenever(networkInfo.isConnectedOrConnecting).thenReturn(true) - whenever(networkInfo.type).thenReturn(ConnectivityManager.TYPE_WIFI) whenever(user.server).thenReturn(legacyServer) + connectivityService.updateConnectivity() assertTrue( "Precondition failed", connectivityService.connectivity.let { @@ -236,12 +237,9 @@ class ConnectivityServiceTest { } internal class WifiConnectionWalledStatus : Base() { - @Before fun setUp() { - whenever(networkInfo.isConnectedOrConnecting).thenReturn(true) - whenever(networkInfo.type).thenReturn(ConnectivityManager.TYPE_WIFI) - whenever(accountManager.getServerVersion(any())).thenReturn(OwnCloudVersion.nextcloud_20) + connectivityService.updateConnectivity() connectivityService.connectivity.let { assertTrue(it.isConnected) assertTrue(it.isWifi) @@ -253,8 +251,9 @@ 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(platformConnectivityManager.activeNetwork).thenReturn(null) + connectivityService.updateConnectivity() + assertFalse("Precondition failed", connectivityService.isConnected) // WHEN // connectivity is checked @@ -263,23 +262,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 @@ -287,30 +292,8 @@ 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()) - } - - @Test - fun `check cache value 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 - assertFalse("Cached value not set", result) - verify(requestBuilder, never()).invoke(any()) - verify(getRequest, never()).execute(any()) + // request IS sent + assertEquals(false, result) } fun mockResponse(contentLength: Long = 0, status: Int = HttpStatus.SC_OK) { @@ -347,7 +330,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) } }