Skip to content

Commit 0c2c303

Browse files
committed
Merge branch 'android-helpers'
2 parents 0c09c42 + 162eb99 commit 0c2c303

File tree

6 files changed

+135
-98
lines changed

6 files changed

+135
-98
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
- Add a dropdown on the "Receiver address" input in the send screen to select an account
55
- Add feedback link to guide and about settings
66
- Move active currencies to top of currency dropdown
7+
- Android: fix connectivity misdetection when switching between WIFI and cellular network.
8+
- Android: dropped support for Android versions lower than 7.
79

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

backend/mobileserver/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ include ../../version.mk.inc
44
# Set -glflags to fix the vendor issue with gomobile, see: https://github.com/golang/go/issues/67927#issuecomment-2241523694
55
build-android:
66
# androidapi version should match minSdkVersion in frontends/android/BitBoxApp/app/build.gradle
7-
ANDROID_HOME=${ANDROID_SDK_ROOT} gomobile bind -x -a -glflags="-mod=readonly" -ldflags="-s -w $(GO_VERSION_LDFLAGS)" -trimpath -target android -androidapi 21 .
7+
ANDROID_HOME=${ANDROID_SDK_ROOT} gomobile bind -x -a -glflags="-mod=readonly" -ldflags="-s -w $(GO_VERSION_LDFLAGS)" -trimpath -target android -androidapi 24 .
88
build-ios:
99
gomobile bind -x -a -glflags="-mod=readonly" -tags="timetzdata" -trimpath -ldflags="-s -w $(GO_VERSION_LDFLAGS)" -target ios,iossimulator .
1010
clean:

frontends/android/BitBoxApp/app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ android {
1313
ndkVersion "28.2.13676358"
1414
defaultConfig {
1515
applicationId "ch.shiftcrypto.bitboxapp"
16+
minSdk 24
1617
// minSdkVersion should match the `-androidapi` gomobile bind flag
1718
// in backend/mobileserver/Makefile
18-
minSdkVersion 21
1919
targetSdkVersion 35
2020
versionCode 66
2121
versionName "${appVersion}"

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)