Skip to content

Commit 162eb99

Browse files
committed
frontend/android: refactor network related code
Our MainActivity code is quite complicated and mixes a lot of different things. This change moves the code related to network management (e.g. to display alerts when the network is not available) to a dedicated helper class and declutters the main activity a bit. It also fixes a bug that caused the app to misdetect mobile connection or missing connectivity when switching between WIFI and cellular connection. The bug was caused by the fact that the "onLost" call is not guaranteed to be executed after the network is actually lost. This can bring both to false positive and false negatives results, depending on race conditions. This caused to occasionally not display the "connection lost" banner when needed, or to e.g. display both the "connection lost" and the "mobile connection" banner at the same time. Adding a 250ms delay in the execution of the "onLost" handler is not nice, but seems to reliably fix the issue on all the tested Android versions. This also drops the `netWorkStateReceiver`, as it was registered as a broadcast receiver for the `CONNECTIVITY_ACTION`, which is now deprecated. See https://developer.android.com/reference/android/net/ConnectivityManager#CONNECTIVITY_ACTION The `usingMobileDataChanged` call, which is used to force the mobile server to fetch again the network source (wifi/mobile) and optionally display the "mobile connection" banner, is moved inside the `checkNetworkConnectivity` call, so that it's executed every time we are reassessing the connectivity status.
1 parent 494f03c commit 162eb99

File tree

4 files changed

+133
-96
lines changed

4 files changed

+133
-96
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
## Unreleased
44
- Add feedback link to guide and about settings
55
- Move active currencies to top of currency dropdown
6+
- Android: fix connectivity misdetection when switching between WIFI and cellular network.
7+
- Android: dropped support for Android versions lower than 7.
68

79
## v4.49.0
810
- Bundle BitBox02 Nova firmware version v9.24.0

frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/GoViewModel.java

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import android.hardware.usb.UsbInterface;
1010
import android.hardware.usb.UsbManager;
1111
import android.net.ConnectivityManager;
12-
import android.net.NetworkCapabilities;
1312
import android.os.Handler;
1413

1514
import androidx.lifecycle.AndroidViewModel;
@@ -170,14 +169,7 @@ public void bluetoothConnect(String identifier) {
170169

171170
@Override
172171
public boolean usingMobileData() {
173-
// Adapted from https://stackoverflow.com/a/53243938
174-
ConnectivityManager cm = (ConnectivityManager) getApplication().getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
175-
if (cm == null) {
176-
return false;
177-
}
178-
NetworkCapabilities capabilities = cm.getNetworkCapabilities(cm.getActiveNetwork());
179-
return capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR);
180-
172+
return networkHelper != null && networkHelper.usingMobileData();
181173
}
182174

