Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## Unreleased
- Add feedback link to guide and about settings
- Move active currencies to top of currency dropdown
- Android: fix connectivity misdetection when switching between WIFI and cellular network.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention dropped support for Android <6 too?

- Android: dropped support for Android versions lower than 7.

## v4.49.0
- Bundle BitBox02 Nova firmware version v9.24.0
Expand Down
2 changes: 1 addition & 1 deletion backend/mobileserver/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ include ../../version.mk.inc
# Set -glflags to fix the vendor issue with gomobile, see: https://github.com/golang/go/issues/67927#issuecomment-2241523694
build-android:
# androidapi version should match minSdkVersion in frontends/android/BitBoxApp/app/build.gradle
ANDROID_HOME=${ANDROID_SDK_ROOT} gomobile bind -x -a -glflags="-mod=readonly" -ldflags="-s -w $(GO_VERSION_LDFLAGS)" -trimpath -target android -androidapi 21 .
ANDROID_HOME=${ANDROID_SDK_ROOT} gomobile bind -x -a -glflags="-mod=readonly" -ldflags="-s -w $(GO_VERSION_LDFLAGS)" -trimpath -target android -androidapi 24 .
build-ios:
gomobile bind -x -a -glflags="-mod=readonly" -tags="timetzdata" -trimpath -ldflags="-s -w $(GO_VERSION_LDFLAGS)" -target ios,iossimulator .
clean:
Expand Down
2 changes: 1 addition & 1 deletion frontends/android/BitBoxApp/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ android {
ndkVersion "28.2.13676358"
defaultConfig {
applicationId "ch.shiftcrypto.bitboxapp"
minSdk 24
// minSdkVersion should match the `-androidapi` gomobile bind flag
// in backend/mobileserver/Makefile
minSdkVersion 21
targetSdkVersion 35
versionCode 66
versionName "${appVersion}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbManager;
import android.net.ConnectivityManager;
import android.net.NetworkCapabilities;
import android.os.Handler;

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

@Override
public boolean usingMobileData() {
// Adapted from https://stackoverflow.com/a/53243938
ConnectivityManager cm = (ConnectivityManager) getApplication().getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null) {
return false;
}
NetworkCapabilities capabilities = cm.getNetworkCapabilities(cm.getActiveNetwork());
return capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR);

return networkHelper != null && networkHelper.usingMobileData();
}

@Override
Expand Down Expand Up @@ -207,11 +199,18 @@ public boolean detectDarkTheme() {
private final MutableLiveData<Boolean> authSetting = new MutableLiveData<>(false);
private final GoEnvironment goEnvironment;
private final GoAPI goAPI;
private NetworkHelper networkHelper;

public NetworkHelper getNetworkHelper() {
return networkHelper;
}

public GoViewModel(Application app) {
super(app);
this.goEnvironment = new GoEnvironment();
this.goAPI = new GoAPI();
this.networkHelper = new NetworkHelper((ConnectivityManager) app.getSystemService(Context.CONNECTIVITY_SERVICE));

}

public MutableLiveData<Boolean> getIsDarkTheme() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@
import android.content.res.Configuration;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
Expand Down Expand Up @@ -62,47 +58,6 @@ public class MainActivity extends AppCompatActivity {

private BitBoxWebChromeClient webChrome;

private ConnectivityManager connectivityManager;
private ConnectivityManager.NetworkCallback networkCallback;

private boolean hasInternetConnectivity(NetworkCapabilities capabilities) {
// To avoid false positives, if we can't obtain connectivity info,
// we return true.
// Note: this should never happen per Android documentation, as:
// - these can not be null it come from the onCapabilitiesChanged callback.
// - when obtained with getNetworkCapabilities(network), they can only be null if the
// network is null or unknown, but we guard against both in the caller.
Comment on lines -69 to -74
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment and the comment below are very useful, any reason you did not copy them to the new file like the test of this function?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this applies anymore in the proposed implementation: checkConnectivity() gets the active network and verifies its capabilities. We don't have guaranties that that network capabilities are not null, and if they are null we should consider it a signal of missing connectivity, not a false positive.

I tested this implementation on various Android versions and I never experienced false positives (or negatives) with this approach.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the comment below, about the VALIDATED capability: I don't think that the comment matches closely the API docs. I wanted to replace it but eventually I forgot about it, I'll make a new one trying to clarify 🙏

if (capabilities == null) {
Util.log("Got null capabilities when we shouldn't have. Assuming we are online.");
return true;
}


boolean hasInternet = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);

// We need to check for both internet and validated, since validated reports that the system
// found connectivity the last time it checked. But if this callback triggers when going offline
// (e.g. airplane mode), this bit would still be true when we execute this method.
boolean isValidated = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
return hasInternet && isValidated;

// Fallback for older devices
}

private void checkConnectivity() {
Network activeNetwork = connectivityManager.getActiveNetwork();

// If there is no active network (e.g. airplane mode), there is no check to perform.
if (activeNetwork == null) {
Mobileserver.setOnline(false);
return;
}

NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(activeNetwork);

Mobileserver.setOnline(hasInternetConnectivity(capabilities));
}

// Connection to bind with GoService
private final ServiceConnection connection = new ServiceConnection() {

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

private final BroadcastReceiver networkStateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Mobileserver.usingMobileDataChanged();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is deleted with no replacement, so it seems the mobile data warning banner will never be shown?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in my tests it was displayed anyway. But in the meantime I have heavily refactored the code anyway.

}
};


