Skip to content

Commit bb34886

Browse files
committed
fix: connection service impl
Signed-off-by: alperozturk <alper_ozturk@proton.me>
1 parent 11366c6 commit bb34886

File tree

5 files changed

+219
-179
lines changed

5 files changed

+219
-179
lines changed

app/src/androidTest/java/com/nextcloud/client/network/ConnectivityServiceImplIT.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ package com.nextcloud.client.network
99

1010
import android.accounts.AccountManager
1111
import android.content.Context
12-
import android.net.ConnectivityManager
1312
import com.nextcloud.client.account.UserAccountManagerImpl
1413
import com.nextcloud.client.core.ClockImpl
1514
import com.nextcloud.client.network.ConnectivityServiceImpl.GetRequestBuilder
@@ -21,15 +20,14 @@ import org.junit.Test
2120
class ConnectivityServiceImplIT : AbstractOnServerIT() {
2221
@Test
2322
fun testInternetWalled() {
24-
val connectivityManager = targetContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
2523
val accountManager = targetContext.getSystemService(Context.ACCOUNT_SERVICE) as AccountManager
2624
val userAccountManager = UserAccountManagerImpl(targetContext, accountManager)
2725
val clientFactory = ClientFactoryImpl(targetContext)
2826
val requestBuilder = GetRequestBuilder()
2927
val walledCheckCache = WalledCheckCache(ClockImpl())
3028

3129
val sut = ConnectivityServiceImpl(
32-
connectivityManager,
30+
targetContext,
3331
userAccountManager,
3432
clientFactory,
3533
requestBuilder,

app/src/main/java/com/nextcloud/client/network/ConnectivityService.java

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
package com.nextcloud.client.network;
88

99

10+
import android.net.ConnectivityManager;
11+
import android.net.Network;
12+
13+
import com.nextcloud.client.account.Server;
14+
import com.nextcloud.client.account.UserAccountManager;
15+
1016
import androidx.annotation.NonNull;
1117

1218
/**
@@ -15,31 +21,81 @@
1521
*/
1622
public interface ConnectivityService {
1723
/**
18-
* Checks the availability of the server and the device's internet connection.
19-
* <p>
20-
* This method performs a network request to verify if the server is accessible and
21-
* checks if the device has an active internet connection.
22-
* </p>
24+
* Asynchronously checks whether both the device's network connection
25+
* and the Nextcloud server are available.
26+
*
27+
* <p>This method executes its logic on a background thread and posts the result
28+
* back to the main thread through the provided {@link GenericCallback}.</p>
29+
*
30+
* <p>The check is based on {@link #isInternetWalled()} — if the Internet is not
31+
* walled (i.e., the server is reachable and not restricted by a captive portal),
32+
* this method reports {@code true}. Otherwise, it reports {@code false}.</p>
2333
*
24-
* @param callback A callback to handle the result of the network and server availability check.
34+
* @param callback a callback that receives {@code true} when the network and
35+
* Nextcloud server are reachable, or {@code false} otherwise.
2536
*/
2637
void isNetworkAndServerAvailable(@NonNull GenericCallback<Boolean> callback);
2738

39+
/**
40+
* Checks whether the device currently has an active, validated Internet connection
41+
* via a recognized transport type.
42+
*
43+
* <p>This method queries the Android {@link ConnectivityManager} to determine
44+
* whether there is an active {@link Network} with Internet capability and an
45+
* acceptable transport such as Wi-Fi, Cellular, Ethernet, VPN, or Bluetooth.</p>
46+
*
47+
* <p>For Android 12 (API 31) and newer, USB network transport is also considered valid.</p>
48+
*
49+
* <p>Note: This only confirms that the Android system has validated Internet access,
50+
* not necessarily that the Nextcloud server itself is reachable.</p>
51+
*
52+
* @return {@code true} if the device is connected to the Internet through a supported
53+
* transport type; {@code false} otherwise.
54+
*/
2855
boolean isConnected();
2956

3057
/**
31-
* Check if server is accessible by issuing HTTP status check request.
32-
* Since this call involves network traffic, it should not be called
33-
* on a main thread.
58+
* Determines whether the device's current Internet connection is "walled" — that is,
59+
* restricted by a captive portal or other form of network access control that prevents
60+
* full connectivity to the Nextcloud server.
61+
*
62+
* <p>This method does <strong>not</strong> test general Internet reachability (e.g. Google or DNS),
63+
* but rather focuses on the ability to access the configured Nextcloud server directly.
64+
* In other words, it checks whether the server can be reached without network interference
65+
* such as a hotel's captive portal, Wi-Fi login page, or similar restrictions.</p>
66+
*
67+
* <p>The implementation performs the following steps:</p>
68+
* <ul>
69+
* <li>Uses cached results from {@link WalledCheckCache} when available to avoid
70+
* redundant network calls.</li>
71+
* <li>Retrieves the active {@link Server} from {@link UserAccountManager}.</li>
72+
* <li>If connected issues a lightweight
73+
* HTTP {@code GET} request to the server’s <code>/index.php/204</code> endpoint
74+
* (which should respond with HTTP 204 No Content when connectivity is healthy).</li>
75+
* <li>If the response differs from the expected 204 No Content, the connection is
76+
* assumed to be behind a captive portal or otherwise restricted.</li>
77+
* <li>If no active network or server is detected, the method assumes the Internet
78+
* is walled.</li>
79+
* </ul>
3480
*
35-
* @return True if server is unreachable, false otherwise
81+
* <p>Results are cached for subsequent checks to minimize unnecessary HTTP requests.</p>
82+
*
83+
* @return {@code true} if the Internet appears to be walled (e.g. captive portal or
84+
* restricted access); {@code false} if the Nextcloud server is reachable and
85+
* the network allows normal Internet access.
3686
*/
3787
boolean isInternetWalled();
3888

3989
/**
40-
* Get current network connectivity status.
90+
* Returns a {@link Connectivity} object that represents the current network state.
91+
*
92+
* <p>This includes whether the device is connected, whether the network is metered,
93+
* and whether it uses Wi-Fi or Ethernet transport. It uses
94+
* {@link #isConnected()} to verify active Internet capability</p>
95+
*
96+
* <p>If no active network is found, {@link Connectivity#DISCONNECTED} is returned.</p>
4197
*
42-
* @return Network connectivity status in platform-agnostic format
98+
* @return a {@link Connectivity} instance describing the current network connection.
4399
*/
44100
Connectivity getConnectivity();
45101

app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java

Lines changed: 90 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@
66
*
77
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
88
*/
9-
109
package com.nextcloud.client.network;
1110

11+
import android.content.Context;
1212
import android.net.ConnectivityManager;
1313
import android.net.Network;
1414
import android.net.NetworkCapabilities;
1515
import android.os.Build;
1616
import android.os.Handler;
1717
import android.os.Looper;
18+
import android.text.TextUtils;
1819

1920
import com.nextcloud.client.account.Server;
2021
import com.nextcloud.client.account.UserAccountManager;
@@ -28,154 +29,150 @@
2829
import java.util.concurrent.Executors;
2930

3031
import androidx.annotation.NonNull;
31-
import kotlin.jvm.functions.Function1;
3232

33-
class ConnectivityServiceImpl implements ConnectivityService {
33+
public class ConnectivityServiceImpl implements ConnectivityService {
3434

3535
private static final String TAG = "ConnectivityServiceImpl";
3636
private static final String CONNECTIVITY_CHECK_ROUTE = "/index.php/204";
3737

38-
private final ConnectivityManager platformConnectivityManager;
38+
private final ConnectivityManager connectivityManager;
3939
private final UserAccountManager accountManager;
4040
private final ClientFactory clientFactory;
4141
private final GetRequestBuilder requestBuilder;
4242
private final WalledCheckCache walledCheckCache;
43-
private final ExecutorService executor = Executors.newSingleThreadExecutor();
4443
private final Handler mainThreadHandler = new Handler(Looper.getMainLooper());
44+
private final ExecutorService executor = Executors.newSingleThreadExecutor();
45+
private Connectivity currentConnectivity = Connectivity.DISCONNECTED;
46+
47+
private final ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() {
48+
@Override
49+
public void onAvailable(@NonNull Network network) {
50+
updateConnectivity();
51+
}
4552

46-
static class GetRequestBuilder implements Function1<String, GetMethod> {
53+
@Override
54+
public void onLost(@NonNull Network network) {
55+
updateConnectivity();
56+
}
57+
58+
@Override
59+
public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) {
60+
updateConnectivity();
61+
}
62+
};
63+
64+
static class GetRequestBuilder implements kotlin.jvm.functions.Function1<String, GetMethod> {
4765
@Override
4866
public GetMethod invoke(String url) {
4967
return new GetMethod(url, false);
5068
}
5169
}
5270

53-
ConnectivityServiceImpl(ConnectivityManager platformConnectivityManager,
54-
UserAccountManager accountManager,
55-
ClientFactory clientFactory,
56-
GetRequestBuilder requestBuilder,
57-
final WalledCheckCache walledCheckCache) {
58-
this.platformConnectivityManager = platformConnectivityManager;
71+
public ConnectivityServiceImpl(@NonNull Context context,
72+
@NonNull UserAccountManager accountManager,
73+
@NonNull ClientFactory clientFactory,
74+
@NonNull GetRequestBuilder requestBuilder,
75+
@NonNull WalledCheckCache walledCheckCache) {
76+
this.connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
5977
this.accountManager = accountManager;
6078
this.clientFactory = clientFactory;
6179
this.requestBuilder = requestBuilder;
6280
this.walledCheckCache = walledCheckCache;
63-
}
6481

65-
@Override
66-
public void isNetworkAndServerAvailable(@NonNull GenericCallback<Boolean> callback) {
67-
executor.submit(() -> {
68-
boolean isAvailable = !isInternetWalled();
69-
mainThreadHandler.post(() -> callback.onComplete(isAvailable));
70-
});
82+
// Register callback for real-time network updates
83+
connectivityManager.registerDefaultNetworkCallback(networkCallback);
84+
updateConnectivity();
7185
}
7286

73-
/**
74-
* Checks whether the device is currently connected to a network
75-
* that has verified Internet access.
76-
*
77-
* <p>This method performs multiple levels of validation:
78-
* <ul>
79-
* <li>Ensures there is an active network connection.</li>
80-
* <li>Retrieves and checks network capabilities.</li>
81-
* <li>Verifies that the active network provides and has validated Internet access.</li>
82-
* <li>Confirms that the network uses a supported transport type
83-
* (Wi-Fi, Cellular, Ethernet, VPN, etc.).</li>
84-
* </ul>
85-
*
86-
* @return {@code true} if the device is connected to the Internet via a valid transport type;
87-
* {@code false} otherwise.
88-
*/
89-
@Override
90-
public boolean isConnected() {
91-
Network nw = platformConnectivityManager.getActiveNetwork();
92-
if (nw == null) {
93-
return false;
87+
private void updateConnectivity() {
88+
Network activeNetwork = connectivityManager.getActiveNetwork();
89+
if (activeNetwork == null) {
90+
currentConnectivity = Connectivity.DISCONNECTED;
91+
return;
9492
}
9593

96-
NetworkCapabilities actNw = platformConnectivityManager.getNetworkCapabilities(nw);
97-
if (actNw == null) {
98-
return false;
94+
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(activeNetwork);
95+
if (capabilities == null) {
96+
currentConnectivity = Connectivity.DISCONNECTED;
97+
return;
9998
}
10099

101-
// Verify that the network both claims to provide Internet
102-
// and has been validated (i.e., Internet is actually reachable).
103-
if (actNw.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && actNw.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
100+
boolean isConnected = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) || isSupportedTransport(capabilities);
101+
boolean isMetered = !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
102+
boolean isWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
103+
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET);
104104

105-
// Check if the active network uses one of the recognized transport types.
106-
if (actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
107-
actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
108-
actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ||
109-
actNw.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
110-
actNw.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) ||
111-
actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) {
105+
currentConnectivity = new Connectivity(isConnected, isMetered, isWifi, null);
106+
}
112107

113-
// Connected through a valid, verified network transport.
114-
return true;
115-
}
108+
private boolean isSupportedTransport(@NonNull NetworkCapabilities capabilities) {
109+
return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
110+
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
111+
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ||
112+
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
113+
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) ||
114+
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE) ||
115+
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
116+
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_USB));
117+
}
116118

117-
// If still nothing matched check Android 12 (API 31, "S") and above via USB network transport.
118-
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
119-
actNw.hasTransport(NetworkCapabilities.TRANSPORT_USB);
120-
}
119+
@Override
120+
public void isNetworkAndServerAvailable(@NonNull GenericCallback<Boolean> callback) {
121+
executor.execute(() -> {
122+
boolean available = !isInternetWalled();
123+
mainThreadHandler.post(() -> callback.onComplete(available));
124+
});
125+
}
121126

122-
return false;
127+
@Override
128+
public boolean isConnected() {
129+
return currentConnectivity.isConnected();
123130
}
124131

125132
@Override
126133
public boolean isInternetWalled() {
127-
final Boolean cachedValue = walledCheckCache.getValue();
128-
if (cachedValue != null) {
129-
return cachedValue;
134+
Boolean cached = walledCheckCache.getValue();
135+
if (cached != null) {
136+
return cached;
130137
}
131138

132-
final Server server = accountManager.getUser().getServer();
133-
final String baseServerAddress = server.getUri().toString();
139+
Server server = accountManager.getUser().getServer();
140+
String baseServerAddress = server.getUri().toString();
134141

135-
if (!isConnected() || baseServerAddress.isEmpty()) {
142+
if (!currentConnectivity.isConnected() || TextUtils.isEmpty(baseServerAddress)) {
136143
walledCheckCache.setValue(true);
137144
return true;
138145
}
139146

140-
final GetMethod get = requestBuilder.invoke(baseServerAddress + CONNECTIVITY_CHECK_ROUTE);
147+
boolean isWalled;
148+
GetMethod get = requestBuilder.invoke(baseServerAddress + CONNECTIVITY_CHECK_ROUTE);
149+
PlainClient client = clientFactory.createPlainClient();
150+
141151
try {
142-
final PlainClient client = clientFactory.createPlainClient();
143152
int status = get.execute(client);
144153

145-
boolean isWalled = !(status == HttpStatus.SC_NO_CONTENT && get.getResponseContentLength() <= 0);
146-
154+
// Server is reachable and responds correctly = NOT walled
155+
isWalled = !(status == HttpStatus.SC_NO_CONTENT && get.getResponseContentLength() <= 0);
147156
if (isWalled) {
148-
Log_OC.w(TAG, "isInternetWalled(): Failed to GET " + CONNECTIVITY_CHECK_ROUTE +
149-
", assuming connectivity is impaired");
157+
Log_OC.w(TAG, "isInternetWalled(): Server returned unexpected response");
150158
}
151-
152-
// Cache and return result
153-
walledCheckCache.setValue(isWalled);
154-
return isWalled;
155159
} catch (Exception e) {
156-
Log_OC.e(TAG, "Exception while checking internet walled state", e);
157-
walledCheckCache.setValue(true);
158-
return true;
160+
Log_OC.e(TAG, "isInternetWalled(): Exception during server check", e);
161+
isWalled = true;
159162
} finally {
160163
get.releaseConnection();
161164
}
165+
166+
walledCheckCache.setValue(isWalled);
167+
return isWalled;
162168
}
163169

164170
@Override
165171
public Connectivity getConnectivity() {
166-
Network nw = platformConnectivityManager.getActiveNetwork();
167-
if (nw == null) {
168-
return Connectivity.DISCONNECTED;
169-
}
170-
171-
NetworkCapabilities nc = platformConnectivityManager.getNetworkCapabilities(nw);
172-
boolean isConnected = isConnected();
173-
boolean isMetered = (nc != null) && !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
174-
boolean isWifi = (nc != null) &&
175-
(nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
176-
nc.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ||
177-
nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE));
172+
return currentConnectivity;
173+
}
178174

179-
return new Connectivity(isConnected, isMetered, isWifi, null);
175+
public void unregisterCallback() {
176+
connectivityManager.unregisterNetworkCallback(networkCallback);
180177
}
181178
}

0 commit comments

Comments
 (0)