183175
@Override
@@ -207,11 +199,18 @@ public boolean detectDarkTheme() {
207199
private final MutableLiveData<Boolean> authSetting = new MutableLiveData<>(false);
208200
private final GoEnvironment goEnvironment;
209201
private final GoAPI goAPI;
202+
private NetworkHelper networkHelper;
203+
204+
public NetworkHelper getNetworkHelper() {
205+
return networkHelper;
206+
}
210207

211208
public GoViewModel(Application app) {
212209
super(app);
213210
this.goEnvironment = new GoEnvironment();
214211
this.goAPI = new GoAPI();
212+
this.networkHelper = new NetworkHelper((ConnectivityManager) app.getSystemService(Context.CONNECTIVITY_SERVICE));
213+
215214
}
216215

217216
public MutableLiveData<Boolean> getIsDarkTheme() {

frontends/android/BitBoxApp/app/src/main/java/ch/shiftcrypto/bitboxapp/MainActivity.java

Lines changed: 7 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@
1414
import android.content.res.Configuration;
1515
import android.hardware.usb.UsbDevice;
1616
import android.hardware.usb.UsbManager;
17-
import android.net.ConnectivityManager;
18-
import android.net.Network;
19-
import android.net.NetworkCapabilities;
20-
import android.net.NetworkRequest;
2117
import android.net.Uri;
2218
import android.os.Bundle;
2319
import android.os.Handler;
@@ -62,47 +58,6 @@ public class MainActivity extends AppCompatActivity {
6258

6359
private BitBoxWebChromeClient webChrome;
6460

65-
private ConnectivityManager connectivityManager;
66-
private ConnectivityManager.NetworkCallback networkCallback;
67-
68-
private boolean hasInternetConnectivity(NetworkCapabilities capabilities) {
69-
// To avoid false positives, if we can't obtain connectivity info,
70-
// we return true.
71-
// Note: this should never happen per Android documentation, as:
72-
// - these can not be null it come from the onCapabilitiesChanged callback.
73-
// - when obtained with getNetworkCapabilities(network), they can only be null if the
74-
// network is null or unknown, but we guard against both in the caller.
75-
if (capabilities == null) {
76-
Util.log("Got null capabilities when we shouldn't have. Assuming we are online.");
77-
return true;
78-
}
79-
80-
81-
boolean hasInternet = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
82-
83-
// We need to check for both internet and validated, since validated reports that the system
84-
// found connectivity the last time it checked. But if this callback triggers when going offline
85-
// (e.g. airplane mode), this bit would still be true when we execute this method.
86-
boolean isValidated = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
87-
return hasInternet && isValidated;
88-
89-
// Fallback for older devices
90-
}
91-
92-
private void checkConnectivity() {
93-
Network activeNetwork = connectivityManager.getActiveNetwork();
94-
95-
// If there is no active network (e.g. airplane mode), there is no check to perform.
96-
if (activeNetwork == null) {
97-
Mobileserver.setOnline(false);
98-
return;
99-
}
100-
101-
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(activeNetwork);
102-
103-
Mobileserver.setOnline(hasInternetConnectivity(capabilities));
104-
}
105-
10661
// Connection to bind with GoService
10762
private final ServiceConnection connection = new ServiceConnection() {
10863

@@ -129,14 +84,6 @@ public void onReceive(Context context, Intent intent) {
12984
}
13085
};
13186

132-
private final BroadcastReceiver networkStateReceiver = new BroadcastReceiver() {
133-
@Override
134-
public void onReceive(Context context, Intent intent) {
135-
Mobileserver.usingMobileDataChanged();
136-
}
137-
};
138-
139-
14087
@Override
14188
public void onConfigurationChanged(Configuration newConfig) {
14289
int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
@@ -248,21 +195,6 @@ protected void onCreate(Bundle savedInstanceState) {
248195
// In that case, handleIntent() is not called with ACTION_USB_DEVICE_ATTACHED.
249196
this.updateDevice();
250197

251-
connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
252-
networkCallback = new ConnectivityManager.NetworkCallback() {
253-
@Override
254-
public void onCapabilitiesChanged(@NonNull android.net.Network network, @NonNull android.net.NetworkCapabilities capabilities) {
255-
super.onCapabilitiesChanged(network, capabilities);
256-
Mobileserver.setOnline(hasInternetConnectivity(capabilities));
257-
}
258-
// When we lose the network, onCapabilitiesChanged does not trigger, so we need to override onLost.
259-
@Override
260-
public void onLost(@NonNull Network network) {
261-
super.onLost(network);
262-
Mobileserver.setOnline(false);
263-
}
264-
};
265-
266198
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
267199
@Override
268200
public void handleOnBackPressed() {
@@ -336,8 +268,8 @@ private void startServer() {
336268
final GoViewModel gVM = ViewModelProviders.of(this).get(GoViewModel.class);
337269
goService.startServer(getApplicationContext().getFilesDir().getAbsolutePath(), gVM.getGoEnvironment(), gVM.getGoAPI());
338270

339-
// Trigger connectivity check (as the network may already be unavailable when the app starts).
340-
checkConnectivity();
271+
// Trigger connectivity and mobile connection check (as the network may already be unavailable when the app starts).
272+
gVM.getNetworkHelper().checkConnectivity();
341273
}
342274

343275
@Override
@@ -355,15 +287,7 @@ protected void onStart() {
355287
Util.log("lifecycle: onStart");
356288
final GoViewModel goViewModel = ViewModelProviders.of(this).get(GoViewModel.class);
357289
goViewModel.getIsDarkTheme().observe(this, this::setDarkTheme);
358-
359-
360-
NetworkRequest request = new NetworkRequest.Builder()
361-
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
362-
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
363-
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
364-
.build();
365-
// Register the network callback to listen for changes in network capabilities.
366-
connectivityManager.registerNetworkCallback(request, networkCallback);
290+
goViewModel.getNetworkHelper().registerNetworkCallback();
367291
}
368292

369293
@Override
@@ -389,11 +313,9 @@ protected void onResume() {
389313
ContextCompat.RECEIVER_NOT_EXPORTED
390314
);
391315

392-
// Listen on changes in the network connection. We are interested in if the user is connected to a mobile data connection.
393-
registerReceiver(this.networkStateReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
394-
395316
// Trigger connectivity check (as the network may already be unavailable when the app starts).
396-
checkConnectivity();
317+
final GoViewModel goViewModel = ViewModelProviders.of(this).get(GoViewModel.class);
318+
goViewModel.getNetworkHelper().checkConnectivity();
397319

398320
Intent intent = getIntent();
399321
handleIntent(intent);
@@ -404,7 +326,6 @@ protected void onPause() {
404326
super.onPause();
405327
Util.log("lifecycle: onPause");
406328
unregisterReceiver(this.usbStateReceiver);
407-
unregisterReceiver(this.networkStateReceiver);
408329
}
409330

410331
private void handleIntent(Intent intent) {
@@ -472,9 +393,8 @@ private void updateDevice() {
472393
@Override
473394
protected void onStop() {
474395
super.onStop();
475-
if (connectivityManager != null && networkCallback != null) {
476-
connectivityManager.unregisterNetworkCallback(networkCallback);
477-
}
396+
final GoViewModel goViewModel = ViewModelProviders.of(this).get(GoViewModel.class);
397+
goViewModel.getNetworkHelper().unregisterNetworkCallback();
478398
Util.log("lifecycle: onStop");
479399
}
480400

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package ch.shiftcrypto.bitboxapp;
2+
3+
import android.net.ConnectivityManager;
4+
import android.net.Network;
5+
import android.net.NetworkCapabilities;
6+
import android.os.Handler;
7+
import android.os.Looper;
8+
9+
import androidx.annotation.NonNull;
10+
11+
import mobileserver.Mobileserver;
12+
13+
public class NetworkHelper {
14+
private final ConnectivityManager connectivityManager;
15+
private final ConnectivityManager.NetworkCallback networkCallback;
16+
17+
public NetworkHelper(ConnectivityManager connectivityManager) {
18+
this.connectivityManager = connectivityManager;
19+
networkCallback = new ConnectivityManager.NetworkCallback() {
20+
@Override
21+
public void onCapabilitiesChanged(@NonNull android.net.Network network, @NonNull android.net.NetworkCapabilities capabilities) {
22+
Util.log("onCapabilitiesChanged");
23+
super.onCapabilitiesChanged(network, capabilities);
24+
checkNetworkConnectivity(network);
25+
}
26+
27+
@Override
28+
public void onLost(@NonNull Network network) {
29+
Util.log("onLost");
30+
super.onLost(network);
31+
// Workaround: onLost could trigger while the network is still winding down.
32+
// Checking immediately for connection and mobile usage could lead to wrong results.
33+
// see https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback#onLost(android.net.Network)
34+
// Since onCapabilitiesChanged doesn't seem to be enough, this is the only solution I
35+
// found to make this reliable. It's not beautiful, but seems to fix the issue.
36+
new Handler(Looper.getMainLooper()).postDelayed(() -> {
37+
checkConnectivity();
38+
Mobileserver.usingMobileDataChanged();
39+
}, 250);
40+
}
41+
};
42+
}
43+
44+
public void registerNetworkCallback() {
45+
if (connectivityManager == null) {
46+
return;
47+
}
48+
49+
// Register the network callback to listen for changes in network capabilities.
50+
// It needs to be unregistered when the app is in background to avoid resource consumption.
51+
// See https://developer.android.com/reference/android/net/ConnectivityManager#registerNetworkCallback(android.net.NetworkRequest,%20android.net.ConnectivityManager.NetworkCallback)
52+
connectivityManager.registerDefaultNetworkCallback(networkCallback);
53+
}
54+
55+
public void unregisterNetworkCallback() {
56+
if (connectivityManager != null && networkCallback != null) {
57+
connectivityManager.unregisterNetworkCallback(networkCallback);
58+
}
59+
}
60+
61+
// Fetches the active network and verifies if that provides internet access.
62+
public void checkConnectivity() {
63+
if (connectivityManager == null) {
64+
Mobileserver.setOnline(true);
65+
return;
66+
}
67+
Network activeNetwork = connectivityManager.getActiveNetwork();
68+
checkNetworkConnectivity(activeNetwork);
69+
}
70+
71+
private void checkNetworkConnectivity(Network network) {
72+
// We force the server to fetch the mobile data status and possibly update the
73+
// related banner.
74+
Mobileserver.usingMobileDataChanged();
75+
76+
if (network == null) {
77+
Util.log("checkConnectivity: network null");
78+
Mobileserver.setOnline(false);
79+
return;
80+
}
81+
82+
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
83+
Mobileserver.setOnline(hasInternetConnectivity(capabilities));
84+
}
85+
86+
private boolean hasInternetConnectivity(NetworkCapabilities capabilities) {
87+
Util.log("hasInternetConnectivity");
88+
if (capabilities == null) {
89+
Util.log("hasInternetConnectivity: null capabilities");
90+
return false;
91+
}
92+
93+
// NET_CAPABILITY_INTERNET means that the network should be able to provide internet access,
94+
// NET_CAPABILITY_VALIDATED means that the network connectivity was successfully detected.
95+
// see https://developer.android.com/reference/android/net/NetworkCapabilities#NET_CAPABILITY_VALIDATED
96+
// Checking only VALIDATED would be probably enough, but checking for both
97+
// is probably better for reliability.
98+
boolean hasInternet = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
99+
boolean isValidated = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
100+
Util.log("Has internet connectivity: " + (hasInternet && isValidated));
101+
return hasInternet && isValidated;
102+
}
103+
104+
// if usingMobileData returns true, a banner will be displayed in the app to warn about
105+
// possible network data consumption.
106+
public boolean usingMobileData() {
107+
if (connectivityManager == null) {
108+
return false;
109+
}
110+
111+
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork());
112+
boolean mobileData = capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR);
113+
Util.log("Using mobile data: " + mobileData);
114+
return mobileData;
115+
}
116+
}

0 commit comments

Comments
 (0)