From 713b2c50e3c9a21143cd6c8cd888b148bd98db59 Mon Sep 17 00:00:00 2001 From: alexandre_poichet Date: Mon, 18 Nov 2024 16:23:06 +0100 Subject: [PATCH] add android connetivity feature --- .fvmrc | 2 +- .github/workflows/test.yaml | 6 +- .gitignore | 7 +- README.md | 39 ++ android/build.gradle | 5 + android/src/main/AndroidManifest.xml | 4 + .../main/kotlin/listener/BatteryListener.kt | 132 ++++++ .../kotlin/listener/ConnectivityListener.kt | 205 +++++++++ .../flutter_eco_mode/FlutterEcoModePlugin.kt | 144 ++----- .../tech/flutter_eco_mode/Messages.g.kt | 166 ++++--- .../FlutterEcoModePluginTest.kt | 23 - .../gradle/wrapper/gradle-wrapper.properties | 3 +- example/android/settings.gradle | 2 +- example/ios/Podfile.lock | 4 +- example/ios/Runner/AppDelegate.swift | 2 +- .../connectivity_state_page.dart | 34 ++ example/lib/extensions.dart | 12 + example/lib/main.dart | 13 + example/pubspec.lock | 38 +- example/pubspec.yaml | 2 + ios/Classes/FlutterEcoModePlugin.swift | 22 + ios/Classes/Messages.g.swift | 109 ++++- lib/flutter_eco_mode.dart | 73 ++++ lib/flutter_eco_mode_platform_interface.dart | 12 + lib/messages.g.dart | 94 +++- lib/streams/collection_extensions.dart | 17 - lib/streams/combine_latest.dart | 181 -------- lib/streams/future.dart | 8 - pigeons/messages.dart | 20 + test/flutter_eco_mode_test.dart | 405 +++++++++++++----- 30 files changed, 1233 insertions(+), 551 deletions(-) create mode 100644 android/src/main/kotlin/listener/BatteryListener.kt create mode 100644 android/src/main/kotlin/listener/ConnectivityListener.kt delete mode 100644 android/src/test/kotlin/sncf/connect/tech/flutter_eco_mode/FlutterEcoModePluginTest.kt create mode 100644 example/lib/connectivity_state/connectivity_state_page.dart diff --git a/.fvmrc b/.fvmrc index f79f9b4..679f8e1 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,4 +1,4 @@ { - "flutter": "3.19.3", + "flutter": "3.24.5", "flavors": {} } \ No newline at end of file diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 32dbb22..201e599 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -17,6 +17,7 @@ jobs: steps: - uses: actions/checkout@v2 + - uses: hrishikesh-kadam/setup-lcov@v1 # Note: This workflow uses the latest stable version of the Dart SDK. # You can specify other versions if desired, see documentation here: @@ -39,7 +40,10 @@ jobs: working-directory: lib - name: Run tests - run: flutter test --coverage --update-goldens + run: flutter test --coverage + + - name: Exclude coverage + run: lcov --remove coverage/lcov.info 'lib/flutter_eco_mode_platform_interface.dart' 'lib/messages.g.dart' 'lib/streams/*' -o coverage/lcov.info - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4.0.1 diff --git a/.gitignore b/.gitignore index 7c3c32e..7d63d3f 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,9 @@ migrate_working_dir/ **/doc/api/ .dart_tool/ build/ -.fvm + +# FVM Version Cache +.fvm/ + +# Coverage +coverage/ \ No newline at end of file diff --git a/README.md b/README.md index 6116f6e..4e82a9f 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ also to offer a less energy-consuming app. | Battery Level | Yes | Yes | X | X | | Battery In Low Power Mode | Yes | Yes | X | X | | **Battery Eco Mode** | Yes | Yes | X | X | +| **Connectivity** | Yes | No | X | X | ## Eco Mode @@ -82,6 +83,44 @@ It will return a boolean. ]).map((event) => event.every((element) => element)).asBroadcastStream(); ``` +## Connectivity + +### /!\ Only available for Android at the moment + +This feature can help you to observe the network, know if the device is connected to the internet, +or just want to adapt your app to the network state. + +We have created a class **_Connectivity_** which contains basic information about the network. + +And you can use directly the methode **_hasEnoughNetwork_** which follows these rules in the code + +``` +extension on Connectivity { + bool? get isEnough => type == ConnectivityType.unknown + ? null + : (_isMobileEnoughNetwork || _isWifiEnoughNetwork || type == ConnectivityType.ethernet); + + bool get _isMobileEnoughNetwork => + [ConnectivityType.mobile5g, ConnectivityType.mobile4g, ConnectivityType.mobile3g].contains(type); + + bool get _isWifiEnoughNetwork => + ConnectivityType.wifi == type && wifiSignalStrength != null ? wifiSignalStrength! >= minWifiSignalStrength : false; +} +``` + +### How does it work ? + +* First, we retrieve the type of network via native access. +* Then, if we have Wifi identified we catch the signal strength. +* And finally, we build and return the object Connectivity. + +At this moment, you can ask your self. Is it really reliable ? Is there a better way ? + +Probably the better thing to do is to make your own speed test in your app. +You're right, it's more precise, and you can directly define what is a good network fo your purposes. +But you need to ping a server, and it's not really eco-friendly. +Here we just use the native access, trust directly your device and OS. + ## Example See the `example` directory for a complete sample app using flutter_eco_mode. diff --git a/android/build.gradle b/android/build.gradle index 8fc7925..496b040 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -3,6 +3,7 @@ version '1.0-SNAPSHOT' buildscript { ext.kotlin_version = '1.7.10' + repositories { google() mavenCentral() @@ -12,9 +13,11 @@ buildscript { classpath 'com.android.tools.build:gradle:8.3.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } + } allprojects { + repositories { google() mavenCentral() @@ -25,6 +28,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { + if (project.android.hasProperty("namespace")) { namespace 'sncf.connect.tech.flutter_eco_mode' } @@ -56,6 +60,7 @@ android { testImplementation 'org.powermock:powermock-module-junit4-rule-agent:1.6.2' testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.2' testImplementation 'org.powermock:powermock-module-junit4:1.6.2' + implementation('com.google.code.gson:gson:2.10.1') } tasks.withType(Test).configureEach { diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 829843f..73c001d 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,7 @@ + + + + diff --git a/android/src/main/kotlin/listener/BatteryListener.kt b/android/src/main/kotlin/listener/BatteryListener.kt new file mode 100644 index 0000000..08f63f1 --- /dev/null +++ b/android/src/main/kotlin/listener/BatteryListener.kt @@ -0,0 +1,132 @@ +package listener + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.Intent.ACTION_BATTERY_CHANGED +import android.content.IntentFilter +import android.os.BatteryManager +import android.os.BatteryManager.BATTERY_STATUS_CHARGING +import android.os.BatteryManager.BATTERY_STATUS_DISCHARGING +import android.os.BatteryManager.BATTERY_STATUS_FULL +import android.os.BatteryManager.BATTERY_STATUS_NOT_CHARGING +import android.os.Build +import android.os.PowerManager +import androidx.annotation.RequiresApi +import io.flutter.plugin.common.EventChannel +import sncf.connect.tech.flutter_eco_mode.BatteryState.CHARGING +import sncf.connect.tech.flutter_eco_mode.BatteryState.DISCHARGING +import sncf.connect.tech.flutter_eco_mode.BatteryState.FULL +import sncf.connect.tech.flutter_eco_mode.BatteryState.UNKNOWN + +class PowerModeStreamHandler(private val context: Context) : EventChannel.StreamHandler { + + private var lowPowerModeEventSink: EventChannel.EventSink? = null + private var powerSavingReceiver: BroadcastReceiver? = null + + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + lowPowerModeEventSink = events + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setupPowerSavingReceiver() + } + } + + override fun onCancel(p0: Any?) { + context.unregisterReceiver(powerSavingReceiver) + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + private fun setupPowerSavingReceiver() { + powerSavingReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent?) { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + lowPowerModeEventSink?.success(powerManager.isPowerSaveMode) + } + } + val filter = IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED) + context.registerReceiver(powerSavingReceiver, filter) + } + +} + +class BatteryStateStreamHandler(private val context: Context) : EventChannel.StreamHandler { + + private var batteryStateEventSink: EventChannel.EventSink? = null + private var batteryStateReceiver: BroadcastReceiver? = null + + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + batteryStateEventSink = events + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setupBatteryStateReceiver() + } + } + + override fun onCancel(p0: Any?) { + context.unregisterReceiver(batteryStateReceiver) + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun setupBatteryStateReceiver() { + batteryStateReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent?) { + val event = when (intent?.action) { + ACTION_BATTERY_CHANGED -> + when (intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)) { + BATTERY_STATUS_CHARGING -> CHARGING.name + BATTERY_STATUS_FULL -> FULL.name + BATTERY_STATUS_DISCHARGING, BATTERY_STATUS_NOT_CHARGING -> DISCHARGING.name + else -> UNKNOWN.name + } + + else -> DISCHARGING.name + } + batteryStateEventSink?.success(event) + } + } + val filterBatteryState = IntentFilter() + filterBatteryState.addAction(ACTION_BATTERY_CHANGED) + context.registerReceiver(batteryStateReceiver, filterBatteryState) + } + +} + +class BatteryLevelStreamHandler(private val context: Context) : EventChannel.StreamHandler { + + private var batteryLevelEventSink: EventChannel.EventSink? = null + private var batteryLevelReceiver: BroadcastReceiver? = null + + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + batteryLevelEventSink = events + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setupBatteryLevelReceiver() + } + } + + override fun onCancel(p0: Any?) { + context.unregisterReceiver(batteryLevelReceiver) + } + + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + private fun setupBatteryLevelReceiver() { + + batteryLevelReceiver = object : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent?) { + val batteryPct = intent?.let { i -> + val level: Int = i.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + val scale: Int = i.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + level * 100 / scale.toFloat() + } + batteryLevelEventSink?.success(batteryPct?.toDouble()) + } + } + val filter = IntentFilter(ACTION_BATTERY_CHANGED) + context.registerReceiver(batteryLevelReceiver, filter) + + } + +} \ No newline at end of file diff --git a/android/src/main/kotlin/listener/ConnectivityListener.kt b/android/src/main/kotlin/listener/ConnectivityListener.kt new file mode 100644 index 0000000..dc06904 --- /dev/null +++ b/android/src/main/kotlin/listener/ConnectivityListener.kt @@ -0,0 +1,205 @@ +package listener + +import android.content.BroadcastReceiver +import android.content.ContentValues.TAG +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.ConnectivityManager.TYPE_ETHERNET +import android.net.ConnectivityManager.TYPE_MOBILE +import android.net.ConnectivityManager.TYPE_MOBILE_DUN +import android.net.ConnectivityManager.TYPE_MOBILE_HIPRI +import android.net.ConnectivityManager.TYPE_WIFI +import android.net.ConnectivityManager.TYPE_WIMAX +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.net.NetworkCapabilities.TRANSPORT_ETHERNET +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.net.wifi.WifiInfo +import android.net.wifi.WifiManager +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.telephony.TelephonyManager +import android.util.Log +import androidx.annotation.RequiresApi +import com.google.gson.Gson +import io.flutter.plugin.common.EventChannel +import sncf.connect.tech.flutter_eco_mode.Connectivity +import sncf.connect.tech.flutter_eco_mode.ConnectivityType +import sncf.connect.tech.flutter_eco_mode.ConnectivityType.ETHERNET +import sncf.connect.tech.flutter_eco_mode.ConnectivityType.MOBILE2G +import sncf.connect.tech.flutter_eco_mode.ConnectivityType.MOBILE3G +import sncf.connect.tech.flutter_eco_mode.ConnectivityType.MOBILE4G +import sncf.connect.tech.flutter_eco_mode.ConnectivityType.MOBILE5G +import sncf.connect.tech.flutter_eco_mode.ConnectivityType.NONE +import sncf.connect.tech.flutter_eco_mode.ConnectivityType.UNKNOWN +import sncf.connect.tech.flutter_eco_mode.ConnectivityType.WIFI + + +class ConnectivityListener(private val context: Context) : EventChannel.StreamHandler { + + private var connectivityStateEventSink: EventChannel.EventSink? = null + private var connectivityStateReceiver: BroadcastReceiver? = null + private var networkCallback: NetworkCallback? = null + private val mainHandler: Handler = Handler(Looper.getMainLooper()) + + @RequiresApi(Build.VERSION_CODES.M) + private val connectivityManager: ConnectivityManager = context.getSystemService( + ConnectivityManager::class.java + ) + + @RequiresApi(Build.VERSION_CODES.M) + private val telephonyManager: TelephonyManager = context.getSystemService( + TelephonyManager::class.java + ) + + @RequiresApi(Build.VERSION_CODES.R) + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + connectivityStateEventSink = events + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + networkCallback = object : NetworkCallback() { + override fun onAvailable(network: Network) { + Log.d(TAG, "The default network is now: $network") + sendEvent( + networkCapabilities = connectivityManager.getNetworkCapabilities(network), + telephonyManager = telephonyManager + ) + } + + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + Log.d(TAG, "The default network changed capabilities: $networkCapabilities") + sendEvent( + networkCapabilities = networkCapabilities, + telephonyManager = telephonyManager + ) + } + + override fun onLost(network: Network) { + Log.d(TAG, "Network lost, the last default network was $network") + sendEvent( + networkCapabilities = connectivityManager.getNetworkCapabilities(network), + telephonyManager = telephonyManager + ) + } + } + + connectivityManager.registerDefaultNetworkCallback(networkCallback as NetworkCallback) + + } else { + connectivityStateReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent?) { + sendEvent(telephonyManager = telephonyManager) + } + } + context.registerReceiver(connectivityStateReceiver, IntentFilter("android.net.conn.CONNECTIVITY_CHANGE")) + } + sendEvent(telephonyManager = telephonyManager) + } + + @RequiresApi(Build.VERSION_CODES.N) + override fun onCancel(p0: Any?) { + networkCallback?.let { + connectivityManager.unregisterNetworkCallback(it) + } ?: run { + try { + context.unregisterReceiver(connectivityStateReceiver) + } catch (e: Exception) { + Log.e(null, "Error on cancel network") + } + } + } + + + @RequiresApi(Build.VERSION_CODES.R) + private fun sendEvent( + networkCapabilities: NetworkCapabilities? = null, + telephonyManager: TelephonyManager, + ) { + val runnable = Runnable { + val networkType = connectivityManager.getNetworkType( + networkCapabilities = networkCapabilities, + telephonyManager = telephonyManager, + ) + connectivityStateEventSink?.success( + Gson().toJson(Connectivity(type = networkType, + wifiSignalStrength = networkCapabilities?.getWifiSignalStrength()?.toLong())) + ) + } + // Emit events on main thread + mainHandler.post(runnable) + } +} + +@RequiresApi(Build.VERSION_CODES.R) +@Suppress("DEPRECATION") +fun ConnectivityManager.getNetworkType( + networkCapabilities: NetworkCapabilities? = null, + telephonyManager: TelephonyManager, +): ConnectivityType { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Log.d(TAG, "NOT LEGACY Network") + return when { + networkCapabilities?.hasTransport(TRANSPORT_ETHERNET) == true -> ETHERNET + networkCapabilities?.hasTransport(TRANSPORT_WIFI) == true -> WIFI + networkCapabilities?.hasTransport(TRANSPORT_CELLULAR) == true -> telephonyManager.networkType() + else -> NONE + } + } else { + Log.d(TAG, "LEGACY Network") + return when (activeNetworkInfo?.type) { + TYPE_ETHERNET -> ETHERNET + TYPE_WIFI -> WIFI + TYPE_WIMAX -> WIFI + TYPE_MOBILE, + TYPE_MOBILE_DUN, + TYPE_MOBILE_HIPRI -> telephonyManager.networkType() + + else -> NONE + } + + } +} + + +@RequiresApi(Build.VERSION_CODES.R) +fun TelephonyManager.networkType(): ConnectivityType { + Log.d(TAG, "The mobile network is now: $dataNetworkType") + when (dataNetworkType) { + TelephonyManager.NETWORK_TYPE_GPRS, + TelephonyManager.NETWORK_TYPE_EDGE, + TelephonyManager.NETWORK_TYPE_CDMA, + TelephonyManager.NETWORK_TYPE_1xRTT, + TelephonyManager.NETWORK_TYPE_GSM + -> return MOBILE2G + + TelephonyManager.NETWORK_TYPE_UMTS, + TelephonyManager.NETWORK_TYPE_EVDO_0, + TelephonyManager.NETWORK_TYPE_EVDO_A, + TelephonyManager.NETWORK_TYPE_HSDPA, + TelephonyManager.NETWORK_TYPE_HSUPA, + TelephonyManager.NETWORK_TYPE_HSPA, + TelephonyManager.NETWORK_TYPE_EVDO_B, + TelephonyManager.NETWORK_TYPE_EHRPD, + TelephonyManager.NETWORK_TYPE_HSPAP, + TelephonyManager.NETWORK_TYPE_TD_SCDMA + -> return MOBILE3G + + TelephonyManager.NETWORK_TYPE_LTE + -> return MOBILE4G + + TelephonyManager.NETWORK_TYPE_NR + -> return MOBILE5G + + else -> return UNKNOWN + } +} + +@RequiresApi(Build.VERSION_CODES.Q) +fun NetworkCapabilities.getWifiSignalStrength(): Int? = let { transportInfo as? WifiInfo }?.rssi diff --git a/android/src/main/kotlin/sncf/connect/tech/flutter_eco_mode/FlutterEcoModePlugin.kt b/android/src/main/kotlin/sncf/connect/tech/flutter_eco_mode/FlutterEcoModePlugin.kt index 5b711d5..bcbe487 100644 --- a/android/src/main/kotlin/sncf/connect/tech/flutter_eco_mode/FlutterEcoModePlugin.kt +++ b/android/src/main/kotlin/sncf/connect/tech/flutter_eco_mode/FlutterEcoModePlugin.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.content.Intent.ACTION_BATTERY_CHANGED import android.content.IntentFilter +import android.net.ConnectivityManager import android.os.BatteryManager import android.os.BatteryManager.BATTERY_STATUS_CHARGING import android.os.BatteryManager.BATTERY_STATUS_DISCHARGING @@ -21,9 +22,16 @@ import android.os.PowerManager.THERMAL_STATUS_NONE import android.os.PowerManager.THERMAL_STATUS_SEVERE import android.os.PowerManager.THERMAL_STATUS_SHUTDOWN import android.os.StatFs +import android.telephony.TelephonyManager import androidx.annotation.RequiresApi import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.EventChannel +import listener.BatteryLevelStreamHandler +import listener.BatteryStateStreamHandler +import listener.ConnectivityListener +import listener.PowerModeStreamHandler +import listener.getNetworkType +import listener.getWifiSignalStrength import sncf.connect.tech.flutter_eco_mode.BatteryState.CHARGING import sncf.connect.tech.flutter_eco_mode.BatteryState.DISCHARGING import sncf.connect.tech.flutter_eco_mode.BatteryState.FULL @@ -40,6 +48,7 @@ class FlutterEcoModePlugin : FlutterPlugin, EcoModeApi { private val lowPowerModeEventChannel = "sncf.connect.tech/battery.isLowPowerMode" private val batteryStateEventChannel = "sncf.connect.tech/battery.state" private val batteryLevelEventChannel = "sncf.connect.tech/battery.level" + private val connectivityStateEventChannel = "sncf.connect.tech/connectivity.state" override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { EcoModeApi.setUp(flutterPluginBinding.binaryMessenger, this) @@ -56,6 +65,14 @@ class FlutterEcoModePlugin : FlutterPlugin, EcoModeApi { flutterPluginBinding.binaryMessenger, batteryLevelEventChannel, ).setStreamHandler(BatteryLevelStreamHandler(context)) + EventChannel( + flutterPluginBinding.binaryMessenger, + connectivityStateEventChannel, + ).setStreamHandler( + ConnectivityListener( + context + ) + ) } @@ -73,6 +90,7 @@ class FlutterEcoModePlugin : FlutterPlugin, EcoModeApi { } override fun getBatteryLevel(): Double = getBatteryLevel(getBatteryStatus()) + private fun getBatteryLevel(intent: Intent?): Double = intent?.let { val level: Int = it.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) val scale: Int = it.getIntExtra(BatteryManager.EXTRA_SCALE, -1) @@ -106,7 +124,6 @@ class FlutterEcoModePlugin : FlutterPlugin, EcoModeApi { // TODO: check if it returns the total of cores } - /// MEMORY override fun getTotalMemory(): Long { return Runtime.getRuntime().totalMemory() } @@ -115,7 +132,6 @@ class FlutterEcoModePlugin : FlutterPlugin, EcoModeApi { return Runtime.getRuntime().freeMemory() } - /// STORAGE override fun getTotalStorage(): Long { val statFs = StatFs(Environment.getExternalStorageDirectory().absolutePath) val blockSizeLong = statFs.blockSizeLong @@ -142,6 +158,21 @@ class FlutterEcoModePlugin : FlutterPlugin, EcoModeApi { return score / nbrParams } + @RequiresApi(Build.VERSION_CODES.R) + override fun getConnectivity(): Connectivity = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val connectivityManager = context.getSystemService(ConnectivityManager::class.java) + val network = connectivityManager.activeNetwork + val telephonyManager = context.getSystemService(TelephonyManager::class.java) + val networkCapabilities = connectivityManager.getNetworkCapabilities(network) + Connectivity(type = connectivityManager.getNetworkType( + telephonyManager = telephonyManager, + networkCapabilities = networkCapabilities + ), wifiSignalStrength = networkCapabilities?.getWifiSignalStrength()?.toLong()) + } else { + Connectivity(type = ConnectivityType.UNKNOWN) + } + private fun getBatteryStatus(): Intent? { return IntentFilter(ACTION_BATTERY_CHANGED).let { intentFilter -> context.registerReceiver(null, intentFilter) @@ -168,113 +199,4 @@ class FlutterEcoModePlugin : FlutterPlugin, EcoModeApi { } } -class PowerModeStreamHandler(private val context: Context) : EventChannel.StreamHandler { - - private var lowPowerModeEventSink: EventChannel.EventSink? = null - private var powerSavingReceiver: BroadcastReceiver? = null - - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - lowPowerModeEventSink = events - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - setupPowerSavingReceiver() - } - } - - override fun onCancel(p0: Any?) { - context.unregisterReceiver(powerSavingReceiver) - } - - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - private fun setupPowerSavingReceiver() { - powerSavingReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent?) { - val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager - lowPowerModeEventSink?.success(powerManager.isPowerSaveMode) - } - } - val filter = IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED) - context.registerReceiver(powerSavingReceiver, filter) - } - -} - -class BatteryStateStreamHandler(private val context: Context) : EventChannel.StreamHandler { - - private var batteryStateEventSink: EventChannel.EventSink? = null - private var batteryStateReceiver: BroadcastReceiver? = null - - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - batteryStateEventSink = events - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - setupBatteryStateReceiver() - } - } - - override fun onCancel(p0: Any?) { - context.unregisterReceiver(batteryStateReceiver) - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun setupBatteryStateReceiver() { - batteryStateReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent?) { - val event = when (intent?.action) { - ACTION_BATTERY_CHANGED -> - when (intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)) { - BATTERY_STATUS_CHARGING -> CHARGING.name - BATTERY_STATUS_FULL -> FULL.name - BATTERY_STATUS_DISCHARGING, BATTERY_STATUS_NOT_CHARGING -> DISCHARGING.name - else -> UNKNOWN.name - } - else -> DISCHARGING.name - } - batteryStateEventSink?.success(event) - } - } - val filterBatteryState = IntentFilter() - filterBatteryState.addAction(ACTION_BATTERY_CHANGED) - context.registerReceiver(batteryStateReceiver, filterBatteryState) - } - -} - -class BatteryLevelStreamHandler(private val context: Context) : EventChannel.StreamHandler { - - private var batteryLevelEventSink: EventChannel.EventSink? = null - private var batteryLevelReceiver: BroadcastReceiver? = null - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - batteryLevelEventSink = events - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - setupBatteryLevelReceiver() - } - } - - override fun onCancel(p0: Any?) { - context.unregisterReceiver(batteryLevelReceiver) - } - - - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - private fun setupBatteryLevelReceiver() { - - batteryLevelReceiver = object : BroadcastReceiver() { - - override fun onReceive(context: Context, intent: Intent?) { - val batteryPct = intent?.let { i -> - val level: Int = i.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) - val scale: Int = i.getIntExtra(BatteryManager.EXTRA_SCALE, -1) - level * 100 / scale.toFloat() - } - batteryLevelEventSink?.success(batteryPct?.toDouble()) - } - } - val filter = IntentFilter(ACTION_BATTERY_CHANGED) - context.registerReceiver(batteryLevelReceiver, filter) - - } - -} diff --git a/android/src/main/kotlin/sncf/connect/tech/flutter_eco_mode/Messages.g.kt b/android/src/main/kotlin/sncf/connect/tech/flutter_eco_mode/Messages.g.kt index 7144cae..c2b676d 100644 --- a/android/src/main/kotlin/sncf/connect/tech/flutter_eco_mode/Messages.g.kt +++ b/android/src/main/kotlin/sncf/connect/tech/flutter_eco_mode/Messages.g.kt @@ -1,5 +1,6 @@ -// Autogenerated from Pigeon (v18.0.0), do not edit directly. +// Autogenerated from Pigeon (v18.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") package sncf.connect.tech.flutter_eco_mode @@ -16,14 +17,14 @@ private fun wrapResult(result: Any?): List { } private fun wrapError(exception: Throwable): List { - if (exception is FlutterError) { - return listOf( + return if (exception is FlutterError) { + listOf( exception.code, exception.message, exception.details ) } else { - return listOf( + listOf( exception.javaClass.simpleName, exception.toString(), "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) @@ -74,6 +75,67 @@ enum class ThermalState(val raw: Int) { } } } + +enum class ConnectivityType(val raw: Int) { + ETHERNET(0), + WIFI(1), + MOBILE2G(2), + MOBILE3G(3), + MOBILE4G(4), + MOBILE5G(5), + NONE(6), + UNKNOWN(7); + + companion object { + fun ofRaw(raw: Int): ConnectivityType? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class Connectivity ( + val type: ConnectivityType, + val wifiSignalStrength: Long? = null + +) { + companion object { + @Suppress("LocalVariableName") + fun fromList(__pigeon_list: List): Connectivity { + val type = ConnectivityType.ofRaw(__pigeon_list[0] as Int)!! + val wifiSignalStrength = __pigeon_list[1].let { num -> if (num is Int) num.toLong() else num as Long? } + return Connectivity(type, wifiSignalStrength) + } + } + fun toList(): List { + return listOf( + type.raw, + wifiSignalStrength, + ) + } +} +private object EcoModeApiCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 128.toByte() -> { + return (readValue(buffer) as? List)?.let { + Connectivity.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is Connectivity -> { + stream.write(128) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface EcoModeApi { fun getPlatformInfo(): String @@ -87,25 +149,24 @@ interface EcoModeApi { fun getTotalStorage(): Long fun getFreeStorage(): Long fun getEcoScore(): Double? + fun getConnectivity(): Connectivity companion object { /** The codec used by EcoModeApi. */ val codec: MessageCodec by lazy { - StandardMessageCodec() + EcoModeApiCodec } /** Sets up an instance of `EcoModeApi` to handle messages through the `binaryMessenger`. */ - @Suppress("UNCHECKED_CAST") fun setUp(binaryMessenger: BinaryMessenger, api: EcoModeApi?, messageChannelSuffix: String = "") { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getPlatformInfo$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - var wrapped: List - try { - wrapped = listOf(api.getPlatformInfo()) + val wrapped: List = try { + listOf(api.getPlatformInfo()) } catch (exception: Throwable) { - wrapped = wrapError(exception) + wrapError(exception) } reply.reply(wrapped) } @@ -117,11 +178,10 @@ interface EcoModeApi { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getBatteryLevel$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - var wrapped: List - try { - wrapped = listOf(api.getBatteryLevel()) + val wrapped: List = try { + listOf(api.getBatteryLevel()) } catch (exception: Throwable) { - wrapped = wrapError(exception) + wrapError(exception) } reply.reply(wrapped) } @@ -133,11 +193,10 @@ interface EcoModeApi { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getBatteryState$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - var wrapped: List - try { - wrapped = listOf(api.getBatteryState().raw) + val wrapped: List = try { + listOf(api.getBatteryState().raw) } catch (exception: Throwable) { - wrapped = wrapError(exception) + wrapError(exception) } reply.reply(wrapped) } @@ -149,11 +208,10 @@ interface EcoModeApi { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.isBatteryInLowPowerMode$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - var wrapped: List - try { - wrapped = listOf(api.isBatteryInLowPowerMode()) + val wrapped: List = try { + listOf(api.isBatteryInLowPowerMode()) } catch (exception: Throwable) { - wrapped = wrapError(exception) + wrapError(exception) } reply.reply(wrapped) } @@ -165,11 +223,10 @@ interface EcoModeApi { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getThermalState$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - var wrapped: List - try { - wrapped = listOf(api.getThermalState().raw) + val wrapped: List = try { + listOf(api.getThermalState().raw) } catch (exception: Throwable) { - wrapped = wrapError(exception) + wrapError(exception) } reply.reply(wrapped) } @@ -181,11 +238,10 @@ interface EcoModeApi { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getProcessorCount$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - var wrapped: List - try { - wrapped = listOf(api.getProcessorCount()) + val wrapped: List = try { + listOf(api.getProcessorCount()) } catch (exception: Throwable) { - wrapped = wrapError(exception) + wrapError(exception) } reply.reply(wrapped) } @@ -197,11 +253,10 @@ interface EcoModeApi { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getTotalMemory$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - var wrapped: List - try { - wrapped = listOf(api.getTotalMemory()) + val wrapped: List = try { + listOf(api.getTotalMemory()) } catch (exception: Throwable) { - wrapped = wrapError(exception) + wrapError(exception) } reply.reply(wrapped) } @@ -213,11 +268,10 @@ interface EcoModeApi { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getFreeMemory$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - var wrapped: List - try { - wrapped = listOf(api.getFreeMemory()) + val wrapped: List = try { + listOf(api.getFreeMemory()) } catch (exception: Throwable) { - wrapped = wrapError(exception) + wrapError(exception) } reply.reply(wrapped) } @@ -229,11 +283,10 @@ interface EcoModeApi { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getTotalStorage$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - var wrapped: List - try { - wrapped = listOf(api.getTotalStorage()) + val wrapped: List = try { + listOf(api.getTotalStorage()) } catch (exception: Throwable) { - wrapped = wrapError(exception) + wrapError(exception) } reply.reply(wrapped) } @@ -245,11 +298,10 @@ interface EcoModeApi { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getFreeStorage$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - var wrapped: List - try { - wrapped = listOf(api.getFreeStorage()) + val wrapped: List = try { + listOf(api.getFreeStorage()) } catch (exception: Throwable) { - wrapped = wrapError(exception) + wrapError(exception) } reply.reply(wrapped) } @@ -261,11 +313,25 @@ interface EcoModeApi { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getEcoScore$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - var wrapped: List - try { - wrapped = listOf(api.getEcoScore()) + val wrapped: List = try { + listOf(api.getEcoScore()) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getConnectivity$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getConnectivity()) } catch (exception: Throwable) { - wrapped = wrapError(exception) + wrapError(exception) } reply.reply(wrapped) } diff --git a/android/src/test/kotlin/sncf/connect/tech/flutter_eco_mode/FlutterEcoModePluginTest.kt b/android/src/test/kotlin/sncf/connect/tech/flutter_eco_mode/FlutterEcoModePluginTest.kt deleted file mode 100644 index a6ecd48..0000000 --- a/android/src/test/kotlin/sncf/connect/tech/flutter_eco_mode/FlutterEcoModePluginTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package sncf.connect.tech.flutter_eco_mode - -import android.os.Build.VERSION -import org.junit.runner.RunWith -import org.powermock.core.classloader.annotations.PrepareForTest -import org.powermock.modules.junit4.PowerMockRunner -import kotlin.test.Test - -/* - * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. - * - * Once you have built the plugin's example app, you can run these tests from the command - * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or - * you can run them directly from IDEs that support JUnit such as Android Studio. - */ - -@RunWith(PowerMockRunner::class) -@PrepareForTest(VERSION::class) -internal class FlutterEcoModePluginTest { - @Test - fun onMethodCall_getPlatformVersion_returnsExpectedValue() { - } -} diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 3c472b9..a3c9831 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Thu Oct 31 15:39:36 CET 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 7cd7128..af1ec35 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -23,7 +23,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.3.0" apply false + id "com.android.application" version '7.4.2' apply false } include ":app" diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 4b3d37b..ef24a3e 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -21,8 +21,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_eco_mode: d754157bc349ad8c9a03bc5f08020569fe5510c6 - integration_test: 13825b8a9334a850581300559b8839134b124670 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 70693e4..b636303 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/example/lib/connectivity_state/connectivity_state_page.dart b/example/lib/connectivity_state/connectivity_state_page.dart new file mode 100644 index 0000000..8a3fd2e --- /dev/null +++ b/example/lib/connectivity_state/connectivity_state_page.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_eco_mode/flutter_eco_mode.dart'; +import 'package:flutter_eco_mode_example/extensions.dart'; + +import '../results.dart'; + +class ConnectivityStatePage extends StatelessWidget { + final FlutterEcoMode ecoMode; + + const ConnectivityStatePage(this.ecoMode, {super.key}); + + @override + Widget build(BuildContext context) { + return ResultsView( + [ + ResultLine( + label: 'Connectivity type', + future: ecoMode.getConnectivityTypeName, + stream: ecoMode.getConnectivityTypeStream, + ), + ResultLine( + label: 'Wifi signal strength', + future: ecoMode.getConnectivitySignalStrength, + stream: ecoMode.getConnectivitySignalStrengthStream, + ), + ResultLine( + label: 'Has enough Network', + future: ecoMode.hasEnoughNetwork, + stream: ecoMode.hasEnoughNetworkStream, + ), + ], + ); + } +} diff --git a/example/lib/extensions.dart b/example/lib/extensions.dart index b52863a..d8ab3f4 100644 --- a/example/lib/extensions.dart +++ b/example/lib/extensions.dart @@ -19,6 +19,18 @@ extension FlutterEcoModeExtension on FlutterEcoMode { Stream getBatteryLevelPercentStream() => batteryLevelEventStream .map((value) => value > 0 ? "${value.toInt()} %" : "not reachable"); + + Future getConnectivityTypeName() => + getConnectivity().then((value) => value.type.name); + + Future getConnectivitySignalStrength() => getConnectivity() + .then((value) => value.wifiSignalStrength?.toString() ?? "not reachable"); + + Stream getConnectivityTypeStream() => + connectivityStream.map((value) => value.type.name); + + Stream getConnectivitySignalStrengthStream() => connectivityStream + .map((value) => value.wifiSignalStrength?.toString() ?? "not reachable"); } extension FutureEcoRangeExtension on Future { diff --git a/example/lib/main.dart b/example/lib/main.dart index c6db0f8..6e8840b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_eco_mode/flutter_eco_mode.dart'; import 'package:flutter_eco_mode_example/eco_battery/eco_battery_page.dart'; import 'package:flutter_eco_mode_example/low_end_device/low_end_device_page.dart'; +import 'package:flutter_eco_mode_example/connectivity_state/connectivity_state_page.dart'; import 'package:flutter_eco_mode_example/wrapper_page.dart'; void main() { @@ -92,6 +93,18 @@ class _MyAppState extends State<_MyApp> { ), child: const Text("Eco Battery"), ), + const SizedBox(height: 32), + TextButton( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => WrapperPage( + ConnectivityStatePage(plugin), + title: "Connectivity State", + ), + ), + ), + child: const Text("Connectivity State"), + ), ], ), ); diff --git a/example/pubspec.lock b/example/pubspec.lock index d9a18c3..902e92a 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -81,7 +81,7 @@ packages: path: ".." relative: true source: path - version: "0.0.2" + version: "0.0.3" flutter_lints: dependency: "direct dev" description: @@ -109,26 +109,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -149,18 +149,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" mocktail: dependency: transitive description: @@ -181,10 +181,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -258,10 +258,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" vector_math: dependency: transitive description: @@ -274,10 +274,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.5" webdriver: dependency: transitive description: @@ -287,5 +287,5 @@ packages: source: hosted version: "3.0.3" sdks: - dart: ">=3.2.3 <4.0.0" - flutter: ">=3.3.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index dc42685..6f90663 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,6 +4,8 @@ description: "Demonstrates how to use the flutter_eco_mode plugin." # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev +version: 0.0.3 + environment: sdk: '>=3.2.3 <4.0.0' diff --git a/ios/Classes/FlutterEcoModePlugin.swift b/ios/Classes/FlutterEcoModePlugin.swift index f98b552..689b49d 100644 --- a/ios/Classes/FlutterEcoModePlugin.swift +++ b/ios/Classes/FlutterEcoModePlugin.swift @@ -22,6 +22,7 @@ public class FlutterEcoModePlugin: NSObject, FlutterPlugin, EcoModeApi { static let lowPowerModeEventChannelName = "sncf.connect.tech/battery.isLowPowerMode" static let batteryStateEventChannelName = "sncf.connect.tech/battery.state" static let batteryLevelEventChannelName = "sncf.connect.tech/battery.level" + static let connectivityStateEventChannelName = "sncf.connect.tech/connectivity.state" static public func register(with registrar: FlutterPluginRegistrar) { let messenger: FlutterBinaryMessenger = registrar.messenger() @@ -33,6 +34,8 @@ public class FlutterEcoModePlugin: NSObject, FlutterPlugin, EcoModeApi { FlutterEventChannel(name: batteryStateEventChannelName, binaryMessenger: messenger).setStreamHandler(BatteryStateStreamHandler()) FlutterEventChannel(name: batteryLevelEventChannelName, binaryMessenger: messenger).setStreamHandler(BatteryLevelStreamHandler()) + + FlutterEventChannel(name: connectivityStateEventChannelName, binaryMessenger: messenger).setStreamHandler(ConnectivityStateStreamHandler()) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { @@ -86,6 +89,10 @@ public class FlutterEcoModePlugin: NSObject, FlutterPlugin, EcoModeApi { return Int64(availabeRam) } + func getConnectivity() throws -> Connectivity { + return Connectivity(type: .unknown) + } + func getTotalStorage() throws -> Int64 { var storage: Int64 = 0 let fileURL: URL @@ -248,3 +255,18 @@ private func enableBatteryMonitoring() { device.isBatteryMonitoringEnabled = true } } + +public class ConnectivityStateStreamHandler: NSObject, FlutterStreamHandler { + + fileprivate var eventSink: FlutterEventSink? + + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + return nil + } + + +} diff --git a/ios/Classes/Messages.g.swift b/ios/Classes/Messages.g.swift index c378c48..8d2844f 100644 --- a/ios/Classes/Messages.g.swift +++ b/ios/Classes/Messages.g.swift @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v18.0.0), do not edit directly. +// Autogenerated from Pigeon (v18.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -58,6 +58,76 @@ enum ThermalState: Int { /// unknown state case unknown = 4 } + +enum ConnectivityType: Int { + case ethernet = 0 + case wifi = 1 + case mobile2g = 2 + case mobile3g = 3 + case mobile4g = 4 + case mobile5g = 5 + case none = 6 + case unknown = 7 +} + +/// Generated class from Pigeon that represents data sent in messages. +struct Connectivity { + var type: ConnectivityType + var wifiSignalStrength: Int64? = nil + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ __pigeon_list: [Any?]) -> Connectivity? { + let type = ConnectivityType(rawValue: __pigeon_list[0] as! Int)! + let wifiSignalStrength: Int64? = isNullish(__pigeon_list[1]) ? nil : (__pigeon_list[1] is Int64? ? __pigeon_list[1] as! Int64? : Int64(__pigeon_list[1] as! Int32)) + + return Connectivity( + type: type, + wifiSignalStrength: wifiSignalStrength + ) + } + func toList() -> [Any?] { + return [ + type.rawValue, + wifiSignalStrength, + ] + } +} +private class EcoModeApiCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 128: + return Connectivity.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class EcoModeApiCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? Connectivity { + super.writeByte(128) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class EcoModeApiCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return EcoModeApiCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return EcoModeApiCodecWriter(data: data) + } +} + +class EcoModeApiCodec: FlutterStandardMessageCodec { + static let shared = EcoModeApiCodec(readerWriter: EcoModeApiCodecReaderWriter()) +} + /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol EcoModeApi { func getPlatformInfo() throws -> String @@ -71,15 +141,17 @@ protocol EcoModeApi { func getTotalStorage() throws -> Int64 func getFreeStorage() throws -> Int64 func getEcoScore() throws -> Double? + func getConnectivity() throws -> Connectivity } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. class EcoModeApiSetup { /// The codec used by EcoModeApi. + static var codec: FlutterStandardMessageCodec { EcoModeApiCodec.shared } /// Sets up an instance of `EcoModeApi` to handle messages through the `binaryMessenger`. static func setUp(binaryMessenger: FlutterBinaryMessenger, api: EcoModeApi?, messageChannelSuffix: String = "") { let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let getPlatformInfoChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getPlatformInfo\(channelSuffix)", binaryMessenger: binaryMessenger) + let getPlatformInfoChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getPlatformInfo\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getPlatformInfoChannel.setMessageHandler { _, reply in do { @@ -92,7 +164,7 @@ class EcoModeApiSetup { } else { getPlatformInfoChannel.setMessageHandler(nil) } - let getBatteryLevelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getBatteryLevel\(channelSuffix)", binaryMessenger: binaryMessenger) + let getBatteryLevelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getBatteryLevel\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getBatteryLevelChannel.setMessageHandler { _, reply in do { @@ -105,7 +177,7 @@ class EcoModeApiSetup { } else { getBatteryLevelChannel.setMessageHandler(nil) } - let getBatteryStateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getBatteryState\(channelSuffix)", binaryMessenger: binaryMessenger) + let getBatteryStateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getBatteryState\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getBatteryStateChannel.setMessageHandler { _, reply in do { @@ -118,7 +190,7 @@ class EcoModeApiSetup { } else { getBatteryStateChannel.setMessageHandler(nil) } - let isBatteryInLowPowerModeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.isBatteryInLowPowerMode\(channelSuffix)", binaryMessenger: binaryMessenger) + let isBatteryInLowPowerModeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.isBatteryInLowPowerMode\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { isBatteryInLowPowerModeChannel.setMessageHandler { _, reply in do { @@ -131,7 +203,7 @@ class EcoModeApiSetup { } else { isBatteryInLowPowerModeChannel.setMessageHandler(nil) } - let getThermalStateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getThermalState\(channelSuffix)", binaryMessenger: binaryMessenger) + let getThermalStateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getThermalState\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getThermalStateChannel.setMessageHandler { _, reply in do { @@ -144,7 +216,7 @@ class EcoModeApiSetup { } else { getThermalStateChannel.setMessageHandler(nil) } - let getProcessorCountChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getProcessorCount\(channelSuffix)", binaryMessenger: binaryMessenger) + let getProcessorCountChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getProcessorCount\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getProcessorCountChannel.setMessageHandler { _, reply in do { @@ -157,7 +229,7 @@ class EcoModeApiSetup { } else { getProcessorCountChannel.setMessageHandler(nil) } - let getTotalMemoryChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getTotalMemory\(channelSuffix)", binaryMessenger: binaryMessenger) + let getTotalMemoryChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getTotalMemory\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getTotalMemoryChannel.setMessageHandler { _, reply in do { @@ -170,7 +242,7 @@ class EcoModeApiSetup { } else { getTotalMemoryChannel.setMessageHandler(nil) } - let getFreeMemoryChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getFreeMemory\(channelSuffix)", binaryMessenger: binaryMessenger) + let getFreeMemoryChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getFreeMemory\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getFreeMemoryChannel.setMessageHandler { _, reply in do { @@ -183,7 +255,7 @@ class EcoModeApiSetup { } else { getFreeMemoryChannel.setMessageHandler(nil) } - let getTotalStorageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getTotalStorage\(channelSuffix)", binaryMessenger: binaryMessenger) + let getTotalStorageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getTotalStorage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getTotalStorageChannel.setMessageHandler { _, reply in do { @@ -196,7 +268,7 @@ class EcoModeApiSetup { } else { getTotalStorageChannel.setMessageHandler(nil) } - let getFreeStorageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getFreeStorage\(channelSuffix)", binaryMessenger: binaryMessenger) + let getFreeStorageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getFreeStorage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getFreeStorageChannel.setMessageHandler { _, reply in do { @@ -209,7 +281,7 @@ class EcoModeApiSetup { } else { getFreeStorageChannel.setMessageHandler(nil) } - let getEcoScoreChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getEcoScore\(channelSuffix)", binaryMessenger: binaryMessenger) + let getEcoScoreChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getEcoScore\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getEcoScoreChannel.setMessageHandler { _, reply in do { @@ -222,5 +294,18 @@ class EcoModeApiSetup { } else { getEcoScoreChannel.setMessageHandler(nil) } + let getConnectivityChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getConnectivity\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getConnectivityChannel.setMessageHandler { _, reply in + do { + let result = try api.getConnectivity() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getConnectivityChannel.setMessageHandler(nil) + } } } diff --git a/lib/flutter_eco_mode.dart b/lib/flutter_eco_mode.dart index 5b2c553..dc430ab 100644 --- a/lib/flutter_eco_mode.dart +++ b/lib/flutter_eco_mode.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:developer'; import 'package:flutter/services.dart'; @@ -9,6 +10,7 @@ import 'package:flutter_eco_mode/streams/combine_latest.dart'; const double minEnoughBattery = 10.0; const double minScoreMidRangeDevice = 0.5; const double minScoreLowEndDevice = 0.3; +const int minWifiSignalStrength = -70; /// An implementation of [FlutterEcoModePlatform] that uses pigeon. class FlutterEcoMode extends FlutterEcoModePlatform { @@ -19,12 +21,15 @@ class FlutterEcoMode extends FlutterEcoModePlatform { StreamController.broadcast(); final StreamController _batteryLowPowerModeStreamController = StreamController.broadcast(); + final StreamController _connectivityStreamController = + StreamController.broadcast(); FlutterEcoMode({ EcoModeApi? api, EventChannel? batteryLevelEventChannel, EventChannel? batteryStatusEventChannel, EventChannel? batteryModeEventChannel, + EventChannel? connectivityStateEventChannel, }) : _api = api ?? EcoModeApi() { (batteryLevelEventChannel ?? const EventChannel('sncf.connect.tech/battery.level')) @@ -44,6 +49,12 @@ class FlutterEcoMode extends FlutterEcoModePlatform { .listen((event) { _batteryLowPowerModeStreamController.add(event); }); + (connectivityStateEventChannel ?? + const EventChannel('sncf.connect.tech/connectivity.state')) + .receiveBroadcastStream() + .listen((event) { + _connectivityStreamController.add(event); + }); } @override @@ -100,6 +111,7 @@ class FlutterEcoMode extends FlutterEcoModePlatform { _batteryLevelStreamController.close(); _batteryStateStreamController.close(); _batteryLowPowerModeStreamController.close(); + _connectivityStreamController.close(); } @override @@ -202,6 +214,48 @@ class FlutterEcoMode extends FlutterEcoModePlatform { batteryLevelEventStream.map((event) => event.isNotEnough), batteryStateEventStream.map((event) => event.isDischarging), ]).map((event) => event.every((element) => element)).asBroadcastStream(); + + @override + Stream get connectivityStream => + _connectivityStreamController.stream.map((event) { + try { + final connectivityMap = jsonDecode(event); + final connectivityTypeString = connectivityMap['type'].toLowerCase(); + final connectivityType = ConnectivityType.values.firstWhere( + (e) => e.name == connectivityTypeString, + orElse: () => ConnectivityType.unknown, + ); + final wifiSignalStrength = connectivityMap['wifiSignalStrength']; + return Connectivity( + type: connectivityType, wifiSignalStrength: wifiSignalStrength); + } catch (error, stackTrace) { + log(stackTrace.toString(), error: error); + return Connectivity(type: ConnectivityType.unknown); + } + }); + + @override + Future getConnectivity() async { + return await _api.getConnectivity(); + } + + @override + Future hasEnoughNetwork() async { + try { + final connectivity = await getConnectivity(); + return connectivity.isEnough; + } catch (error, stackTrace) { + log(stackTrace.toString(), error: error); + return null; + } + } + + @override + Stream hasEnoughNetworkStream() { + return connectivityStream + .map((event) => event.isEnough) + .asBroadcastStream(); + } } extension _BatteryLevel on double { @@ -217,6 +271,25 @@ extension on ThermalState { this == ThermalState.serious || this == ThermalState.critical; } +extension on Connectivity { + bool? get isEnough => type == ConnectivityType.unknown + ? null + : (_isMobileEnoughNetwork || + _isWifiEnoughNetwork || + type == ConnectivityType.ethernet); + + bool get _isMobileEnoughNetwork => [ + ConnectivityType.mobile5g, + ConnectivityType.mobile4g, + ConnectivityType.mobile3g + ].contains(type); + + bool get _isWifiEnoughNetwork => + ConnectivityType.wifi == type && wifiSignalStrength != null + ? wifiSignalStrength! >= minWifiSignalStrength + : false; +} + extension StreamExtensions on Stream { Stream withInitialValue(Future value) async* { yield await value; diff --git a/lib/flutter_eco_mode_platform_interface.dart b/lib/flutter_eco_mode_platform_interface.dart index 61527e8..86b19e9 100644 --- a/lib/flutter_eco_mode_platform_interface.dart +++ b/lib/flutter_eco_mode_platform_interface.dart @@ -71,6 +71,18 @@ abstract class FlutterEcoModePlatform extends PlatformInterface { /// Return the eco range. Future getDeviceRange(); + + /// Stream an object Connectivity with type and wifi signal strength. + Stream get connectivityStream; + + /// Return an object Connectivity with type and wifi signal strength. + Future getConnectivity(); + + /// Return a boolean which represents if the network is good enough. + Future hasEnoughNetwork(); + + /// Stream a boolean which represents if the network is good enough. + Stream hasEnoughNetworkStream(); } class DeviceRange { diff --git a/lib/messages.g.dart b/lib/messages.g.dart index 0df1f23..0a990b8 100644 --- a/lib/messages.g.dart +++ b/lib/messages.g.dart @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v18.0.0), do not edit directly. +// Autogenerated from Pigeon (v18.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -39,6 +39,66 @@ enum ThermalState { unknown, } +enum ConnectivityType { + ethernet, + wifi, + mobile2g, + mobile3g, + mobile4g, + mobile5g, + none, + unknown, +} + +class Connectivity { + Connectivity({ + required this.type, + this.wifiSignalStrength, + }); + + ConnectivityType type; + + int? wifiSignalStrength; + + Object encode() { + return [ + type.index, + wifiSignalStrength, + ]; + } + + static Connectivity decode(Object result) { + result as List; + return Connectivity( + type: ConnectivityType.values[result[0]! as int], + wifiSignalStrength: result[1] as int?, + ); + } +} + +class _EcoModeApiCodec extends StandardMessageCodec { + const _EcoModeApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is Connectivity) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return Connectivity.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + class EcoModeApi { /// Constructor for [EcoModeApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default @@ -50,8 +110,7 @@ class EcoModeApi { messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? __pigeon_binaryMessenger; - static const MessageCodec pigeonChannelCodec = - StandardMessageCodec(); + static const MessageCodec pigeonChannelCodec = _EcoModeApiCodec(); final String __pigeon_messageChannelSuffix; @@ -368,4 +427,33 @@ class EcoModeApi { return (__pigeon_replyList[0] as double?); } } + + Future getConnectivity() async { + final String __pigeon_channelName = + 'dev.flutter.pigeon.flutter_eco_mode.EcoModeApi.getConnectivity$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send(null) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as Connectivity?)!; + } + } } diff --git a/lib/streams/collection_extensions.dart b/lib/streams/collection_extensions.dart index f54622d..b1279c0 100644 --- a/lib/streams/collection_extensions.dart +++ b/lib/streams/collection_extensions.dart @@ -3,23 +3,6 @@ import 'dart:collection'; /// @internal /// Provides [mapNotNull] extension method on [Iterable]. extension MapNotNullIterableExtension on Iterable { - /// @internal - /// The non-`null` results of calling [transform] on the elements of [this]. - /// - /// Returns a lazy iterable which calls [transform] - /// on the elements of this iterable in iteration order, - /// then emits only the non-`null` values. - /// - /// If [transform] throws, the iteration is terminated. - Iterable mapNotNull(R? Function(T) transform) sync* { - for (final e in this) { - final v = transform(e); - if (v != null) { - yield v; - } - } - } - /// @internal /// Maps each element and its index to a new value. Iterable mapIndexed(R Function(int index, T element) transform) sync* { diff --git a/lib/streams/combine_latest.dart b/lib/streams/combine_latest.dart index 5d39bbc..c9379d0 100644 --- a/lib/streams/combine_latest.dart +++ b/lib/streams/combine_latest.dart @@ -105,187 +105,6 @@ class CombineLatestStream extends StreamView { }, ); - /// Constructs a [CombineLatestStream] from 4 [Stream]s - /// where [combiner] is used to create a new event of type [R], based on the - /// latest events emitted by the provided [Stream]s. - static CombineLatestStream combine4( - Stream streamA, - Stream streamB, - Stream streamC, - Stream streamD, - R Function(A a, B b, C c, D d) combiner, - ) => - CombineLatestStream( - [streamA, streamB, streamC, streamD], - (List values) { - return combiner( - values[0] as A, - values[1] as B, - values[2] as C, - values[3] as D, - ); - }, - ); - - /// Constructs a [CombineLatestStream] from 5 [Stream]s - /// where [combiner] is used to create a new event of type [R], based on the - /// latest events emitted by the provided [Stream]s. - static CombineLatestStream combine5( - Stream streamA, - Stream streamB, - Stream streamC, - Stream streamD, - Stream streamE, - R Function(A a, B b, C c, D d, E e) combiner, - ) => - CombineLatestStream( - [streamA, streamB, streamC, streamD, streamE], - (List values) { - return combiner( - values[0] as A, - values[1] as B, - values[2] as C, - values[3] as D, - values[4] as E, - ); - }, - ); - - /// Constructs a [CombineLatestStream] from 6 [Stream]s - /// where [combiner] is used to create a new event of type [R], based on the - /// latest events emitted by the provided [Stream]s. - static CombineLatestStream combine6( - Stream streamA, - Stream streamB, - Stream streamC, - Stream streamD, - Stream streamE, - Stream streamF, - R Function(A a, B b, C c, D d, E e, F f) combiner, - ) => - CombineLatestStream( - [streamA, streamB, streamC, streamD, streamE, streamF], - (List values) { - return combiner( - values[0] as A, - values[1] as B, - values[2] as C, - values[3] as D, - values[4] as E, - values[5] as F, - ); - }, - ); - - /// Constructs a [CombineLatestStream] from 7 [Stream]s - /// where [combiner] is used to create a new event of type [R], based on the - /// latest events emitted by the provided [Stream]s. - static CombineLatestStream combine7( - Stream streamA, - Stream streamB, - Stream streamC, - Stream streamD, - Stream streamE, - Stream streamF, - Stream streamG, - R Function(A a, B b, C c, D d, E e, F f, G g) combiner, - ) => - CombineLatestStream( - [streamA, streamB, streamC, streamD, streamE, streamF, streamG], - (List values) { - return combiner( - values[0] as A, - values[1] as B, - values[2] as C, - values[3] as D, - values[4] as E, - values[5] as F, - values[6] as G, - ); - }, - ); - - /// Constructs a [CombineLatestStream] from 8 [Stream]s - /// where [combiner] is used to create a new event of type [R], based on the - /// latest events emitted by the provided [Stream]s. - static CombineLatestStream combine8( - Stream streamA, - Stream streamB, - Stream streamC, - Stream streamD, - Stream streamE, - Stream streamF, - Stream streamG, - Stream streamH, - R Function(A a, B b, C c, D d, E e, F f, G g, H h) combiner, - ) => - CombineLatestStream( - [ - streamA, - streamB, - streamC, - streamD, - streamE, - streamF, - streamG, - streamH - ], - (List values) { - return combiner( - values[0] as A, - values[1] as B, - values[2] as C, - values[3] as D, - values[4] as E, - values[5] as F, - values[6] as G, - values[7] as H, - ); - }, - ); - - /// Constructs a [CombineLatestStream] from 9 [Stream]s - /// where [combiner] is used to create a new event of type [R], based on the - /// latest events emitted by the provided [Stream]s. - static CombineLatestStream combine9( - Stream streamA, - Stream streamB, - Stream streamC, - Stream streamD, - Stream streamE, - Stream streamF, - Stream streamG, - Stream streamH, - Stream streamI, - R Function(A a, B b, C c, D d, E e, F f, G g, H h, I i) combiner, - ) => - CombineLatestStream( - [ - streamA, - streamB, - streamC, - streamD, - streamE, - streamF, - streamG, - streamH, - streamI - ], - (List values) { - return combiner( - values[0] as A, - values[1] as B, - values[2] as C, - values[3] as D, - values[4] as E, - values[5] as F, - values[6] as G, - values[7] as H, - values[8] as I, - ); - }, - ); - static StreamController _buildController( Iterable> streams, R Function(List values) combiner, diff --git a/lib/streams/future.dart b/lib/streams/future.dart index 77e241f..856f240 100644 --- a/lib/streams/future.dart +++ b/lib/streams/future.dart @@ -1,13 +1,5 @@ import 'dart:async'; -/// @internal -/// An optimized version of [Future.wait]. -FutureOr waitTwoFutures(Future? f1, FutureOr f2) => f1 == null - ? f2 - : f2 is Future - ? Future.wait([f1, f2]).then(_ignore) - : f1; - /// @internal /// An optimized version of [Future.wait]. Future? waitFuturesList(List> futures) { diff --git a/pigeons/messages.dart b/pigeons/messages.dart index 9b06407..662670e 100644 --- a/pigeons/messages.dart +++ b/pigeons/messages.dart @@ -34,6 +34,24 @@ enum ThermalState { unknown, } +class Connectivity { + final ConnectivityType type; + final int? wifiSignalStrength; + + Connectivity({required this.type, this.wifiSignalStrength}); +} + +enum ConnectivityType { + ethernet, + wifi, + mobile2g, + mobile3g, + mobile4g, + mobile5g, + none, + unknown, +} + @HostApi() abstract class EcoModeApi { String getPlatformInfo(); @@ -57,4 +75,6 @@ abstract class EcoModeApi { int getFreeStorage(); double? getEcoScore(); + + Connectivity getConnectivity(); } diff --git a/test/flutter_eco_mode_test.dart b/test/flutter_eco_mode_test.dart index 4fdb743..f33f3fb 100644 --- a/test/flutter_eco_mode_test.dart +++ b/test/flutter_eco_mode_test.dart @@ -15,6 +15,7 @@ void main() { late EventChannel batteryLevelEventChannel; late EventChannel batteryStatusEventChannel; late EventChannel batteryModeEventChannel; + late EventChannel connectivityStateEventChannel; late EcoModeApi ecoModeApi; FlutterEcoMode buildEcoMode() => FlutterEcoMode( @@ -22,6 +23,7 @@ void main() { batteryLevelEventChannel: batteryLevelEventChannel, batteryStatusEventChannel: batteryStatusEventChannel, batteryModeEventChannel: batteryModeEventChannel, + connectivityStateEventChannel: connectivityStateEventChannel, ); setUp(() { @@ -33,148 +35,313 @@ void main() { .thenAnswer((_) async => false); when(() => ecoModeApi.getThermalState()) .thenAnswer((_) async => ThermalState.safe); + when(() => ecoModeApi.getConnectivity()) + .thenAnswer((_) async => Connectivity(type: ConnectivityType.unknown)); batteryLevelEventChannel = MockEventChannel(); batteryStatusEventChannel = MockEventChannel(); batteryModeEventChannel = MockEventChannel(); + connectivityStateEventChannel = MockEventChannel(); when(() => batteryLevelEventChannel.receiveBroadcastStream()) .thenAnswer((_) => Stream.value(100.0)); when(() => batteryStatusEventChannel.receiveBroadcastStream()) .thenAnswer((_) => Stream.value(BatteryState.charging.name)); when(() => batteryModeEventChannel.receiveBroadcastStream()) .thenAnswer((_) => Stream.value(false)); + when(() => connectivityStateEventChannel.receiveBroadcastStream()) + .thenAnswer((_) => Stream.value('{"type": "UNKNOWN"}')); }); - group( - 'isBatteryEcoMode', - () { - test('should return false', () async { - expect(await buildEcoMode().isBatteryEcoMode(), false); - }); + group('Battery Eco Mode', () { + group( + 'Future isBatteryEcoMode', + () { + test('should return false initially', () async { + expect(await buildEcoMode().isBatteryEcoMode(), false); + }); - test('should return true when not enough battery and discharging', - () async { - when(() => ecoModeApi.getBatteryLevel()) - .thenAnswer((_) async => minEnoughBattery - 1); - when(() => ecoModeApi.getBatteryState()) - .thenAnswer((_) async => BatteryState.discharging); - expect(await buildEcoMode().isBatteryEcoMode(), true); - }); + test('should return true when not enough battery and discharging', + () async { + when(() => ecoModeApi.getBatteryLevel()) + .thenAnswer((_) async => minEnoughBattery - 1); + when(() => ecoModeApi.getBatteryState()) + .thenAnswer((_) async => BatteryState.discharging); + expect(await buildEcoMode().isBatteryEcoMode(), true); + }); - test('should return true when battery in low power mode', () async { - when(() => ecoModeApi.isBatteryInLowPowerMode()) - .thenAnswer((_) async => true); - expect(await buildEcoMode().isBatteryEcoMode(), true); - }); + test('should return true when battery in low power mode', () async { + when(() => ecoModeApi.isBatteryInLowPowerMode()) + .thenAnswer((_) async => true); + expect(await buildEcoMode().isBatteryEcoMode(), true); + }); - test('should return true when thermal state is critical', () async { - when(() => ecoModeApi.getThermalState()) - .thenAnswer((_) async => ThermalState.critical); - expect(await buildEcoMode().isBatteryEcoMode(), true); - }); + test('should return true when thermal state is critical', () async { + when(() => ecoModeApi.getThermalState()) + .thenAnswer((_) async => ThermalState.critical); + expect(await buildEcoMode().isBatteryEcoMode(), true); + }); - test('should return true when thermal state is serious', () async { - when(() => ecoModeApi.getThermalState()) - .thenAnswer((_) async => ThermalState.serious); - expect(await buildEcoMode().isBatteryEcoMode(), true); - }); + test('should return true when thermal state is serious', () async { + when(() => ecoModeApi.getThermalState()) + .thenAnswer((_) async => ThermalState.serious); + expect(await buildEcoMode().isBatteryEcoMode(), true); + }); - test( - 'should return true when thermal state is serious and battery level is in error', - () async { - when(() => ecoModeApi.getThermalState()) - .thenAnswer((_) async => ThermalState.serious); - when(() => ecoModeApi.getBatteryLevel()) - .thenAnswer((_) => Future.error('error battery level')); - expect(await buildEcoMode().isBatteryEcoMode(), true); - }); + test( + 'should return true when thermal state is serious and battery level is in error', + () async { + when(() => ecoModeApi.getThermalState()) + .thenAnswer((_) async => ThermalState.serious); + when(() => ecoModeApi.getBatteryLevel()) + .thenAnswer((_) => Future.error('error battery level')); + expect(await buildEcoMode().isBatteryEcoMode(), true); + }); - test( - 'should return false when thermal state is safe and battery level is in error', - () async { - when(() => ecoModeApi.getThermalState()) - .thenAnswer((_) async => ThermalState.safe); - when(() => ecoModeApi.getBatteryLevel()) - .thenAnswer((_) => Future.error('error battery level')); - expect(await buildEcoMode().isBatteryEcoMode(), false); - }); + test( + 'should return false when thermal state is safe and battery level is in error', + () async { + when(() => ecoModeApi.getThermalState()) + .thenAnswer((_) async => ThermalState.safe); + when(() => ecoModeApi.getBatteryLevel()) + .thenAnswer((_) => Future.error('error battery level')); + expect(await buildEcoMode().isBatteryEcoMode(), false); + }); - test('should return null when impossible to get battery info', () async { - when(() => ecoModeApi.getBatteryLevel()) - .thenAnswer((_) => Future.error('error battery level')); - when(() => ecoModeApi.getBatteryState()) - .thenAnswer((_) => Future.error('error battery state')); - when(() => ecoModeApi.isBatteryInLowPowerMode()) - .thenAnswer((_) => Future.error('error battery low power mode')); - when(() => ecoModeApi.getThermalState()) - .thenAnswer((_) => Future.error('error thermal state')); - expect(await buildEcoMode().isBatteryEcoMode(), null); - }); + test('should return null when impossible to get battery info', + () async { + when(() => ecoModeApi.getBatteryLevel()) + .thenAnswer((_) => Future.error('error battery level')); + when(() => ecoModeApi.getBatteryState()) + .thenAnswer((_) => Future.error('error battery state')); + when(() => ecoModeApi.isBatteryInLowPowerMode()) + .thenAnswer((_) => Future.error('error battery low power mode')); + when(() => ecoModeApi.getThermalState()) + .thenAnswer((_) => Future.error('error thermal state')); + expect(await buildEcoMode().isBatteryEcoMode(), null); + }); - test('should wait all the future to complete the statement', () async { - when(() => ecoModeApi.getBatteryLevel()).thenAnswer((_) => - Future.delayed(const Duration(milliseconds: 100), () => 100.0)); - when(() => ecoModeApi.getBatteryState()).thenAnswer((_) => - Future.delayed(const Duration(milliseconds: 200), - () => BatteryState.charging)); - when(() => ecoModeApi.isBatteryInLowPowerMode()).thenAnswer((_) => - Future.delayed(const Duration(milliseconds: 300), () => false)); - when(() => ecoModeApi.getThermalState()).thenAnswer((_) => - Future.delayed( - const Duration(milliseconds: 400), () => ThermalState.serious)); - expect(await buildEcoMode().isBatteryEcoMode(), true); - }); - }, - ); + test('should wait all the future to complete the statement', () async { + when(() => ecoModeApi.getBatteryLevel()).thenAnswer((_) => + Future.delayed(const Duration(milliseconds: 100), () => 100.0)); + when(() => ecoModeApi.getBatteryState()).thenAnswer((_) => + Future.delayed(const Duration(milliseconds: 200), + () => BatteryState.charging)); + when(() => ecoModeApi.isBatteryInLowPowerMode()).thenAnswer((_) => + Future.delayed(const Duration(milliseconds: 300), () => false)); + when(() => ecoModeApi.getThermalState()).thenAnswer((_) => + Future.delayed(const Duration(milliseconds: 400), + () => ThermalState.serious)); + expect(await buildEcoMode().isBatteryEcoMode(), true); + }); + }, + ); + + group( + 'Stream isBatteryEcoMode', + () { + test('should return false initially', () async { + buildEcoMode().isBatteryEcoModeStream.listen(expectAsync1((event) { + expect(event, false); + }, count: 1)); + }); + + test('should return false when not enough battery and charging', + () async { + when(() => batteryLevelEventChannel.receiveBroadcastStream()) + .thenAnswer((_) => Stream.value(minEnoughBattery - 1)); + when(() => batteryStatusEventChannel.receiveBroadcastStream()) + .thenAnswer( + (_) => Stream.value(BatteryState.charging.name)); + buildEcoMode().isBatteryEcoModeStream.listen(expectAsync1((event) { + expect(event, false); + }, count: 1)); + }); + + test('should return false when enough battery and discharging', + () async { + when(() => batteryLevelEventChannel.receiveBroadcastStream()) + .thenAnswer((_) => Stream.value(minEnoughBattery + 1)); + when(() => batteryStatusEventChannel.receiveBroadcastStream()) + .thenAnswer( + (_) => Stream.value(BatteryState.discharging.name)); + buildEcoMode().isBatteryEcoModeStream.listen(expectAsync1((event) { + expect(event, false); + }, count: 1)); + }); + + test('should return true when not enough battery and discharging', + () async { + when(() => batteryLevelEventChannel.receiveBroadcastStream()) + .thenAnswer((_) => Stream.value(minEnoughBattery - 1)); + when(() => batteryStatusEventChannel.receiveBroadcastStream()) + .thenAnswer( + (_) => Stream.value(BatteryState.discharging.name)); + buildEcoMode().isBatteryEcoModeStream.listen(expectAsync1((event) { + expect(event, true); + }, count: 1)); + }); + + test('should return true when battery in low power mode', () async { + when(() => ecoModeApi.isBatteryInLowPowerMode()) + .thenAnswer((_) async => true); + buildEcoMode().isBatteryEcoModeStream.listen(expectAsync1((event) { + expect(event, true); + }, count: 1)); + }); + }, + ); + }); + + group('Connectivity', () { + group( + 'Future hasEnoughNetwork', + () { + test('should return null when connectivity is unknown', () async { + expect(await buildEcoMode().hasEnoughNetwork(), null); + }); + + void mockConnectivityType(ConnectivityType type, + {int? wifiSignalStrength}) { + when(() => ecoModeApi.getConnectivity()).thenAnswer((_) => + Future.value(Connectivity( + type: type, wifiSignalStrength: wifiSignalStrength))); + } + + void assertHasEnoughNetwork(bool? expected) async { + expect(await buildEcoMode().hasEnoughNetwork(), expected); + } + + test('should return true when connectivity type is ethernet', () async { + mockConnectivityType(ConnectivityType.ethernet); + assertHasEnoughNetwork(true); + }); + + test('should return false when connectivity type is mobile2g', + () async { + mockConnectivityType(ConnectivityType.mobile2g); + assertHasEnoughNetwork(false); + }); + + test('should return true when connectivity type is mobile3g', () async { + mockConnectivityType(ConnectivityType.mobile3g); + assertHasEnoughNetwork(true); + }); + + test('should return true when connectivity type is mobile4g', () async { + mockConnectivityType(ConnectivityType.mobile4g); + assertHasEnoughNetwork(true); + }); + + test('should return true when connectivity type is mobile5g', () async { + mockConnectivityType(ConnectivityType.mobile5g); + assertHasEnoughNetwork(true); + }); + + test( + 'should return true when connectivity type is WIFI and signal is enough', + () async { + mockConnectivityType(ConnectivityType.wifi, + wifiSignalStrength: minWifiSignalStrength); + assertHasEnoughNetwork(true); + }); + + test( + 'should return false when connectivity type is WIFI and signal is not enough', + () async { + mockConnectivityType(ConnectivityType.wifi, + wifiSignalStrength: minWifiSignalStrength - 1); + assertHasEnoughNetwork(false); + }); + + test( + 'should return false when connectivity type is WIFI and signal is null', + () async { + mockConnectivityType(ConnectivityType.wifi); + assertHasEnoughNetwork(false); + }); + }, + ); + + group( + 'Stream hasEnoughNetwork', + () { + test('should return null when connectivity is unknown', () async { + buildEcoMode().hasEnoughNetworkStream().listen(expectAsync1((event) { + expect(event, null); + }, count: 1)); + }); + + void mockConnectivityType(String type, {int? wifiSignalStrength}) { + when(() => connectivityStateEventChannel + .receiveBroadcastStream()).thenAnswer((_) => Stream< + String>.value( + '{"type": "$type", "wifiSignalStrength": $wifiSignalStrength}')); + } + + void assertHasEnoughNetwork(bool? expected) { + buildEcoMode().hasEnoughNetworkStream().listen(expectAsync1((event) { + expect(event, expected); + }, count: 1)); + } + + test('should return null when connectivity type not exists', () async { + mockConnectivityType('NOT_EXISTS'); + assertHasEnoughNetwork(null); + }); + + test('should return true when connectivity type is ETHERNET', () async { + mockConnectivityType('ETHERNET'); + assertHasEnoughNetwork(true); + }); + + test('should return false when connectivity type is MOBILE2G', + () async { + mockConnectivityType('MOBILE2G'); + assertHasEnoughNetwork(false); + }); + + test('should return true when connectivity type is MOBILE3G', () async { + mockConnectivityType('MOBILE3G'); + assertHasEnoughNetwork(true); + }); + + test('should return true when connectivity type is MOBILE4G', () async { + mockConnectivityType('MOBILE4G'); + assertHasEnoughNetwork(true); + }); + + test('should return true when connectivity type is MOBILE5G', () async { + mockConnectivityType('MOBILE5G'); + assertHasEnoughNetwork(true); + }); + + test( + 'should return true when connectivity type is WIFI and signal is enough', + () async { + mockConnectivityType('WIFI', + wifiSignalStrength: minWifiSignalStrength); + assertHasEnoughNetwork(true); + }); + + test( + 'should return false when connectivity type is WIFI and signal is not enough', + () async { + mockConnectivityType('WIFI', + wifiSignalStrength: minWifiSignalStrength - 1); + assertHasEnoughNetwork(false); + }); - group('isBatteryEcoMode Stream', () { - test('should return false', () async { - buildEcoMode().isBatteryEcoModeStream.listen(expectAsync1((event) { - expect(event, false); - }, count: 1)); - }); - - test('should return false when not enough battery and charging', () async { - when(() => batteryLevelEventChannel.receiveBroadcastStream()) - .thenAnswer((_) => Stream.value(minEnoughBattery - 1)); - when(() => batteryStatusEventChannel.receiveBroadcastStream()) - .thenAnswer((_) => Stream.value(BatteryState.charging.name)); - buildEcoMode().isBatteryEcoModeStream.listen(expectAsync1((event) { - expect(event, false); - }, count: 1)); - }); - - test('should return false when enough battery and discharging', () async { - when(() => batteryLevelEventChannel.receiveBroadcastStream()) - .thenAnswer((_) => Stream.value(minEnoughBattery + 1)); - when(() => batteryStatusEventChannel.receiveBroadcastStream()).thenAnswer( - (_) => Stream.value(BatteryState.discharging.name)); - buildEcoMode().isBatteryEcoModeStream.listen(expectAsync1((event) { - expect(event, false); - }, count: 1)); - }); - - test('should return true when not enough battery and discharging', - () async { - when(() => batteryLevelEventChannel.receiveBroadcastStream()) - .thenAnswer((_) => Stream.value(minEnoughBattery - 1)); - when(() => batteryStatusEventChannel.receiveBroadcastStream()).thenAnswer( - (_) => Stream.value(BatteryState.discharging.name)); - buildEcoMode().isBatteryEcoModeStream.listen(expectAsync1((event) { - expect(event, true); - }, count: 1)); - }); - - test('should return true when battery in low power mode', () async { - when(() => ecoModeApi.isBatteryInLowPowerMode()) - .thenAnswer((_) async => true); - buildEcoMode().isBatteryEcoModeStream.listen(expectAsync1((event) { - expect(event, true); - }, count: 1)); - }); + test( + 'should return false when connectivity type is WIFI and signal is null', + () async { + mockConnectivityType('WIFI'); + assertHasEnoughNetwork(false); + }); + }, + ); }); group( - 'getDeviceRange', + 'Device Range getEcoScore', () { test('should return null when get eco score error', () async { when(() => ecoModeApi.getEcoScore())