@Override
public void onConfigurationChanged(Configuration newConfig) {
int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
Expand Down Expand Up @@ -248,21 +195,6 @@ protected void onCreate(Bundle savedInstanceState) {
// In that case, handleIntent() is not called with ACTION_USB_DEVICE_ATTACHED.
this.updateDevice();

connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
networkCallback = new ConnectivityManager.NetworkCallback() {
@Override
public void onCapabilitiesChanged(@NonNull android.net.Network network, @NonNull android.net.NetworkCapabilities capabilities) {
super.onCapabilitiesChanged(network, capabilities);
Mobileserver.setOnline(hasInternetConnectivity(capabilities));
}
// When we lose the network, onCapabilitiesChanged does not trigger, so we need to override onLost.
@Override
public void onLost(@NonNull Network network) {
super.onLost(network);
Mobileserver.setOnline(false);
}
};

getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
Expand Down Expand Up @@ -336,8 +268,8 @@ private void startServer() {
final GoViewModel gVM = ViewModelProviders.of(this).get(GoViewModel.class);
goService.startServer(getApplicationContext().getFilesDir().getAbsolutePath(), gVM.getGoEnvironment(), gVM.getGoAPI());

// Trigger connectivity check (as the network may already be unavailable when the app starts).
checkConnectivity();
// Trigger connectivity and mobile connection check (as the network may already be unavailable when the app starts).
gVM.getNetworkHelper().checkConnectivity();
}

@Override
Expand All @@ -355,15 +287,7 @@ protected void onStart() {
Util.log("lifecycle: onStart");
final GoViewModel goViewModel = ViewModelProviders.of(this).get(GoViewModel.class);
goViewModel.getIsDarkTheme().observe(this, this::setDarkTheme);


NetworkRequest request = new NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build();
// Register the network callback to listen for changes in network capabilities.
connectivityManager.registerNetworkCallback(request, networkCallback);
goViewModel.getNetworkHelper().registerNetworkCallback();
}

@Override
Expand All @@ -389,11 +313,9 @@ protected void onResume() {
ContextCompat.RECEIVER_NOT_EXPORTED
);

// Listen on changes in the network connection. We are interested in if the user is connected to a mobile data connection.
registerReceiver(this.networkStateReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));

// Trigger connectivity check (as the network may already be unavailable when the app starts).
checkConnectivity();
final GoViewModel goViewModel = ViewModelProviders.of(this).get(GoViewModel.class);
goViewModel.getNetworkHelper().checkConnectivity();

Intent intent = getIntent();
handleIntent(intent);
Expand All @@ -404,7 +326,6 @@ protected void onPause() {
super.onPause();
Util.log("lifecycle: onPause");
unregisterReceiver(this.usbStateReceiver);
unregisterReceiver(this.networkStateReceiver);
}

private void handleIntent(Intent intent) {
Expand Down Expand Up @@ -472,9 +393,8 @@ private void updateDevice() {
@Override
protected void onStop() {
super.onStop();
if (connectivityManager != null && networkCallback != null) {
connectivityManager.unregisterNetworkCallback(networkCallback);
}
final GoViewModel goViewModel = ViewModelProviders.of(this).get(GoViewModel.class);
goViewModel.getNetworkHelper().unregisterNetworkCallback();
Util.log("lifecycle: onStop");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package ch.shiftcrypto.bitboxapp;

import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.os.Handler;
import android.os.Looper;

import androidx.annotation.NonNull;

import mobileserver.Mobileserver;

public class NetworkHelper {
private final ConnectivityManager connectivityManager;
private final ConnectivityManager.NetworkCallback networkCallback;

public NetworkHelper(ConnectivityManager connectivityManager) {
this.connectivityManager = connectivityManager;
networkCallback = new ConnectivityManager.NetworkCallback() {
@Override
public void onCapabilitiesChanged(@NonNull android.net.Network network, @NonNull android.net.NetworkCapabilities capabilities) {
Util.log("onCapabilitiesChanged");
super.onCapabilitiesChanged(network, capabilities);
checkNetworkConnectivity(network);
}

@Override
public void onLost(@NonNull Network network) {
Util.log("onLost");
super.onLost(network);
// Workaround: onLost could trigger while the network is still winding down.
// Checking immediately for connection and mobile usage could lead to wrong results.
// see https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback#onLost(android.net.Network)
// Since onCapabilitiesChanged doesn't seem to be enough, this is the only solution I
// found to make this reliable. It's not beautiful, but seems to fix the issue.
new Handler(Looper.getMainLooper()).postDelayed(() -> {
checkConnectivity();
Mobileserver.usingMobileDataChanged();
}, 250);
}
};
}

public void registerNetworkCallback() {
if (connectivityManager == null) {
return;
}

// Register the network callback to listen for changes in network capabilities.
// It needs to be unregistered when the app is in background to avoid resource consumption.
// See https://developer.android.com/reference/android/net/ConnectivityManager#registerNetworkCallback(android.net.NetworkRequest,%20android.net.ConnectivityManager.NetworkCallback)
connectivityManager.registerDefaultNetworkCallback(networkCallback);
}

public void unregisterNetworkCallback() {
if (connectivityManager != null && networkCallback != null) {
connectivityManager.unregisterNetworkCallback(networkCallback);
}
}

// Fetches the active network and verifies if that provides internet access.
public void checkConnectivity() {
if (connectivityManager == null) {
Mobileserver.setOnline(true);
return;
}
Network activeNetwork = connectivityManager.getActiveNetwork();
checkNetworkConnectivity(activeNetwork);
}

private void checkNetworkConnectivity(Network network) {
// We force the server to fetch the mobile data status and possibly update the
// related banner.
Mobileserver.usingMobileDataChanged();

if (network == null) {
Util.log("checkConnectivity: network null");
Mobileserver.setOnline(false);
return;
}

NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
Mobileserver.setOnline(hasInternetConnectivity(capabilities));
}

private boolean hasInternetConnectivity(NetworkCapabilities capabilities) {
Util.log("hasInternetConnectivity");
if (capabilities == null) {
Util.log("hasInternetConnectivity: null capabilities");
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the original PR where @bznein introduced the connectivity check, we carefully returned true in all the failure cases, so that the user does not get false banners about being offline:

// To avoid false positives, if we can't obtain connectivity info,
// we return true.

It seems you have changed back all these defaults. Please change it back.

See also

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answered in my other comment about this 😇

}

// NET_CAPABILITY_INTERNET means that the network should be able to provide internet access,
// NET_CAPABILITY_VALIDATED means that the network connectivity was successfully detected.
// see https://developer.android.com/reference/android/net/NetworkCapabilities#NET_CAPABILITY_VALIDATED
// Checking only VALIDATED would be probably enough, but checking for both
// is probably better for reliability.
boolean hasInternet = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
boolean isValidated = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
Util.log("Has internet connectivity: " + (hasInternet && isValidated));
return hasInternet && isValidated;
}

// if usingMobileData returns true, a banner will be displayed in the app to warn about
// possible network data consumption.
public boolean usingMobileData() {
if (connectivityManager == null) {
return false;
}

NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork());
boolean mobileData = capabilities != null && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR);
Util.log("Using mobile data: " + mobileData);
return mobileData;
}
}