Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(connectivity_plus): Ensure Connectivity on Android is correctly reported when lost #2836

Merged
merged 12 commits into from
Apr 22, 2024
4 changes: 3 additions & 1 deletion packages/connectivity_plus/connectivity_plus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ if (connectivityResult.contains(ConnectivityResult.mobile)) {
```

You can also listen for active connectivity types changes by subscribing to the stream
exposed by the plugin:
exposed by the plugin.

This method should ensure emitting only distinct values.

```dart
import 'package:connectivity_plus/connectivity_plus.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import android.net.Network;
import android.net.NetworkCapabilities;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -27,44 +29,54 @@ public Connectivity(ConnectivityManager connectivityManager) {
}

List<String> getNetworkTypes() {
List<String> types = new ArrayList<>();
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Network network = connectivityManager.getActiveNetwork();
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
if (capabilities == null
|| !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
types.add(CONNECTIVITY_NONE);
return types;
}
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) {
types.add(CONNECTIVITY_WIFI);
}
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
types.add(CONNECTIVITY_ETHERNET);
}
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
types.add(CONNECTIVITY_VPN);
}
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
types.add(CONNECTIVITY_MOBILE);
}
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) {
types.add(CONNECTIVITY_BLUETOOTH);
}
if (types.isEmpty()
&& capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
types.add(CONNECTIVITY_OTHER);
}
if (types.isEmpty()) {
types.add(CONNECTIVITY_NONE);
}
return getCapabilitiesFromNetwork(network);
} else {
// For legacy versions, return a single type as before or adapt similarly if multiple types
// need to be supported
return getNetworkTypesLegacy();
}
}

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
List<String> getCapabilitiesFromNetwork(Network network) {
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
return getCapabilitiesList(capabilities);
}

@NonNull
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
List<String> getCapabilitiesList(NetworkCapabilities capabilities) {
List<String> types = new ArrayList<>();
if (capabilities == null
|| !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
types.add(CONNECTIVITY_NONE);
return types;
}
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) {
types.add(CONNECTIVITY_WIFI);
}
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
types.add(CONNECTIVITY_ETHERNET);
}
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
types.add(CONNECTIVITY_VPN);
}
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
types.add(CONNECTIVITY_MOBILE);
}
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) {
types.add(CONNECTIVITY_BLUETOOTH);
}
if (types.isEmpty()
&& capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
types.add(CONNECTIVITY_OTHER);
}
if (types.isEmpty()) {
types.add(CONNECTIVITY_NONE);
}
return types;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import android.os.Handler;
import android.os.Looper;
import io.flutter.plugin.common.EventChannel;
import java.util.List;

/**
* The ConnectivityBroadcastReceiver receives the connectivity updates and send them to the UIThread
Expand Down Expand Up @@ -46,18 +47,31 @@ public void onListen(Object arguments, EventChannel.EventSink events) {
new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
sendEvent();
// onAvailable is called when the phone switches to a new network
// e.g. the phone was offline and gets wifi connection
// or the phone was on wifi and now switches to mobile.
// The plugin sends the current capability connection to the users.
sendEvent(connectivity.getCapabilitiesFromNetwork(network));
}

@Override
public void onCapabilitiesChanged(
Network network, NetworkCapabilities networkCapabilities) {
sendEvent();
// This callback is called multiple times after a call to onAvailable
// this also causes multiple callbacks to the Flutter layer.
sendEvent(connectivity.getCapabilitiesList(networkCapabilities));
}

@Override
public void onLost(Network network) {
sendEvent();
// This callback is called when a capability is lost.
//
// The provided Network object contains information about the
// network capability that has been lost, so we cannot use it.
//
// Instead, post the current network but with a delay long enough
// that we avoid a race condition.
sendCurrentStatusWithDelay();
}
};
connectivity.getConnectivityManager().registerDefaultNetworkCallback(networkCallback);
Expand All @@ -66,7 +80,7 @@ public void onLost(Network network) {
}
// Need to emit first event with connectivity types without waiting for first change in system
// that might happen much later
sendEvent();
sendEvent(connectivity.getNetworkTypes());
}

@Override
Expand All @@ -92,11 +106,16 @@ public void onReceive(Context context, Intent intent) {
}
}

private void sendEvent() {
private void sendEvent(List<String> networkTypes) {
Runnable runnable = () -> events.success(networkTypes);
// Emit events on main thread
mainHandler.post(runnable);
}

private void sendCurrentStatusWithDelay() {
Runnable runnable = () -> events.success(connectivity.getNetworkTypes());
// The dalay is needed because callback methods suffer from race conditions.
// More info:
// https://developer.android.com/develop/connectivity/network-ops/reading-network-state#listening-events
mainHandler.postDelayed(runnable, 100);
// Emit events on main thread
// 500 milliseconds to avoid race conditions
mainHandler.postDelayed(runnable, 500);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ class _MyHomePageState extends State<MyHomePage> {
setState(() {
_connectionStatus = result;
});
// ignore: avoid_print
print('Connectivity changed: $_connectionStatus');
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'dart:async';

import 'package:connectivity_plus_platform_interface/connectivity_plus_platform_interface.dart';
import 'package:collection/collection.dart';

// Export enums from the platform_interface so plugin users can use them directly.
export 'package:connectivity_plus_platform_interface/connectivity_plus_platform_interface.dart'
Expand Down Expand Up @@ -40,16 +41,14 @@ class Connectivity {
/// status changes, this is a known issue that only affects simulators.
/// For details see https://github.com/fluttercommunity/plus_plugins/issues/479.
///
/// On Android, the Stream may emit new values even when
/// the [ConnectivityResult] list remains the same.
///
/// The emitted list is never empty. In case of no connectivity, the list contains
/// a single element of [ConnectivityResult.none]. Note also that this is the only
/// case where [ConnectivityResult.none] is present.
///
/// This method doesn't filter events, nor it ensures distinct values.
/// This method applies [Stream.distinct] over the received events to ensure
/// only emiting when connectivity changes.
Stream<List<ConnectivityResult>> get onConnectivityChanged {
return _platform.onConnectivityChanged;
return _platform.onConnectivityChanged.distinct((a, b) => a.equals(b));
}

/// Checks the connection status of the device.
Expand Down
1 change: 1 addition & 0 deletions packages/connectivity_plus/connectivity_plus/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dependencies:
web: '>=0.3.0 <=0.6.0'
meta: ^1.8.0
nm: ^0.5.0
collection: ^1.18.0

dev_dependencies:
flutter_test:
Expand Down
Loading