From 2e1c6431a1d27b0a4b20bd8dd0df1563267e00b6 Mon Sep 17 00:00:00 2001 From: Hennie Date: Mon, 11 Apr 2022 16:04:17 +0200 Subject: [PATCH] Add NMEA message event stream and channel --- .../baseflow/geolocator/GeolocatorPlugin.java | 11 +- .../geolocator/NmeaStreamHandlerImpl.java | 82 +++++++++++++ .../geolocator/location/NMEACallback.java | 5 + .../geolocator/location/NmeaClient.java | 19 ++- .../geolocator/location/NmeaMapper.java | 48 ++++++++ .../lib/geolocator_android.dart | 1 + .../lib/src/geolocator_android.dart | 27 +++++ .../lib/src/types/NMEAMessage.dart | 111 ++++++++++++++++++ .../lib/src/types/android_settings.dart | 2 + 9 files changed, 301 insertions(+), 5 deletions(-) create mode 100644 geolocator_android/android/src/main/java/com/baseflow/geolocator/NmeaStreamHandlerImpl.java create mode 100644 geolocator_android/android/src/main/java/com/baseflow/geolocator/location/NMEACallback.java create mode 100644 geolocator_android/android/src/main/java/com/baseflow/geolocator/location/NmeaMapper.java create mode 100644 geolocator_android/lib/src/types/NMEAMessage.dart diff --git a/geolocator_android/android/src/main/java/com/baseflow/geolocator/GeolocatorPlugin.java b/geolocator_android/android/src/main/java/com/baseflow/geolocator/GeolocatorPlugin.java index 5421febfa..d3c8b8c05 100644 --- a/geolocator_android/android/src/main/java/com/baseflow/geolocator/GeolocatorPlugin.java +++ b/geolocator_android/android/src/main/java/com/baseflow/geolocator/GeolocatorPlugin.java @@ -38,7 +38,7 @@ public class GeolocatorPlugin implements FlutterPlugin, ActivityAware { public void onServiceConnected(ComponentName name, IBinder service) { Log.d(TAG, "Geolocator foreground service connected"); if (service instanceof GeolocatorLocationService.LocalBinder) { - initialize(((GeolocatorLocationService.LocalBinder) service).getLocationService()); + initialize(((GeolocatorLocationService.LocalBinder) service).getLocationService()); } } @@ -51,6 +51,7 @@ public void onServiceDisconnected(ComponentName name) { } } }; + @Nullable private NmeaStreamHandlerImpl nmeaStreamHandlerImpl; @Nullable private LocationServiceHandlerImpl locationServiceHandler; @SuppressWarnings("deprecation") @@ -108,6 +109,10 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBindin streamHandler.startListening( flutterPluginBinding.getApplicationContext(), flutterPluginBinding.getBinaryMessenger()); + nmeaStreamHandlerImpl = new NmeaStreamHandlerImpl(); + nmeaStreamHandlerImpl.startListening( + flutterPluginBinding.getApplicationContext(), flutterPluginBinding.getBinaryMessenger()); + locationServiceHandler = new LocationServiceHandlerImpl(); locationServiceHandler.setContext(flutterPluginBinding.getApplicationContext()); locationServiceHandler.startListening( @@ -216,6 +221,10 @@ private void dispose() { streamHandler.setForegroundLocationService(null); streamHandler = null; } + + if (nmeaStreamHandlerImpl != null) { + nmeaStreamHandlerImpl.stopListening(); + } if (locationServiceHandler != null) { locationServiceHandler.setContext(null); locationServiceHandler.stopListening(); diff --git a/geolocator_android/android/src/main/java/com/baseflow/geolocator/NmeaStreamHandlerImpl.java b/geolocator_android/android/src/main/java/com/baseflow/geolocator/NmeaStreamHandlerImpl.java new file mode 100644 index 000000000..ef6694cee --- /dev/null +++ b/geolocator_android/android/src/main/java/com/baseflow/geolocator/NmeaStreamHandlerImpl.java @@ -0,0 +1,82 @@ +package com.baseflow.geolocator; + +import android.content.Context; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.baseflow.geolocator.location.NmeaClient; +import com.baseflow.geolocator.location.NmeaMapper; + +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.EventChannel; + +class NmeaStreamHandlerImpl implements EventChannel.StreamHandler { + private static final String TAG = "FlutterGeolocator"; + + @Nullable private EventChannel channel; + @Nullable private Context context; + @Nullable private NmeaClient nmeaClient; + + public NmeaStreamHandlerImpl() {} + + /** + * Registers this instance as event stream handler on the given {@code messenger}. + * + *

Stops any previously started and unstopped calls. + * + *

This should be cleaned with {@link #stopListening} once the messenger is disposed of. + */ + void startListening(Context context, BinaryMessenger messenger) { + if (channel != null) { + Log.w(TAG, "Setting a event call handler before the last was disposed."); + stopListening(); + } + + channel = new EventChannel(messenger, "flutter.baseflow.com/geolocator_nmea_updates_android"); + channel.setStreamHandler(this); + this.context = context; + this.nmeaClient = new NmeaClient(this.context); + } + + /** + * Clears this instance from listening to method calls. + * + *

Does nothing if {@link #startListening} hasn't been called, or if we're already stopped. + */ + void stopListening() { + if (channel == null) { + Log.d(TAG, "Tried to stop listening when no MethodChannel had been initialized."); + return; + } + + disposeListeners(); + channel.setStreamHandler(null); + channel = null; + } + + @Override + public void onListen(Object arguments, EventChannel.EventSink events) { + + if (nmeaClient == null) { + Log.e(TAG, "NMEA Client has not started correctly"); + return; + } + + nmeaClient.start(); + nmeaClient.setCallback((message -> events.success(NmeaMapper.toHashMap(message)))); + } + + @Override + public void onCancel(Object arguments) { + disposeListeners(); + } + + private void disposeListeners() { + Log.e(TAG, "Geolocator position updates stopped"); + if (nmeaClient != null) { + nmeaClient.setCallback(null); + nmeaClient.stop(); + } + } +} diff --git a/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/NMEACallback.java b/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/NMEACallback.java new file mode 100644 index 000000000..6c644147e --- /dev/null +++ b/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/NMEACallback.java @@ -0,0 +1,5 @@ +package com.baseflow.geolocator.location; + +public interface NMEACallback { + void onMessage(String message); +} diff --git a/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/NmeaClient.java b/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/NmeaClient.java index f422dc16c..b5052652b 100644 --- a/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/NmeaClient.java +++ b/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/NmeaClient.java @@ -16,13 +16,14 @@ public class NmeaClient { public static final String NMEA_MESSAGE_EXTRA = "geolocator_nmeaMessage"; public static final String NMEA_ALTITUDE_EXTRA = "geolocator_mslAltitude"; - private final Context context; - private final LocationManager locationManager; + @NonNull private final Context context; + @Nullable private final LocationManager locationManager; @TargetApi(Build.VERSION_CODES.N) - private OnNmeaMessageListener nmeaMessageListener; + @NonNull private OnNmeaMessageListener nmeaMessageListener; - private String lastNmeaMessage; + @Nullable private NMEACallback callback; + @Nullable private String lastNmeaMessage; private boolean listenerAdded = false; public NmeaClient(@NonNull Context context) { @@ -35,6 +36,9 @@ public NmeaClient(@NonNull Context context) { if (message.startsWith("$")) { lastNmeaMessage = message; } + if(callback != null) { + callback.onMessage(message); + } }; } } @@ -50,6 +54,13 @@ public void start() { } } + public void setCallback(NMEACallback callback) { + if(this.callback != null) { + throw new IllegalArgumentException("A callback has already been registered"); + } + this.callback = callback; + } + public void stop() { if (!listenerAdded) { return; diff --git a/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/NmeaMapper.java b/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/NmeaMapper.java new file mode 100644 index 000000000..29b018fec --- /dev/null +++ b/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/NmeaMapper.java @@ -0,0 +1,48 @@ +package com.baseflow.geolocator.location; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +public class NmeaMapper { + + public static Map toHashMap(String message) { + if (message == null) { + return null; + } + + Map nmeaMessage = new HashMap<>(); + + if (message.startsWith("$")) { + + nmeaMessage.put("nmeaMessage", message); + nmeaMessage.putAll(tryParseGPGAAMessage(message)); + } + + + return nmeaMessage; + } + + private static Map tryParseGPGAAMessage(String message) { + + Map parsedMessage = new HashMap<>(); + + String[] tokens = message.split(","); + String type = tokens[0]; + // Parse altitude above sea level, Detailed description of NMEA string here + // http://aprs.gids.nl/nmea/#gga + if (type.startsWith("$GPGGA") && tokens.length > 9) { + + parsedMessage.put("time", tokens[1]); + parsedMessage.put("latitude", tokens[2] + ',' + tokens[3]); + parsedMessage.put("longitude", tokens[4] + ',' + tokens[5]); + parsedMessage.put("quality", Integer.parseInt(tokens[6])); + parsedMessage.put("numberOfSatellites", Integer.parseInt(tokens[7])); + parsedMessage.put("horizontalDilutionOfPrecision", Double.parseDouble(tokens[8])); + parsedMessage.put("altitude", Double.parseDouble(tokens[9])); + parsedMessage.put("heightAboveEllipsoid", Double.parseDouble(tokens[11])); + } + + return parsedMessage; + } +} diff --git a/geolocator_android/lib/geolocator_android.dart b/geolocator_android/lib/geolocator_android.dart index 35b6d84e8..f95d79dd9 100644 --- a/geolocator_android/lib/geolocator_android.dart +++ b/geolocator_android/lib/geolocator_android.dart @@ -18,3 +18,4 @@ export 'package:geolocator_platform_interface/geolocator_platform_interface.dart export 'src/geolocator_android.dart'; export 'src/types/android_settings.dart' show AndroidSettings; export 'src/types/foreground_settings.dart' show ForegroundNotificationConfig; +export 'src/types/NMEAMessage.dart' show NMEAMessage; diff --git a/geolocator_android/lib/src/geolocator_android.dart b/geolocator_android/lib/src/geolocator_android.dart index 8dc27edb9..2c801ab09 100644 --- a/geolocator_android/lib/src/geolocator_android.dart +++ b/geolocator_android/lib/src/geolocator_android.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/services.dart'; +import 'package:geolocator_android/src/types/NMEAMessage.dart'; import 'package:geolocator_platform_interface/geolocator_platform_interface.dart'; /// An implementation of [GeolocatorPlatform] that uses method channels. @@ -19,6 +20,11 @@ class GeolocatorAndroid extends GeolocatorPlatform { static const _serviceStatusEventChannel = EventChannel('flutter.baseflow.com/geolocator_service_updates_android'); + /// The event channel used to receive [LocationServiceStatus] updates from the + /// native platform. + static const _nmeaEventChannel = + EventChannel('flutter.baseflow.com/geolocator_nmea_updates_android'); + /// Registers this class as the default instance of [GeolocatorPlatform]. static void registerWith() { GeolocatorPlatform.instance = GeolocatorAndroid(); @@ -32,6 +38,7 @@ class GeolocatorAndroid extends GeolocatorPlatform { Stream? _positionStream; Stream? _serviceStatusStream; + Stream? _nmeaMessagesStream; @override Future checkPermission() async { @@ -148,6 +155,26 @@ class GeolocatorAndroid extends GeolocatorPlatform { return _serviceStatusStream!; } + Stream getNmeaMessagesStream() { + if (_nmeaMessagesStream != null) { + return _nmeaMessagesStream!; + } + var nmeaMessagesStream = + _nmeaEventChannel.receiveBroadcastStream(); + + _nmeaMessagesStream = nmeaMessagesStream + .map((dynamic element) => NMEAMessage.fromMap(element.cast())) + .handleError((error) { + _nmeaMessagesStream = null; + if (error is PlatformException) { + error = _handlePlatformException(error); + } + throw error; + }); + + return _nmeaMessagesStream!; + } + @override Stream getPositionStream({ LocationSettings? locationSettings, diff --git a/geolocator_android/lib/src/types/NMEAMessage.dart b/geolocator_android/lib/src/types/NMEAMessage.dart new file mode 100644 index 000000000..4b13f0947 --- /dev/null +++ b/geolocator_android/lib/src/types/NMEAMessage.dart @@ -0,0 +1,111 @@ +/// Represents an NMEA message received from the platform. +/// If the NMEA message represents a GPGGA sequence then the message +/// will be parsed and the GPGGA fields will be populate as well. +class NMEAMessage { + + /// Initializes a new [NMEAMessage] instance with default values. + const NMEAMessage({ + required this.nmeaMessage, + this.time, + this.latitude, + this.longitude, + this.quality, + this.numberOfSatellites, + this.altitude, + this.heightAboveEllipsoid, + }); + + /// The raw NMEA Message + final String nmeaMessage; + + /// UTC time if the NMEA Message was GPGGA Sequence + /// GPGGA formatting can be found here: http://aprs.gids.nl/nmea/#gga + final String? time; + + /// Latitude if the NMEA Message was GPGGA Sequence + /// GPGGA formatting can be found here: http://aprs.gids.nl/nmea/#gga + final String? latitude; + + /// Longitude if the NMEA Message was GPGGA Sequence + /// GPGGA formatting can be found here: http://aprs.gids.nl/nmea/#gga + final String? longitude; + + /// Quality if the NMEA Message was GPGGA Sequence + /// Valid values can be found here: http://aprs.gids.nl/nmea/#gga + final int? quality; + + /// Number of Satellites if the NMEA Message was GPGGA Sequence + final int? numberOfSatellites; + + /// Altitude above mean sea level in meters if the NMEA Message was GPGGA Sequence + final double? altitude; + + /// Height of geoid above QGS84 ellipsoid + /// if the NMEA Message was GPGGA Sequence + final double? heightAboveEllipsoid; + + @override + bool operator ==(Object other) { + var areEqual = other is NMEAMessage && + other.nmeaMessage == nmeaMessage && + other.time == time && + other.latitude == latitude && + other.longitude == longitude && + other.quality == quality && + other.numberOfSatellites == numberOfSatellites && + other.altitude == altitude && + other.heightAboveEllipsoid == heightAboveEllipsoid; + + return areEqual; + } + + @override + int get hashCode => + nmeaMessage.hashCode ^ + time.hashCode ^ + latitude.hashCode ^ + longitude.hashCode ^ + quality.hashCode ^ + numberOfSatellites.hashCode ^ + altitude.hashCode ^ + heightAboveEllipsoid.hashCode; + + @override + String toString() { + return 'NMEA Message: $nmeaMessage'; + } + + /// Converts the supplied [Map] to an instance of the [NMEAMessage] class. + static NMEAMessage fromMap(dynamic message) { + final Map nmeaMap = message; + + if (!nmeaMap.containsKey('nmeaMessage')) { + throw ArgumentError.value(nmeaMap, 'nmeaMap', + 'The supplied map doesn\'t contain the mandatory key `nmeaMessage`.'); + } + + return NMEAMessage( + nmeaMessage: nmeaMap['nmeaMessage'], + time: nmeaMap['time'], + latitude: nmeaMap['latitude'], + longitude: nmeaMap['longitude'], + quality: nmeaMap['quality'], + numberOfSatellites: nmeaMap['numberOfSatellites'], + altitude: nmeaMap['altitude'], + heightAboveEllipsoid: nmeaMap['heightAboveEllipsoid'], + ); + } + + /// Converts the [NMEAMessage] instance into a [Map] instance that can be + /// serialized to JSON. + Map toJson() => { + 'nmeaMessage': nmeaMessage, + 'time': time, + 'latitude': latitude, + 'longitude': longitude, + 'quality': quality, + 'numberOfSatellites': numberOfSatellites, + 'altitude': altitude, + 'heightAboveEllipsoid': heightAboveEllipsoid, + }; +} diff --git a/geolocator_android/lib/src/types/android_settings.dart b/geolocator_android/lib/src/types/android_settings.dart index 67d8c2519..7246fb619 100644 --- a/geolocator_android/lib/src/types/android_settings.dart +++ b/geolocator_android/lib/src/types/android_settings.dart @@ -9,6 +9,8 @@ class AndroidSettings extends LocationSettings { /// /// The following default values are used: /// - forceLocationManager: false + /// - accuracy: best + /// - useMSLAltitude: false AndroidSettings({ this.forceLocationManager = false, LocationAccuracy accuracy = LocationAccuracy.best,