diff --git a/geolocator_android/CHANGELOG.md b/geolocator_android/CHANGELOG.md index a4067024..18a37faf 100644 --- a/geolocator_android/CHANGELOG.md +++ b/geolocator_android/CHANGELOG.md @@ -1,3 +1,11 @@ +## UNRELEASED + +* Adds location accuracy filtering to prevent random GPS drifts (Issue #1574). Features added: + * `LocationAccuracyFilter` class to filter out location updates with unrealistic movement patterns + * Optional filtering through new `enableAccuracyFilter` parameter + * Configurable thresholds for accuracy, speed, and distance jumps + * Comprehensive test coverage for various transportation modes + ## 5.0.1+1 - Bump `androidx.core:core` to version 1.16.0 diff --git a/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/FusedLocationClient.java b/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/FusedLocationClient.java index a5e3f2b1..04be16c7 100644 --- a/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/FusedLocationClient.java +++ b/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/FusedLocationClient.java @@ -79,7 +79,11 @@ public synchronized void onLocationResult(@NonNull LocationResult locationResult } nmeaClient.enrichExtrasWithNmea(location); - positionChangedCallback.onPositionChanged(location); + + // Apply the location accuracy filter before reporting the position + if (LocationAccuracyFilter.shouldAcceptLocation(location)) { + positionChangedCallback.onPositionChanged(location); + } } @Override @@ -230,6 +234,13 @@ public void startPositionUpdates( this.positionChangedCallback = positionChangedCallback; this.errorCallback = errorCallback; + // Enable or disable the location accuracy filter based on options + if (this.locationOptions != null) { + LocationAccuracyFilter.setFilterEnabled(this.locationOptions.isEnableAccuracyFilter()); + } else { + LocationAccuracyFilter.setFilterEnabled(false); + } + LocationRequest locationRequest = buildLocationRequest(this.locationOptions); LocationSettingsRequest settingsRequest = buildLocationSettingsRequest(locationRequest); @@ -278,5 +289,7 @@ public void startPositionUpdates( public void stopPositionUpdates() { this.nmeaClient.stop(); fusedLocationProviderClient.removeLocationUpdates(locationCallback); + // Reset the location filter when updates are stopped + LocationAccuracyFilter.reset(); } } diff --git a/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/LocationAccuracyFilter.java b/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/LocationAccuracyFilter.java new file mode 100644 index 00000000..704e096d --- /dev/null +++ b/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/LocationAccuracyFilter.java @@ -0,0 +1,112 @@ +package com.baseflow.geolocator.location; + +import android.location.Location; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Utility class to filter inaccurate location updates that might cause GPS drift. + * Detects and filters out positions that are physically implausible based on + * speed, distance, and accuracy. + */ +public class LocationAccuracyFilter { + private static final String TAG = "LocationAccuracyFilter"; + + // Constants for filtering + private static final float MAX_ACCURACY_THRESHOLD = 300.0f; // meters + private static final float MAX_SPEED_THRESHOLD = 280.0f; // m/s (~1000 km/h to support aircraft) + private static final float MAX_DISTANCE_JUMP = 1000.0f; // meters + + @Nullable private static Location lastFilteredLocation = null; + private static boolean filterEnabled = false; + + /** + * Sets whether filtering is enabled. + * This should be called when starting location updates based on user preferences. + * + * @param enabled Whether filtering should be enabled + */ + public static void setFilterEnabled(boolean enabled) { + filterEnabled = enabled; + // Reset the filter state when changing the setting + if (enabled) { + reset(); + } + } + + /** + * Checks if a location update should be accepted based on realistic movement patterns. + * + * @param newLocation The new location to evaluate + * @return true if the location should be accepted, false if it should be filtered + */ + public static boolean shouldAcceptLocation(@NonNull Location newLocation) { + // If filtering is disabled, always accept locations + if (!filterEnabled) { + return true; + } + + // Always accept the first position + if (lastFilteredLocation == null) { + lastFilteredLocation = newLocation; + return true; + } + + // Time difference in seconds + float timeDelta = (newLocation.getTime() - lastFilteredLocation.getTime()) / 1000.0f; + + // Don't filter if time hasn't advanced + if (timeDelta <= 0) { + lastFilteredLocation = newLocation; + return true; + } + + // Calculate distance in meters + float distance = newLocation.distanceTo(lastFilteredLocation); + + // Calculate speed (m/s) + float speed = distance / timeDelta; + + // Get position accuracy (if available) + float accuracy = newLocation.hasAccuracy() ? newLocation.getAccuracy() : 0.0f; + + // Filters to apply - use a conservative approach to avoid affecting legitimate use cases + boolean shouldFilter = false; + + // Filter based on very poor accuracy - this catches points with extremely poor accuracy + if (accuracy > MAX_ACCURACY_THRESHOLD) { + Log.d(TAG, "Filtered location: Poor accuracy " + accuracy + "m"); + shouldFilter = true; + } + + // Filter based on unrealistically high speeds + if (speed > MAX_SPEED_THRESHOLD) { + Log.d(TAG, "Filtered location: Unrealistic speed " + speed + "m/s"); + shouldFilter = true; + } + + // Filter based on large jumps when accuracy is moderate or poor + if (distance > MAX_DISTANCE_JUMP && accuracy > 75.0f) { + Log.d(TAG, "Filtered location: Large jump with poor accuracy - distance: " + + distance + "m, accuracy: " + accuracy + "m"); + shouldFilter = true; + } + + // Accept and update reference if it passes filters + if (!shouldFilter) { + lastFilteredLocation = newLocation; + } + + return !shouldFilter; + } + + /** + * Resets the internal state of the filter. + * This should be called when location updates are stopped. + */ + public static void reset() { + lastFilteredLocation = null; + } +} \ No newline at end of file diff --git a/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/LocationManagerClient.java b/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/LocationManagerClient.java index c8b3e59a..aede5c9d 100644 --- a/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/LocationManagerClient.java +++ b/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/LocationManagerClient.java @@ -170,6 +170,11 @@ public void startPositionUpdates( ? LocationRequestCompat.PASSIVE_INTERVAL : locationOptions.getTimeInterval(); quality = accuracyToQuality(accuracy); + + // Enable or disable the location accuracy filter based on options + LocationAccuracyFilter.setFilterEnabled(locationOptions.isEnableAccuracyFilter()); + } else { + LocationAccuracyFilter.setFilterEnabled(false); } this.currentLocationProvider = determineProvider(this.locationManager, accuracy); @@ -202,11 +207,12 @@ public void stopPositionUpdates() { this.isListening = false; this.nmeaClient.stop(); this.locationManager.removeUpdates(this); + LocationAccuracyFilter.reset(); } @Override public synchronized void onLocationChanged(Location location) { - if (isBetterLocation(location, currentBestLocation)) { + if (isBetterLocation(location, currentBestLocation) && LocationAccuracyFilter.shouldAcceptLocation(location)) { this.currentBestLocation = location; if (this.positionChangedCallback != null) { diff --git a/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/LocationOptions.java b/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/LocationOptions.java index 9087c827..4e2fe16b 100644 --- a/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/LocationOptions.java +++ b/geolocator_android/android/src/main/java/com/baseflow/geolocator/location/LocationOptions.java @@ -9,24 +9,27 @@ public class LocationOptions { private final long distanceFilter; private final long timeInterval; private final boolean useMSLAltitude; + private final boolean enableAccuracyFilter; private LocationOptions( - LocationAccuracy accuracy, long distanceFilter, long timeInterval, boolean useMSLAltitude) { + LocationAccuracy accuracy, long distanceFilter, long timeInterval, boolean useMSLAltitude, boolean enableAccuracyFilter) { this.accuracy = accuracy; this.distanceFilter = distanceFilter; this.timeInterval = timeInterval; this.useMSLAltitude = useMSLAltitude; + this.enableAccuracyFilter = enableAccuracyFilter; } public static LocationOptions parseArguments(Map arguments) { if (arguments == null) { - return new LocationOptions(LocationAccuracy.best, 0, 5000, false); + return new LocationOptions(LocationAccuracy.best, 0, 5000, false, false); } final Integer accuracy = (Integer) arguments.get("accuracy"); final Integer distanceFilter = (Integer) arguments.get("distanceFilter"); final Integer timeInterval = (Integer) arguments.get("timeInterval"); final Boolean useMSLAltitude = (Boolean) arguments.get("useMSLAltitude"); + final Boolean enableAccuracyFilter = (Boolean) arguments.get("enableAccuracyFilter"); LocationAccuracy locationAccuracy = LocationAccuracy.best; @@ -57,7 +60,8 @@ public static LocationOptions parseArguments(Map arguments) { locationAccuracy, distanceFilter != null ? distanceFilter : 0, timeInterval != null ? timeInterval : 5000, - useMSLAltitude != null && useMSLAltitude); + useMSLAltitude != null && useMSLAltitude, + enableAccuracyFilter != null && enableAccuracyFilter); } public LocationAccuracy getAccuracy() { @@ -75,4 +79,8 @@ public long getTimeInterval() { public boolean isUseMSLAltitude() { return useMSLAltitude; } + + public boolean isEnableAccuracyFilter() { + return enableAccuracyFilter; + } } diff --git a/geolocator_android/android/src/test/java/com/baseflow/geolocator/LocationAccuracyFilterTest.java b/geolocator_android/android/src/test/java/com/baseflow/geolocator/LocationAccuracyFilterTest.java new file mode 100644 index 00000000..3faf7108 --- /dev/null +++ b/geolocator_android/android/src/test/java/com/baseflow/geolocator/LocationAccuracyFilterTest.java @@ -0,0 +1,204 @@ +package com.baseflow.geolocator; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import android.location.Location; + +import com.baseflow.geolocator.location.LocationAccuracyFilter; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class LocationAccuracyFilterTest { + @Mock private Location mockLocation1; + @Mock private Location mockLocation2; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + // Reset the filter state before each test + LocationAccuracyFilter.reset(); + // Enable filtering by default for tests + LocationAccuracyFilter.setFilterEnabled(true); + } + + @Test + public void shouldAcceptLocation_withFirstLocation_returnsTrue() { + // Arrange + when(mockLocation1.getTime()).thenReturn(1000L); + when(mockLocation1.hasAccuracy()).thenReturn(true); + when(mockLocation1.getAccuracy()).thenReturn(10.0f); + + // Act & Assert + assertTrue(LocationAccuracyFilter.shouldAcceptLocation(mockLocation1)); + } + + @Test + public void shouldAcceptLocation_withPoorAccuracy_returnsFalse() { + // Arrange - First accepted location + when(mockLocation1.getTime()).thenReturn(1000L); + when(mockLocation1.hasAccuracy()).thenReturn(true); + when(mockLocation1.getAccuracy()).thenReturn(10.0f); + LocationAccuracyFilter.shouldAcceptLocation(mockLocation1); + + // Arrange - Second location with poor accuracy + when(mockLocation2.getTime()).thenReturn(2000L); + when(mockLocation2.hasAccuracy()).thenReturn(true); + when(mockLocation2.getAccuracy()).thenReturn(350.0f); // Above threshold + when(mockLocation2.distanceTo(mockLocation1)).thenReturn(50.0f); + + // Act & Assert + assertFalse(LocationAccuracyFilter.shouldAcceptLocation(mockLocation2)); + } + + @Test + public void shouldAcceptLocation_withUnrealisticSpeed_returnsFalse() { + // Arrange - First accepted location + when(mockLocation1.getTime()).thenReturn(1000L); + when(mockLocation1.hasAccuracy()).thenReturn(true); + when(mockLocation1.getAccuracy()).thenReturn(10.0f); + LocationAccuracyFilter.shouldAcceptLocation(mockLocation1); + + // Arrange - Second location with unrealistic speed (5000m in 1 second = 5000 m/s) + when(mockLocation2.getTime()).thenReturn(2000L); + when(mockLocation2.hasAccuracy()).thenReturn(true); + when(mockLocation2.getAccuracy()).thenReturn(10.0f); + when(mockLocation2.distanceTo(mockLocation1)).thenReturn(5000.0f); + + // Act & Assert + assertFalse(LocationAccuracyFilter.shouldAcceptLocation(mockLocation2)); + } + + @Test + public void shouldAcceptLocation_withLargeJumpAndModerateAccuracy_returnsFalse() { + // Arrange - First accepted location + when(mockLocation1.getTime()).thenReturn(1000L); + when(mockLocation1.hasAccuracy()).thenReturn(true); + when(mockLocation1.getAccuracy()).thenReturn(10.0f); + LocationAccuracyFilter.shouldAcceptLocation(mockLocation1); + + // Arrange - Second location with large jump and moderate accuracy + when(mockLocation2.getTime()).thenReturn(2000L); + when(mockLocation2.hasAccuracy()).thenReturn(true); + when(mockLocation2.getAccuracy()).thenReturn(80.0f); // Moderate accuracy + when(mockLocation2.distanceTo(mockLocation1)).thenReturn(2000.0f); // Large jump + + // Act & Assert + assertFalse(LocationAccuracyFilter.shouldAcceptLocation(mockLocation2)); + } + + @Test + public void shouldAcceptLocation_withNormalMovement_returnsTrue() { + // Arrange - First accepted location + when(mockLocation1.getTime()).thenReturn(1000L); + when(mockLocation1.hasAccuracy()).thenReturn(true); + when(mockLocation1.getAccuracy()).thenReturn(10.0f); + LocationAccuracyFilter.shouldAcceptLocation(mockLocation1); + + // Arrange - Second location with normal movement (50m in 10 seconds = 5 m/s) + when(mockLocation2.getTime()).thenReturn(11000L); + when(mockLocation2.hasAccuracy()).thenReturn(true); + when(mockLocation2.getAccuracy()).thenReturn(10.0f); + when(mockLocation2.distanceTo(mockLocation1)).thenReturn(50.0f); + + // Act & Assert + assertTrue(LocationAccuracyFilter.shouldAcceptLocation(mockLocation2)); + } + + @Test + public void shouldAcceptLocation_withResetBetweenLocations_returnsTrue() { + // Arrange - First accepted location + when(mockLocation1.getTime()).thenReturn(1000L); + when(mockLocation1.hasAccuracy()).thenReturn(true); + when(mockLocation1.getAccuracy()).thenReturn(10.0f); + LocationAccuracyFilter.shouldAcceptLocation(mockLocation1); + + // Reset the filter + LocationAccuracyFilter.reset(); + + // Arrange - Second location that would normally be rejected (5000m in 1 second = 5000 m/s) + when(mockLocation2.getTime()).thenReturn(2000L); + when(mockLocation2.hasAccuracy()).thenReturn(true); + when(mockLocation2.getAccuracy()).thenReturn(10.0f); + when(mockLocation2.distanceTo(mockLocation1)).thenReturn(5000.0f); + + // Act & Assert - Should be accepted because filter was reset + assertTrue(LocationAccuracyFilter.shouldAcceptLocation(mockLocation2)); + } + + @Test + public void shouldAcceptLocation_withRealisticTransportationSpeeds() { + // Arrange - First accepted location + when(mockLocation1.getTime()).thenReturn(1000L); + when(mockLocation1.hasAccuracy()).thenReturn(true); + when(mockLocation1.getAccuracy()).thenReturn(10.0f); + LocationAccuracyFilter.shouldAcceptLocation(mockLocation1); + + // Test case 1: Airplane speed (900 km/h = 250 m/s) + // 1000 meters in 4 seconds = 250 m/s + when(mockLocation2.getTime()).thenReturn(5000L); // 4 seconds later + when(mockLocation2.hasAccuracy()).thenReturn(true); + when(mockLocation2.getAccuracy()).thenReturn(15.0f); // Good accuracy + when(mockLocation2.distanceTo(mockLocation1)).thenReturn(1000.0f); + + // Should NOT accept airplane-like movement (exceeds threshold of 120 m/s) + assertFalse(LocationAccuracyFilter.shouldAcceptLocation(mockLocation2)); + + // Reset for next test + LocationAccuracyFilter.reset(); + LocationAccuracyFilter.shouldAcceptLocation(mockLocation1); + + // Test case 2: High-speed train (300 km/h = 83.3 m/s) + // 833 meters in 10 seconds = 83.3 m/s + when(mockLocation2.getTime()).thenReturn(11000L); // 10 seconds later + when(mockLocation2.hasAccuracy()).thenReturn(true); + when(mockLocation2.getAccuracy()).thenReturn(15.0f); // Good accuracy + when(mockLocation2.distanceTo(mockLocation1)).thenReturn(833.0f); + + // Should accept high-speed train movement + assertTrue(LocationAccuracyFilter.shouldAcceptLocation(mockLocation2)); + + // Reset for next test + LocationAccuracyFilter.reset(); + LocationAccuracyFilter.shouldAcceptLocation(mockLocation1); + + // Test case 3: Fast car on highway (120 km/h = 33.3 m/s) + // 333 meters in 10 seconds = 33.3 m/s + when(mockLocation2.getTime()).thenReturn(11000L); // 10 seconds later + when(mockLocation2.hasAccuracy()).thenReturn(true); + when(mockLocation2.getAccuracy()).thenReturn(15.0f); // Good accuracy + when(mockLocation2.distanceTo(mockLocation1)).thenReturn(333.0f); + + // Should accept fast car movement + assertTrue(LocationAccuracyFilter.shouldAcceptLocation(mockLocation2)); + } + + @Test + public void shouldAcceptLocation_whenFilterDisabled_alwaysReturnsTrue() { + // Arrange + // Disable filtering + LocationAccuracyFilter.setFilterEnabled(false); + + // Set up first location + when(mockLocation1.getTime()).thenReturn(1000L); + when(mockLocation1.hasAccuracy()).thenReturn(true); + when(mockLocation1.getAccuracy()).thenReturn(10.0f); + LocationAccuracyFilter.shouldAcceptLocation(mockLocation1); + + // Set up second location that would normally be filtered (unrealistic speed) + when(mockLocation2.getTime()).thenReturn(2000L); + when(mockLocation2.hasAccuracy()).thenReturn(true); + when(mockLocation2.getAccuracy()).thenReturn(10.0f); + when(mockLocation2.distanceTo(mockLocation1)).thenReturn(5000.0f); // 5000m in 1s = 5000 m/s + + // Act & Assert - Should be accepted because filtering is disabled + assertTrue(LocationAccuracyFilter.shouldAcceptLocation(mockLocation2)); + + // Make sure to enable filtering again for other tests + LocationAccuracyFilter.setFilterEnabled(true); + } +} \ No newline at end of file diff --git a/geolocator_android/example/lib/main.dart b/geolocator_android/example/lib/main.dart index 3934a102..3b7b57e2 100644 --- a/geolocator_android/example/lib/main.dart +++ b/geolocator_android/example/lib/main.dart @@ -9,7 +9,8 @@ import 'package:geolocator_platform_interface/geolocator_platform_interface.dart /// Defines the main theme color. final MaterialColor themeMaterialColor = BaseflowPluginExample.createMaterialColor( - const Color.fromRGBO(48, 49, 60, 1)); + const Color.fromRGBO(48, 49, 60, 1), +); void main() { runApp(const GeolocatorWidget()); @@ -18,14 +19,14 @@ void main() { /// Example [Widget] showing the functionalities of the geolocator plugin. class GeolocatorWidget extends StatefulWidget { /// Creates a [PermissionHandlerWidget]. - const GeolocatorWidget({ - super.key, - }); + const GeolocatorWidget({super.key}); /// Create a page containing the functionality of this plugin static ExamplePage createPage() { return ExamplePage( - Icons.location_on, (context) => const GeolocatorWidget()); + Icons.location_on, + (context) => const GeolocatorWidget(), + ); } @override @@ -78,109 +79,99 @@ class _GeolocatorWidgetState extends State { } }, itemBuilder: (context) => [ - const PopupMenuItem( - value: 1, - child: Text("Get Location Accuracy"), - ), + const PopupMenuItem(value: 1, child: Text("Get Location Accuracy")), if (Platform.isIOS) const PopupMenuItem( value: 2, child: Text("Request Temporary Full Accuracy"), ), - const PopupMenuItem( - value: 3, - child: Text("Open App Settings"), - ), + const PopupMenuItem(value: 3, child: Text("Open App Settings")), if (Platform.isAndroid) const PopupMenuItem( value: 4, child: Text("Open Location Settings"), ), - const PopupMenuItem( - value: 5, - child: Text("Clear"), - ), + const PopupMenuItem(value: 5, child: Text("Clear")), ], ); } @override Widget build(BuildContext context) { - const sizedBox = SizedBox( - height: 10, - ); + const sizedBox = SizedBox(height: 10); return BaseflowPluginExample( - pluginName: 'Geolocator', - githubURL: 'https://github.com/Baseflow/flutter-geolocator', - pubDevURL: 'https://pub.dev/packages/geolocator', - appBarActions: [ - _createActions() - ], - pages: [ - ExamplePage( - Icons.location_on, - (context) => Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - body: ListView.builder( - itemCount: _positionItems.length, - itemBuilder: (context, index) { - final positionItem = _positionItems[index]; - - if (positionItem.type == _PositionItemType.log) { - return ListTile( - title: Text(positionItem.displayValue, - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - )), - ); - } else { - return Card( - child: ListTile( - tileColor: themeMaterialColor, - title: Text( - positionItem.displayValue, - style: const TextStyle(color: Colors.white), - ), + pluginName: 'Geolocator', + githubURL: 'https://github.com/Baseflow/flutter-geolocator', + pubDevURL: 'https://pub.dev/packages/geolocator', + appBarActions: [_createActions()], + pages: [ + ExamplePage( + Icons.location_on, + (context) => Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + body: ListView.builder( + itemCount: _positionItems.length, + itemBuilder: (context, index) { + final positionItem = _positionItems[index]; + + if (positionItem.type == _PositionItemType.log) { + return ListTile( + title: Text( + positionItem.displayValue, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ); + } else { + return Card( + child: ListTile( + tileColor: themeMaterialColor, + title: Text( + positionItem.displayValue, + style: const TextStyle(color: Colors.white), ), - ); - } - }, - ), - floatingActionButton: Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FloatingActionButton( - onPressed: _toggleListening, - tooltip: (_positionStreamSubscription == null) - ? 'Start position updates' - : _positionStreamSubscription!.isPaused - ? 'Resume' - : 'Pause', - backgroundColor: _determineButtonColor(), - child: (_positionStreamSubscription == null || - _positionStreamSubscription!.isPaused) - ? const Icon(Icons.play_arrow) - : const Icon(Icons.pause), - ), - sizedBox, - FloatingActionButton( - onPressed: _getCurrentPosition, - child: const Icon(Icons.my_location), - ), - sizedBox, - FloatingActionButton( - onPressed: _getLastKnownPosition, - child: const Icon(Icons.bookmark), - ), - ], - ), + ), + ); + } + }, ), - ) - ]); + floatingActionButton: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FloatingActionButton( + onPressed: _toggleListening, + tooltip: (_positionStreamSubscription == null) + ? 'Start position updates' + : _positionStreamSubscription!.isPaused + ? 'Resume' + : 'Pause', + backgroundColor: _determineButtonColor(), + child: (_positionStreamSubscription == null || + _positionStreamSubscription!.isPaused) + ? const Icon(Icons.play_arrow) + : const Icon(Icons.pause), + ), + sizedBox, + FloatingActionButton( + onPressed: _getCurrentPosition, + child: const Icon(Icons.my_location), + ), + sizedBox, + FloatingActionButton( + onPressed: _getLastKnownPosition, + child: const Icon(Icons.bookmark), + ), + ], + ), + ), + ), + ], + ); } Future _getCurrentPosition() async { @@ -191,10 +182,7 @@ class _GeolocatorWidgetState extends State { } final position = await geolocatorAndroid.getCurrentPosition(); - _updatePositionList( - _PositionItemType.position, - position.toString(), - ); + _updatePositionList(_PositionItemType.position, position.toString()); } Future _handlePermission() async { @@ -224,10 +212,7 @@ class _GeolocatorWidgetState extends State { // Android's shouldShowRequestPermissionRationale // returned true. According to Android guidelines // your App should show an explanatory UI now. - _updatePositionList( - _PositionItemType.log, - _kPermissionDeniedMessage, - ); + _updatePositionList(_PositionItemType.log, _kPermissionDeniedMessage); return false; } @@ -245,10 +230,7 @@ class _GeolocatorWidgetState extends State { // When we reach here, permissions are granted and we can // continue accessing the position of the device. - _updatePositionList( - _PositionItemType.log, - _kPermissionGrantedMessage, - ); + _updatePositionList(_PositionItemType.log, _kPermissionGrantedMessage); return true; } @@ -313,7 +295,8 @@ class _GeolocatorWidgetState extends State { ), ); final positionStream = geolocatorAndroid.getPositionStream( - locationSettings: androidSettings); + locationSettings: androidSettings, + ); _positionStreamSubscription = positionStream.handleError((error) { _positionStreamSubscription?.cancel(); _positionStreamSubscription = null; @@ -361,10 +344,7 @@ class _GeolocatorWidgetState extends State { void _getLastKnownPosition() async { final position = await geolocatorAndroid.getLastKnownPosition(); if (position != null) { - _updatePositionList( - _PositionItemType.position, - position.toString(), - ); + _updatePositionList(_PositionItemType.position, position.toString()); } else { _updatePositionList( _PositionItemType.log, @@ -410,10 +390,7 @@ class _GeolocatorWidgetState extends State { displayValue = 'Error opening Application Settings.'; } - _updatePositionList( - _PositionItemType.log, - displayValue, - ); + _updatePositionList(_PositionItemType.log, displayValue); } void _openLocationSettings() async { @@ -426,17 +403,11 @@ class _GeolocatorWidgetState extends State { displayValue = 'Error opening Location Settings'; } - _updatePositionList( - _PositionItemType.log, - displayValue, - ); + _updatePositionList(_PositionItemType.log, displayValue); } } -enum _PositionItemType { - log, - position, -} +enum _PositionItemType { log, position } class _PositionItem { _PositionItem(this.type, this.displayValue); diff --git a/geolocator_android/lib/src/geolocator_android.dart b/geolocator_android/lib/src/geolocator_android.dart index 04c1ced8..00dd4d11 100644 --- a/geolocator_android/lib/src/geolocator_android.dart +++ b/geolocator_android/lib/src/geolocator_android.dart @@ -8,18 +8,21 @@ import 'package:uuid/uuid.dart'; /// An implementation of [GeolocatorPlatform] that uses method channels. class GeolocatorAndroid extends GeolocatorPlatform { /// The method channel used to interact with the native platform. - static const _methodChannel = - MethodChannel('flutter.baseflow.com/geolocator_android'); + static const _methodChannel = MethodChannel( + 'flutter.baseflow.com/geolocator_android', + ); /// The event channel used to receive [Position] updates from the native /// platform. - static const _eventChannel = - EventChannel('flutter.baseflow.com/geolocator_updates_android'); + static const _eventChannel = EventChannel( + 'flutter.baseflow.com/geolocator_updates_android', + ); /// The event channel used to receive [LocationServiceStatus] updates from the /// native platform. - static const _serviceStatusEventChannel = - EventChannel('flutter.baseflow.com/geolocator_service_updates_android'); + static const _serviceStatusEventChannel = EventChannel( + 'flutter.baseflow.com/geolocator_service_updates_android', + ); /// Registers this class as the default instance of [GeolocatorPlatform]. static void registerWith() { @@ -41,8 +44,9 @@ class GeolocatorAndroid extends GeolocatorPlatform { Future checkPermission() async { try { // ignore: omit_local_variable_types - final int permission = - await _methodChannel.invokeMethod('checkPermission'); + final int permission = await _methodChannel.invokeMethod( + 'checkPermission', + ); return permission.toLocationPermission(); } on PlatformException catch (e) { @@ -56,8 +60,9 @@ class GeolocatorAndroid extends GeolocatorPlatform { Future requestPermission() async { try { // ignore: omit_local_variable_types - final int permission = - await _methodChannel.invokeMethod('requestPermission'); + final int permission = await _methodChannel.invokeMethod( + 'requestPermission', + ); return permission.toLocationPermission(); } on PlatformException catch (e) { @@ -81,8 +86,10 @@ class GeolocatorAndroid extends GeolocatorPlatform { 'forceLocationManager': forceLocationManager, }; - final positionMap = - await _methodChannel.invokeMethod('getLastKnownPosition', parameters); + final positionMap = await _methodChannel.invokeMethod( + 'getLastKnownPosition', + parameters, + ); return positionMap != null ? AndroidPosition.fromMap(positionMap) : null; } on PlatformException catch (e) { @@ -94,8 +101,9 @@ class GeolocatorAndroid extends GeolocatorPlatform { @override Future getLocationAccuracy() async { - final int accuracy = - await _methodChannel.invokeMethod('getLocationAccuracy'); + final int accuracy = await _methodChannel.invokeMethod( + 'getLocationAccuracy', + ); return LocationAccuracyStatus.values[accuracy]; } @@ -111,13 +119,10 @@ class GeolocatorAndroid extends GeolocatorPlatform { final Duration? timeLimit = locationSettings?.timeLimit; - positionFuture = _methodChannel.invokeMethod( - 'getCurrentPosition', - { - ...?locationSettings?.toJson(), - 'requestId': requestId, - }, - ); + positionFuture = _methodChannel.invokeMethod('getCurrentPosition', { + ...?locationSettings?.toJson(), + 'requestId': requestId, + }); if (timeLimit != null) { positionFuture = positionFuture.timeout(timeLimit); @@ -126,13 +131,8 @@ class GeolocatorAndroid extends GeolocatorPlatform { final positionMap = await positionFuture; return AndroidPosition.fromMap(positionMap); } on TimeoutException { - final parameters = { - 'requestId': requestId, - }; - _methodChannel.invokeMethod( - 'cancelGetCurrentPosition', - parameters, - ); + final parameters = {'requestId': requestId}; + _methodChannel.invokeMethod('cancelGetCurrentPosition', parameters); rethrow; } on PlatformException catch (e) { final error = _handlePlatformException(e); @@ -163,9 +163,7 @@ class GeolocatorAndroid extends GeolocatorPlatform { } @override - Stream getPositionStream({ - LocationSettings? locationSettings, - }) { + Stream getPositionStream({LocationSettings? locationSettings}) { if (_positionStream != null) { return _positionStream!; } @@ -181,34 +179,38 @@ class GeolocatorAndroid extends GeolocatorPlatform { timeLimit, onTimeout: (s) { _positionStream = null; - s.addError(TimeoutException( - 'Time limit reached while waiting for position update.', - timeLimit, - )); + s.addError( + TimeoutException( + 'Time limit reached while waiting for position update.', + timeLimit, + ), + ); s.close(); }, ); } _positionStream = positionStream - .map((dynamic element) => - AndroidPosition.fromMap(element.cast())) - .handleError( - (error) { - if (error is PlatformException) { - error = _handlePlatformException(error); - } - throw error; - }, - ); + .map( + (dynamic element) => + AndroidPosition.fromMap(element.cast()), + ) + .handleError((error) { + if (error is PlatformException) { + error = _handlePlatformException(error); + } + throw error; + }); return _positionStream!; } Stream _wrapStream(Stream incoming) { - return incoming.asBroadcastStream(onCancel: (subscription) { - subscription.cancel(); - _positionStream = null; - }); + return incoming.asBroadcastStream( + onCancel: (subscription) { + subscription.cancel(); + _positionStream = null; + }, + ); } @override @@ -218,9 +220,7 @@ class GeolocatorAndroid extends GeolocatorPlatform { try { final int status = await _methodChannel.invokeMethod( 'requestTemporaryFullAccuracy', - { - 'purposeKey': purposeKey, - }, + {'purposeKey': purposeKey}, ); return LocationAccuracyStatus.values[status]; } on PlatformException catch (e) { diff --git a/geolocator_android/lib/src/types/android_settings.dart b/geolocator_android/lib/src/types/android_settings.dart index 0cd39923..cff08b51 100644 --- a/geolocator_android/lib/src/types/android_settings.dart +++ b/geolocator_android/lib/src/types/android_settings.dart @@ -9,6 +9,7 @@ class AndroidSettings extends LocationSettings { /// /// The following default values are used: /// - forceLocationManager: false + /// - enableAccuracyFilter: false AndroidSettings({ this.forceLocationManager = false, super.accuracy, @@ -17,6 +18,7 @@ class AndroidSettings extends LocationSettings { super.timeLimit, this.foregroundNotificationConfig, this.useMSLAltitude = false, + this.enableAccuracyFilter = false, }); /// Forces the Geolocator plugin to use the legacy LocationManager instead of @@ -71,6 +73,21 @@ class AndroidSettings extends LocationSettings { /// Defaults to false final bool useMSLAltitude; + /// Enables filtering for inaccurate GPS positions that might cause random GPS drift. + /// + /// When enabled, the plugin will filter out location updates that are physically implausible + /// based on speed, distance jumps, and accuracy thresholds. This is useful for applications + /// that require smooth location tracking without sudden jumps that can occur due to GPS + /// inaccuracies. + /// + /// The filter uses the following criteria to filter out problematic positions: + /// - Locations with very poor accuracy (> 300 meters) + /// - Unrealistically high speeds (> 280 m/s or ~1000 km/h) + /// - Large position jumps combined with poor accuracy + /// + /// Defaults to false + final bool enableAccuracyFilter; + @override Map toJson() { return super.toJson() @@ -79,6 +96,7 @@ class AndroidSettings extends LocationSettings { 'timeInterval': intervalDuration?.inMilliseconds, 'foregroundNotificationConfig': foregroundNotificationConfig?.toJson(), 'useMSLAltitude': useMSLAltitude, + 'enableAccuracyFilter': enableAccuracyFilter, }); } } diff --git a/geolocator_android/lib/src/types/foreground_settings.dart b/geolocator_android/lib/src/types/foreground_settings.dart index 22983e09..76f810bf 100644 --- a/geolocator_android/lib/src/types/foreground_settings.dart +++ b/geolocator_android/lib/src/types/foreground_settings.dart @@ -11,17 +11,11 @@ class AndroidResource { final String defType; /// Uniquely identifies an Android resource. - const AndroidResource({ - required this.name, - this.defType = 'drawable', - }); + const AndroidResource({required this.name, this.defType = 'drawable'}); /// Returns a JSON representation of this class. Map toJson() { - return { - 'name': name, - 'defType': defType, - }; + return {'name': name, 'defType': defType}; } } @@ -53,8 +47,10 @@ class ForegroundNotificationConfig { required this.notificationTitle, required this.notificationText, this.notificationChannelName = 'Background Location', - this.notificationIcon = - const AndroidResource(name: 'ic_launcher', defType: 'mipmap'), + this.notificationIcon = const AndroidResource( + name: 'ic_launcher', + defType: 'mipmap', + ), this.enableWifiLock = false, this.enableWakeLock = false, this.setOngoing = false, diff --git a/geolocator_android/test/event_channel_mock.dart b/geolocator_android/test/event_channel_mock.dart index afe77e6a..9ef874de 100644 --- a/geolocator_android/test/event_channel_mock.dart +++ b/geolocator_android/test/event_channel_mock.dart @@ -10,10 +10,8 @@ class EventChannelMock { Stream? stream; StreamSubscription? _streamSubscription; - EventChannelMock({ - required String channelName, - required this.stream, - }) : _methodChannel = MethodChannel(channelName) { + EventChannelMock({required String channelName, required this.stream}) + : _methodChannel = MethodChannel(channelName) { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(_methodChannel, _handler); } @@ -64,8 +62,11 @@ class EventChannelMock { details = error.details; } - final envelope = const StandardMethodCodec() - .encodeErrorEnvelope(code: code, message: message, details: details); + final envelope = const StandardMethodCodec().encodeErrorEnvelope( + code: code, + message: message, + details: details, + ); _sendEnvelope(envelope); } @@ -77,10 +78,6 @@ class EventChannelMock { void _sendEnvelope(ByteData? envelope) { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .handlePlatformMessage( - _methodChannel.name, - envelope, - (_) {}, - ); + .handlePlatformMessage(_methodChannel.name, envelope, (_) {}); } } diff --git a/geolocator_android/test/geolocator_android_test.dart b/geolocator_android/test/geolocator_android_test.dart index 67c8a60c..337fbe44 100644 --- a/geolocator_android/test/geolocator_android_test.dart +++ b/geolocator_android/test/geolocator_android_test.dart @@ -10,22 +10,20 @@ import 'event_channel_mock.dart'; import 'method_channel_mock.dart'; Position get mockPosition => AndroidPosition( - latitude: 52.561270, - longitude: 5.639382, - timestamp: DateTime.fromMillisecondsSinceEpoch( - 500, - isUtc: true, - ), - altitude: 3000.0, - altitudeAccuracy: 0.0, - satelliteCount: 2.0, - satellitesUsedInFix: 2.0, - accuracy: 0.0, - heading: 0.0, - headingAccuracy: 0.0, - speed: 0.0, - speedAccuracy: 0.0, - isMocked: false); + latitude: 52.561270, + longitude: 5.639382, + timestamp: DateTime.fromMillisecondsSinceEpoch(500, isUtc: true), + altitude: 3000.0, + altitudeAccuracy: 0.0, + satelliteCount: 2.0, + satellitesUsedInFix: 2.0, + accuracy: 0.0, + heading: 0.0, + headingAccuracy: 0.0, + speed: 0.0, + speedAccuracy: 0.0, + isMocked: false, + ); void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -37,29 +35,27 @@ void main() { group('checkPermission: When checking for permission', () { test( - // ignore: lines_longer_than_80_chars - 'Should receive whenInUse if permission is granted when App is in use', - () async { - // Arrange - MethodChannelMock( - channelName: 'flutter.baseflow.com/geolocator_android', - methods: [ - MethodMock( - methodName: 'checkPermission', - result: LocationPermission.whileInUse.index, - ), - ], - ); + // ignore: lines_longer_than_80_chars + 'Should receive whenInUse if permission is granted when App is in use', + () async { + // Arrange + MethodChannelMock( + channelName: 'flutter.baseflow.com/geolocator_android', + methods: [ + MethodMock( + methodName: 'checkPermission', + result: LocationPermission.whileInUse.index, + ), + ], + ); - // Act - final permission = await GeolocatorAndroid().checkPermission(); + // Act + final permission = await GeolocatorAndroid().checkPermission(); - // Assert - expect( - permission, - LocationPermission.whileInUse, - ); - }); + // Assert + expect(permission, LocationPermission.whileInUse); + }, + ); test('Should receive always if permission is granted always', () async { // Arrange @@ -77,10 +73,7 @@ void main() { final permission = await GeolocatorAndroid().checkPermission(); // Assert - expect( - permission, - LocationPermission.always, - ); + expect(permission, LocationPermission.always); }); test('Should receive denied if permission is denied', () async { @@ -99,67 +92,65 @@ void main() { final permission = await GeolocatorAndroid().checkPermission(); // Assert - expect( - permission, - LocationPermission.denied, - ); + expect(permission, LocationPermission.denied); }); - test('Should receive deniedForEver if permission is denied for ever', - () async { - // Arrange - MethodChannelMock( - channelName: 'flutter.baseflow.com/geolocator_android', - methods: [ - MethodMock( - methodName: 'checkPermission', - result: LocationPermission.deniedForever.index, - ), - ], - ); + test( + 'Should receive deniedForEver if permission is denied for ever', + () async { + // Arrange + MethodChannelMock( + channelName: 'flutter.baseflow.com/geolocator_android', + methods: [ + MethodMock( + methodName: 'checkPermission', + result: LocationPermission.deniedForever.index, + ), + ], + ); - // Act - final permission = await GeolocatorAndroid().checkPermission(); + // Act + final permission = await GeolocatorAndroid().checkPermission(); - // Assert - expect( - permission, - LocationPermission.deniedForever, - ); - }); + // Assert + expect(permission, LocationPermission.deniedForever); + }, + ); - test('Should receive an exception when permission definitions not found', - () async { - // Arrange - MethodChannelMock( - channelName: 'flutter.baseflow.com/geolocator_android', - methods: [ - MethodMock( - methodName: 'checkPermission', - result: PlatformException( - code: 'PERMISSION_DEFINITIONS_NOT_FOUND', - message: 'Permission definitions are not found.', - details: null, + test( + 'Should receive an exception when permission definitions not found', + () async { + // Arrange + MethodChannelMock( + channelName: 'flutter.baseflow.com/geolocator_android', + methods: [ + MethodMock( + methodName: 'checkPermission', + result: PlatformException( + code: 'PERMISSION_DEFINITIONS_NOT_FOUND', + message: 'Permission definitions are not found.', + details: null, + ), ), - ), - ], - ); + ], + ); - // Act - final permissionFuture = GeolocatorAndroid().checkPermission(); + // Act + final permissionFuture = GeolocatorAndroid().checkPermission(); - // Assert - expect( - permissionFuture, - throwsA( - isA().having( - (e) => e.message, - 'description', - 'Permission definitions are not found.', + // Assert + expect( + permissionFuture, + throwsA( + isA().having( + (e) => e.message, + 'description', + 'Permission definitions are not found.', + ), ), - ), - ); - }); + ); + }, + ); }); group( @@ -172,10 +163,7 @@ void main() { final methodChannel = MethodChannelMock( channelName: 'flutter.baseflow.com/geolocator_android', methods: const [ - MethodMock( - methodName: 'requestTemporaryFullAccuracy', - result: 0, - ), + MethodMock(methodName: 'requestTemporaryFullAccuracy', result: 0), ], ); @@ -206,126 +194,125 @@ void main() { MethodChannelMock( channelName: 'flutter.baseflow.com/geolocator_android', methods: const [ - MethodMock( - methodName: 'requestTemporaryFullAccuracy', - result: 1, - ), + MethodMock(methodName: 'requestTemporaryFullAccuracy', result: 1), ], ); // Act - final accuracy = await GeolocatorAndroid() - .requestTemporaryFullAccuracy(purposeKey: 'purposeKey'); + final accuracy = await GeolocatorAndroid().requestTemporaryFullAccuracy( + purposeKey: 'purposeKey', + ); // Assert expect(accuracy, LocationAccuracyStatus.precise); }); - test('Should receive an exception when permission definitions not found', - () async { - // Arrange - MethodChannelMock( - channelName: 'flutter.baseflow.com/geolocator_android', - methods: [ - MethodMock( - methodName: 'requestTemporaryFullAccuracy', - result: PlatformException( - code: 'PERMISSION_DEFINITIONS_NOT_FOUND', - message: 'Permission definitions are not found.', - details: null, + test( + 'Should receive an exception when permission definitions not found', + () async { + // Arrange + MethodChannelMock( + channelName: 'flutter.baseflow.com/geolocator_android', + methods: [ + MethodMock( + methodName: 'requestTemporaryFullAccuracy', + result: PlatformException( + code: 'PERMISSION_DEFINITIONS_NOT_FOUND', + message: 'Permission definitions are not found.', + details: null, + ), ), - ), - ], - ); + ], + ); - // Act - final future = GeolocatorAndroid() - .requestTemporaryFullAccuracy(purposeKey: 'purposeKey'); + // Act + final future = GeolocatorAndroid().requestTemporaryFullAccuracy( + purposeKey: 'purposeKey', + ); - // Assert - expect( - future, - throwsA( - isA().having( - (e) => e.message, - 'description', - 'Permission definitions are not found.', + // Assert + expect( + future, + throwsA( + isA().having( + (e) => e.message, + 'description', + 'Permission definitions are not found.', + ), ), - ), - ); - }); + ); + }, + ); }); - group('getLocationAccuracy: When requesting the Location Accuracy Status', - () { - test('Should receive reduced accuracy if Location Accuracy is reduced', + group( + 'getLocationAccuracy: When requesting the Location Accuracy Status', + () { + test( + 'Should receive reduced accuracy if Location Accuracy is reduced', () async { - // Arrange - MethodChannelMock( - channelName: 'flutter.baseflow.com/geolocator_android', - methods: const [ - MethodMock( - methodName: 'getLocationAccuracy', - result: 0, - ), - ], - ); + // Arrange + MethodChannelMock( + channelName: 'flutter.baseflow.com/geolocator_android', + methods: const [ + MethodMock(methodName: 'getLocationAccuracy', result: 0), + ], + ); - // Act - final locationAccuracy = - await GeolocatorAndroid().getLocationAccuracy(); + // Act + final locationAccuracy = + await GeolocatorAndroid().getLocationAccuracy(); - // Assert - expect(locationAccuracy, LocationAccuracyStatus.reduced); - }); + // Assert + expect(locationAccuracy, LocationAccuracyStatus.reduced); + }, + ); - test('Should receive reduced accuracy if Location Accuracy is reduced', + test( + 'Should receive reduced accuracy if Location Accuracy is reduced', () async { - // Arrange - MethodChannelMock( - channelName: 'flutter.baseflow.com/geolocator_android', - methods: const [ - MethodMock( - methodName: 'getLocationAccuracy', - result: 1, - ), - ], - ); + // Arrange + MethodChannelMock( + channelName: 'flutter.baseflow.com/geolocator_android', + methods: const [ + MethodMock(methodName: 'getLocationAccuracy', result: 1), + ], + ); - // Act - final locationAccuracy = - await GeolocatorAndroid().getLocationAccuracy(); + // Act + final locationAccuracy = + await GeolocatorAndroid().getLocationAccuracy(); - // Assert - expect(locationAccuracy, LocationAccuracyStatus.precise); - }); - }); + // Assert + expect(locationAccuracy, LocationAccuracyStatus.precise); + }, + ); + }, + ); group('requestPermission: When requesting for permission', () { test( - // ignore: lines_longer_than_80_chars - 'Should receive whenInUse if permission is granted when App is in use', - () async { - // Arrange - MethodChannelMock( - channelName: 'flutter.baseflow.com/geolocator_android', - methods: [ - MethodMock( - methodName: 'requestPermission', - result: LocationPermission.whileInUse.index, - ), - ], - ); + // ignore: lines_longer_than_80_chars + 'Should receive whenInUse if permission is granted when App is in use', + () async { + // Arrange + MethodChannelMock( + channelName: 'flutter.baseflow.com/geolocator_android', + methods: [ + MethodMock( + methodName: 'requestPermission', + result: LocationPermission.whileInUse.index, + ), + ], + ); - // Act - final permission = await GeolocatorAndroid().requestPermission(); + // Act + final permission = await GeolocatorAndroid().requestPermission(); - // Assert - expect( - permission, - LocationPermission.whileInUse, - ); - }); + // Assert + expect(permission, LocationPermission.whileInUse); + }, + ); test('Should receive always if permission is granted always', () async { // Arrange @@ -343,10 +330,7 @@ void main() { final permission = await GeolocatorAndroid().requestPermission(); // Assert - expect( - permission, - LocationPermission.always, - ); + expect(permission, LocationPermission.always); }); test('Should receive denied if permission is denied', () async { @@ -357,7 +341,7 @@ void main() { MethodMock( methodName: 'requestPermission', result: LocationPermission.denied.index, - ) + ), ], ); @@ -365,183 +349,181 @@ void main() { final permission = await GeolocatorAndroid().requestPermission(); // Assert - expect( - permission, - LocationPermission.denied, - ); + expect(permission, LocationPermission.denied); }); - test('Should receive deniedForever if permission is denied for ever', - () async { - // Arrange - MethodChannelMock( - channelName: 'flutter.baseflow.com/geolocator_android', - methods: [ - MethodMock( - methodName: 'requestPermission', - result: LocationPermission.deniedForever.index, - ), - ], - ); + test( + 'Should receive deniedForever if permission is denied for ever', + () async { + // Arrange + MethodChannelMock( + channelName: 'flutter.baseflow.com/geolocator_android', + methods: [ + MethodMock( + methodName: 'requestPermission', + result: LocationPermission.deniedForever.index, + ), + ], + ); - // Act - final permission = await GeolocatorAndroid().requestPermission(); + // Act + final permission = await GeolocatorAndroid().requestPermission(); - // Assert - expect( - permission, - LocationPermission.deniedForever, - ); - }); + // Assert + expect(permission, LocationPermission.deniedForever); + }, + ); - test('Should receive an exception when already requesting permission', - () async { - // Arrange - MethodChannelMock( - channelName: 'flutter.baseflow.com/geolocator_android', - methods: [ - MethodMock( - methodName: 'requestPermission', - result: PlatformException( - code: "PERMISSION_REQUEST_IN_PROGRESS", - message: "Permissions already being requested.", - details: null, + test( + 'Should receive an exception when already requesting permission', + () async { + // Arrange + MethodChannelMock( + channelName: 'flutter.baseflow.com/geolocator_android', + methods: [ + MethodMock( + methodName: 'requestPermission', + result: PlatformException( + code: "PERMISSION_REQUEST_IN_PROGRESS", + message: "Permissions already being requested.", + details: null, + ), ), - ), - ], - ); + ], + ); - // Act - final permissionFuture = GeolocatorAndroid().requestPermission(); + // Act + final permissionFuture = GeolocatorAndroid().requestPermission(); - // Assert - expect( - permissionFuture, - throwsA( - isA().having( - (e) => e.message, - 'description', - 'Permissions already being requested.', + // Assert + expect( + permissionFuture, + throwsA( + isA().having( + (e) => e.message, + 'description', + 'Permissions already being requested.', + ), ), - ), - ); - }); + ); + }, + ); - test('Should receive an exception when permission definitions not found', - () async { - // Arrange - MethodChannelMock( - channelName: 'flutter.baseflow.com/geolocator_android', - methods: [ - MethodMock( - methodName: 'requestPermission', - result: PlatformException( - code: 'PERMISSION_DEFINITIONS_NOT_FOUND', - message: 'Permission definitions are not found.', - details: null, + test( + 'Should receive an exception when permission definitions not found', + () async { + // Arrange + MethodChannelMock( + channelName: 'flutter.baseflow.com/geolocator_android', + methods: [ + MethodMock( + methodName: 'requestPermission', + result: PlatformException( + code: 'PERMISSION_DEFINITIONS_NOT_FOUND', + message: 'Permission definitions are not found.', + details: null, + ), ), - ), - ], - ); + ], + ); - // Act - final permissionFuture = GeolocatorAndroid().requestPermission(); + // Act + final permissionFuture = GeolocatorAndroid().requestPermission(); - // Assert - expect( - permissionFuture, - throwsA( - isA().having( - (e) => e.message, - 'description', - 'Permission definitions are not found.', + // Assert + expect( + permissionFuture, + throwsA( + isA().having( + (e) => e.message, + 'description', + 'Permission definitions are not found.', + ), ), - ), - ); - }); + ); + }, + ); - test('Should receive an exception when android activity is missing', - () async { - // Arrange - MethodChannelMock( - channelName: 'flutter.baseflow.com/geolocator_android', - methods: [ - MethodMock( - methodName: 'requestPermission', - result: PlatformException( - code: 'ACTIVITY_MISSING', - message: 'Activity is missing.', - details: null, + test( + 'Should receive an exception when android activity is missing', + () async { + // Arrange + MethodChannelMock( + channelName: 'flutter.baseflow.com/geolocator_android', + methods: [ + MethodMock( + methodName: 'requestPermission', + result: PlatformException( + code: 'ACTIVITY_MISSING', + message: 'Activity is missing.', + details: null, + ), ), - ), - ], - ); + ], + ); - // Act - final permissionFuture = GeolocatorAndroid().requestPermission(); + // Act + final permissionFuture = GeolocatorAndroid().requestPermission(); - // Assert - expect( - permissionFuture, - throwsA( - isA().having( - (e) => e.message, - 'description', - 'Activity is missing.', + // Assert + expect( + permissionFuture, + throwsA( + isA().having( + (e) => e.message, + 'description', + 'Activity is missing.', + ), ), - ), - ); - }); + ); + }, + ); }); - group('isLocationServiceEnabled: When checking the location service status', - () { - test('Should receive true if location services are enabled', () async { - // Arrange - MethodChannelMock( - channelName: 'flutter.baseflow.com/geolocator_android', - methods: const [ - MethodMock( - methodName: 'isLocationServiceEnabled', - result: true, - ), - ], - ); + group( + 'isLocationServiceEnabled: When checking the location service status', + () { + test('Should receive true if location services are enabled', () async { + // Arrange + MethodChannelMock( + channelName: 'flutter.baseflow.com/geolocator_android', + methods: const [ + MethodMock(methodName: 'isLocationServiceEnabled', result: true), + ], + ); - // Act - final isLocationServiceEnabled = - await GeolocatorAndroid().isLocationServiceEnabled(); + // Act + final isLocationServiceEnabled = + await GeolocatorAndroid().isLocationServiceEnabled(); - // Assert - expect( - isLocationServiceEnabled, - true, - ); - }); + // Assert + expect(isLocationServiceEnabled, true); + }); - test('Should receive false if location services are disabled', () async { - // Arrange - MethodChannelMock( - channelName: 'flutter.baseflow.com/geolocator_android', - methods: const [ - MethodMock( - methodName: 'isLocationServiceEnabled', - result: false, - ), - ], - ); + test( + 'Should receive false if location services are disabled', + () async { + // Arrange + MethodChannelMock( + channelName: 'flutter.baseflow.com/geolocator_android', + methods: const [ + MethodMock( + methodName: 'isLocationServiceEnabled', + result: false, + ), + ], + ); - // Act - final isLocationServiceEnabled = - await GeolocatorAndroid().isLocationServiceEnabled(); + // Act + final isLocationServiceEnabled = + await GeolocatorAndroid().isLocationServiceEnabled(); - // Assert - expect( - isLocationServiceEnabled, - false, + // Assert + expect(isLocationServiceEnabled, false); + }, ); - }); - }); + }, + ); group('getLastKnownPosition: When requesting the last know position', () { test('Should receive a position if permissions are granted', () async { @@ -568,10 +550,7 @@ void main() { // Arrange expect(position, mockPosition); expect(methodChannel.log, [ - isMethodCall( - 'getLastKnownPosition', - arguments: expectedArguments, - ), + isMethodCall('getLastKnownPosition', arguments: expectedArguments), ]); }); @@ -624,8 +603,9 @@ void main() { ); const requestId = 'requestId'; - const locationSettings = - LocationSettings(accuracy: LocationAccuracy.low); + const locationSettings = LocationSettings( + accuracy: LocationAccuracy.low, + ); final expectedArguments = { ...locationSettings.toJson(), @@ -641,10 +621,7 @@ void main() { // Assert expect(position, mockPosition); expect(channel.log, [ - isMethodCall( - 'getCurrentPosition', - arguments: expectedArguments, - ), + isMethodCall('getCurrentPosition', arguments: expectedArguments), ]); }); @@ -663,10 +640,12 @@ void main() { const requestIdFirst = 'requestIdFirst'; const requestIdSecond = 'requestIdSecond'; - const locationSettingsFirst = - LocationSettings(accuracy: LocationAccuracy.low); - const locationSettingsSecond = - LocationSettings(accuracy: LocationAccuracy.high); + const locationSettingsFirst = LocationSettings( + accuracy: LocationAccuracy.low, + ); + const locationSettingsSecond = LocationSettings( + accuracy: LocationAccuracy.high, + ); final expectedFirstArguments = { ...locationSettingsFirst.toJson(), @@ -692,10 +671,7 @@ void main() { expect(firstPosition, mockPosition); expect(secondPosition, mockPosition); expect(channel.log, [ - isMethodCall( - 'getCurrentPosition', - arguments: expectedFirstArguments, - ), + isMethodCall('getCurrentPosition', arguments: expectedFirstArguments), isMethodCall( 'getCurrentPosition', arguments: expectedSecondArguments, @@ -703,97 +679,97 @@ void main() { ]); }); - test('Should throw a permission denied exception if permission is denied', - () async { - // Arrange - MethodChannelMock( - channelName: 'flutter.baseflow.com/geolocator_android', - methods: [ - MethodMock( - methodName: 'getCurrentPosition', - result: PlatformException( - code: 'PERMISSION_DENIED', - message: 'Permission denied', - details: null, + test( + 'Should throw a permission denied exception if permission is denied', + () async { + // Arrange + MethodChannelMock( + channelName: 'flutter.baseflow.com/geolocator_android', + methods: [ + MethodMock( + methodName: 'getCurrentPosition', + result: PlatformException( + code: 'PERMISSION_DENIED', + message: 'Permission denied', + details: null, + ), ), - ), - ], - ); + ], + ); - // Act - final future = GeolocatorAndroid().getCurrentPosition(); + // Act + final future = GeolocatorAndroid().getCurrentPosition(); - // Assert - expect( - future, - throwsA( - isA().having( - (e) => e.message, - 'message', - 'Permission denied', + // Assert + expect( + future, + throwsA( + isA().having( + (e) => e.message, + 'message', + 'Permission denied', + ), ), - ), - ); - }); + ); + }, + ); test( - // ignore: lines_longer_than_80_chars - 'Should throw a location service disabled exception if location services are disabled', - () async { - // Arrange - MethodChannelMock( - channelName: 'flutter.baseflow.com/geolocator_android', - methods: [ - MethodMock( - methodName: 'getCurrentPosition', - result: PlatformException( - code: 'LOCATION_SERVICES_DISABLED', - message: '', - details: null, + // ignore: lines_longer_than_80_chars + 'Should throw a location service disabled exception if location services are disabled', + () async { + // Arrange + MethodChannelMock( + channelName: 'flutter.baseflow.com/geolocator_android', + methods: [ + MethodMock( + methodName: 'getCurrentPosition', + result: PlatformException( + code: 'LOCATION_SERVICES_DISABLED', + message: '', + details: null, + ), ), - ), - ], - ); + ], + ); - // Act - final future = GeolocatorAndroid().getCurrentPosition(); + // Act + final future = GeolocatorAndroid().getCurrentPosition(); - // Assert - expect( - future, - throwsA(isA()), - ); - }); - - test('Should throw a timeout exception when timeLimit is reached', - () async { - // Arrange - MethodChannelMock( - channelName: 'flutter.baseflow.com/geolocator_android', - methods: [ - MethodMock( - methodName: 'getCurrentPosition', - delay: const Duration(milliseconds: 10), - result: mockPosition.toJson(), - ), - const MethodMock( - methodName: 'cancelGetCurrentPosition', - ), - ], - ); + // Assert + expect(future, throwsA(isA())); + }, + ); - try { - await GeolocatorAndroid().getCurrentPosition( - locationSettings: const LocationSettings( - timeLimit: Duration(milliseconds: 5), - ), + test( + 'Should throw a timeout exception when timeLimit is reached', + () async { + // Arrange + MethodChannelMock( + channelName: 'flutter.baseflow.com/geolocator_android', + methods: [ + MethodMock( + methodName: 'getCurrentPosition', + delay: const Duration(milliseconds: 10), + result: mockPosition.toJson(), + ), + const MethodMock(methodName: 'cancelGetCurrentPosition'), + ], ); - fail('Expected a TimeoutException and should not reach here.'); - } on TimeoutException catch (e) { - expect(e, isA()); - } - }); + try { + await GeolocatorAndroid().getCurrentPosition( + locationSettings: const LocationSettings( + timeLimit: Duration(milliseconds: 5), + ), + ); + + fail('Expected a TimeoutException and should not reach here.'); + } on TimeoutException catch (e) { + expect(e, isA()); + } + }, + ); test('Should cancel location stream when timeLimit is reached', () async { // Arrange @@ -805,9 +781,7 @@ void main() { methodName: 'getCurrentPosition', delay: Duration(milliseconds: 10), ), - MethodMock( - methodName: cancelMethodName, - ), + MethodMock(methodName: cancelMethodName), ], ); @@ -834,46 +808,45 @@ void main() { final firstStream = plugin.getPositionStream(); final secondStream = plugin.getPositionStream(); - expect( - identical(firstStream, secondStream), - true, - ); + expect(identical(firstStream, secondStream), true); }); - test('Should return a new stream when all subscriptions are cancelled', - () { - final plugin = GeolocatorAndroid(); + test( + 'Should return a new stream when all subscriptions are cancelled', + () { + final plugin = GeolocatorAndroid(); - // Get two position streams - final firstStream = plugin.getPositionStream(); - final secondStream = plugin.getPositionStream(); + // Get two position streams + final firstStream = plugin.getPositionStream(); + final secondStream = plugin.getPositionStream(); - // Streams are the same object - expect(firstStream == secondStream, true); + // Streams are the same object + expect(firstStream == secondStream, true); - // Add multiple subscriptions - StreamSubscription? firstSubscription = - firstStream.listen((event) {}); - StreamSubscription? secondSubscription = - secondStream.listen((event) {}); + // Add multiple subscriptions + StreamSubscription? firstSubscription = + firstStream.listen((event) {}); + StreamSubscription? secondSubscription = + secondStream.listen((event) {}); - // Cancel first subscription - firstSubscription.cancel(); - firstSubscription = null; + // Cancel first subscription + firstSubscription.cancel(); + firstSubscription = null; - // Stream is still the same as the first one - final cachedStream = plugin.getPositionStream(); - expect(firstStream == cachedStream, true); + // Stream is still the same as the first one + final cachedStream = plugin.getPositionStream(); + expect(firstStream == cachedStream, true); - // Cancel second subscription - secondSubscription.cancel(); - secondSubscription = null; + // Cancel second subscription + secondSubscription.cancel(); + secondSubscription = null; - // After all listeners have been removed, the next stream - // retrieved is a new one. - final thirdStream = plugin.getPositionStream(); - expect(firstStream != thirdStream, true); - }); + // After all listeners have been removed, the next stream + // retrieved is a new one. + final thirdStream = plugin.getPositionStream(); + expect(firstStream != thirdStream, true); + }, + ); }); test('PositionStream can be listened to and can be canceled', () { @@ -886,9 +859,11 @@ void main() { ); var stream = GeolocatorAndroid().getPositionStream( - locationSettings: AndroidSettings(useMSLAltitude: false)); - StreamSubscription? streamSubscription = - stream.listen((event) {}); + locationSettings: AndroidSettings(useMSLAltitude: false), + ); + StreamSubscription? streamSubscription = stream.listen( + (event) {}, + ); streamSubscription.pause(); expect(streamSubscription.isPaused, true); @@ -899,91 +874,101 @@ void main() { }); test( - // ignore: lines_longer_than_80_chars - 'Should correctly handle done event', () async { - // Arrange - final completer = Completer(); - completer.future.timeout(const Duration(milliseconds: 50), - onTimeout: () => - fail('getPositionStream should trigger done and not timeout.')); - final streamController = - StreamController>.broadcast(); - EventChannelMock( - channelName: 'flutter.baseflow.com/geolocator_updates_android', - stream: streamController.stream, - ); + // ignore: lines_longer_than_80_chars + 'Should correctly handle done event', + () async { + // Arrange + final completer = Completer(); + completer.future.timeout( + const Duration(milliseconds: 50), + onTimeout: () => fail( + 'getPositionStream should trigger done and not timeout.', + ), + ); + final streamController = + StreamController>.broadcast(); + EventChannelMock( + channelName: 'flutter.baseflow.com/geolocator_updates_android', + stream: streamController.stream, + ); - // Act - GeolocatorAndroid().getPositionStream().listen( - (event) {}, - onDone: completer.complete, - ); + // Act + GeolocatorAndroid().getPositionStream().listen( + (event) {}, + onDone: completer.complete, + ); - await streamController.close(); + await streamController.close(); - //Assert - await completer.future; - }); + //Assert + await completer.future; + }, + ); test( - // ignore: lines_longer_than_80_chars - 'Should receive a stream with position updates if permissions are granted', - () async { - // Arrange - final streamController = - StreamController>.broadcast(); - EventChannelMock( - channelName: 'flutter.baseflow.com/geolocator_updates_android', - stream: streamController.stream, - ); + // ignore: lines_longer_than_80_chars + 'Should receive a stream with position updates if permissions are granted', + () async { + // Arrange + final streamController = + StreamController>.broadcast(); + EventChannelMock( + channelName: 'flutter.baseflow.com/geolocator_updates_android', + stream: streamController.stream, + ); - // Act - final positionStream = GeolocatorAndroid().getPositionStream( - locationSettings: AndroidSettings(useMSLAltitude: false)); - final streamQueue = StreamQueue(positionStream); + // Act + final positionStream = GeolocatorAndroid().getPositionStream( + locationSettings: AndroidSettings(useMSLAltitude: false), + ); + final streamQueue = StreamQueue(positionStream); - // Emit test events - streamController.add(mockPosition.toJson()); - streamController.add(mockPosition.toJson()); - streamController.add(mockPosition.toJson()); + // Emit test events + streamController.add(mockPosition.toJson()); + streamController.add(mockPosition.toJson()); + streamController.add(mockPosition.toJson()); - // Assert - expect(await streamQueue.next, mockPosition); - expect(await streamQueue.next, mockPosition); - expect(await streamQueue.next, mockPosition); + // Assert + expect(await streamQueue.next, mockPosition); + expect(await streamQueue.next, mockPosition); + expect(await streamQueue.next, mockPosition); - // Clean up - await streamQueue.cancel(); - await streamController.close(); - }); + // Clean up + await streamQueue.cancel(); + await streamController.close(); + }, + ); test( - // ignore: lines_longer_than_80_chars - 'Should continue listening to the stream when exception is thrown ', - () async { - // Arrange - final streamController = - StreamController>.broadcast(); - EventChannelMock( - channelName: 'flutter.baseflow.com/geolocator_updates_android', - stream: streamController.stream, - ); - - // Act - final positionStream = GeolocatorAndroid().getPositionStream(); - final streamQueue = StreamQueue(positionStream); + // ignore: lines_longer_than_80_chars + 'Should continue listening to the stream when exception is thrown ', + () async { + // Arrange + final streamController = + StreamController>.broadcast(); + EventChannelMock( + channelName: 'flutter.baseflow.com/geolocator_updates_android', + stream: streamController.stream, + ); - // Emit test events - streamController.add(mockPosition.toJson()); - streamController.addError(PlatformException( - code: 'PERMISSION_DENIED', - message: 'Permission denied', - details: null)); - streamController.add(mockPosition.toJson()); + // Act + final positionStream = GeolocatorAndroid().getPositionStream(); + final streamQueue = StreamQueue(positionStream); + + // Emit test events + streamController.add(mockPosition.toJson()); + streamController.addError( + PlatformException( + code: 'PERMISSION_DENIED', + message: 'Permission denied', + details: null, + ), + ); + streamController.add(mockPosition.toJson()); - // Assert - expect(await streamQueue.next, mockPosition); - expect( + // Assert + expect(await streamQueue.next, mockPosition); + expect( streamQueue.next, throwsA( isA().having( @@ -991,38 +976,43 @@ void main() { 'message', 'Permission denied', ), - )); - expect(await streamQueue.next, mockPosition); + ), + ); + expect(await streamQueue.next, mockPosition); - // Clean up - await streamQueue.cancel(); - await streamController.close(); - }); + // Clean up + await streamQueue.cancel(); + await streamController.close(); + }, + ); test( - // ignore: lines_longer_than_80_chars - 'Should receive a permission denied exception if permission is denied', - () async { - // Arrange - final streamController = - StreamController.broadcast(); - EventChannelMock( - channelName: 'flutter.baseflow.com/geolocator_updates_android', - stream: streamController.stream, - ); + // ignore: lines_longer_than_80_chars + 'Should receive a permission denied exception if permission is denied', + () async { + // Arrange + final streamController = + StreamController.broadcast(); + EventChannelMock( + channelName: 'flutter.baseflow.com/geolocator_updates_android', + stream: streamController.stream, + ); - // Act - final positionStream = GeolocatorAndroid().getPositionStream(); - final streamQueue = StreamQueue(positionStream); + // Act + final positionStream = GeolocatorAndroid().getPositionStream(); + final streamQueue = StreamQueue(positionStream); - // Emit test error - streamController.addError(PlatformException( - code: 'PERMISSION_DENIED', - message: 'Permission denied', - details: null)); + // Emit test error + streamController.addError( + PlatformException( + code: 'PERMISSION_DENIED', + message: 'Permission denied', + details: null, + ), + ); - // Assert - expect( + // Assert + expect( streamQueue.next, throwsA( isA().having( @@ -1030,176 +1020,189 @@ void main() { 'message', 'Permission denied', ), - )); + ), + ); - // Clean up - streamQueue.cancel(); - streamController.close(); - }); + // Clean up + streamQueue.cancel(); + streamController.close(); + }, + ); test( - // ignore: lines_longer_than_80_chars - 'Should receive a location service disabled exception if location service is disabled', - () async { - // Arrange - final streamController = - StreamController.broadcast(); - EventChannelMock( - channelName: 'flutter.baseflow.com/geolocator_updates_android', - stream: streamController.stream, - ); + // ignore: lines_longer_than_80_chars + 'Should receive a location service disabled exception if location service is disabled', + () async { + // Arrange + final streamController = + StreamController.broadcast(); + EventChannelMock( + channelName: 'flutter.baseflow.com/geolocator_updates_android', + stream: streamController.stream, + ); - // Act - final positionStream = GeolocatorAndroid().getPositionStream(); - final streamQueue = StreamQueue(positionStream); + // Act + final positionStream = GeolocatorAndroid().getPositionStream(); + final streamQueue = StreamQueue(positionStream); - // Emit test error - streamController.addError(PlatformException( - code: 'LOCATION_SERVICES_DISABLED', - message: 'Location services disabled', - details: null)); + // Emit test error + streamController.addError( + PlatformException( + code: 'LOCATION_SERVICES_DISABLED', + message: 'Location services disabled', + details: null, + ), + ); - // Assert - expect( + // Assert + expect( streamQueue.next, - throwsA( - isA(), - )); + throwsA(isA()), + ); - // Clean up - streamQueue.cancel(); - streamController.close(); - }); + // Clean up + streamQueue.cancel(); + streamController.close(); + }, + ); test( - // ignore: lines_longer_than_80_chars - 'Should receive a already subscribed exception', () async { - // Arrange - final streamController = - StreamController.broadcast(); - EventChannelMock( - channelName: 'flutter.baseflow.com/geolocator_updates_android', - stream: streamController.stream, - ); + // ignore: lines_longer_than_80_chars + 'Should receive a already subscribed exception', + () async { + // Arrange + final streamController = + StreamController.broadcast(); + EventChannelMock( + channelName: 'flutter.baseflow.com/geolocator_updates_android', + stream: streamController.stream, + ); - // Act - final positionStream = GeolocatorAndroid().getPositionStream(); - final streamQueue = StreamQueue(positionStream); + // Act + final positionStream = GeolocatorAndroid().getPositionStream(); + final streamQueue = StreamQueue(positionStream); - // Emit test error - streamController.addError(PlatformException( - code: 'PERMISSION_REQUEST_IN_PROGRESS', - message: 'A permission request is already in progress', - details: null)); + // Emit test error + streamController.addError( + PlatformException( + code: 'PERMISSION_REQUEST_IN_PROGRESS', + message: 'A permission request is already in progress', + details: null, + ), + ); - // Assert - expect( + // Assert + expect( streamQueue.next, - throwsA( - isA(), - )); + throwsA(isA()), + ); - // Clean up - streamQueue.cancel(); - streamController.close(); - }); + // Clean up + streamQueue.cancel(); + streamController.close(); + }, + ); test( - // ignore: lines_longer_than_80_chars - 'Should receive a already subscribed exception', () async { - // Arrange - final streamController = - StreamController.broadcast(); - EventChannelMock( - channelName: 'flutter.baseflow.com/geolocator_updates_android', - stream: streamController.stream, - ); + // ignore: lines_longer_than_80_chars + 'Should receive a already subscribed exception', + () async { + // Arrange + final streamController = + StreamController.broadcast(); + EventChannelMock( + channelName: 'flutter.baseflow.com/geolocator_updates_android', + stream: streamController.stream, + ); - // Act - final positionStream = GeolocatorAndroid().getPositionStream(); - final streamQueue = StreamQueue(positionStream); + // Act + final positionStream = GeolocatorAndroid().getPositionStream(); + final streamQueue = StreamQueue(positionStream); - // Emit test error - streamController.addError(PlatformException( - code: 'LOCATION_SUBSCRIPTION_ACTIVE', - message: 'Already subscribed to receive a position stream', - details: null)); + // Emit test error + streamController.addError( + PlatformException( + code: 'LOCATION_SUBSCRIPTION_ACTIVE', + message: 'Already subscribed to receive a position stream', + details: null, + ), + ); - // Assert - expect( - streamQueue.next, - throwsA( - isA(), - )); + // Assert + expect(streamQueue.next, throwsA(isA())); - // Clean up - streamQueue.cancel(); - streamController.close(); - }); + // Clean up + streamQueue.cancel(); + streamController.close(); + }, + ); test( - // ignore: lines_longer_than_80_chars - 'Should receive a position update exception', () async { - // Arrange - final streamController = - StreamController.broadcast(); - EventChannelMock( - channelName: 'flutter.baseflow.com/geolocator_updates_android', - stream: streamController.stream, - ); + // ignore: lines_longer_than_80_chars + 'Should receive a position update exception', + () async { + // Arrange + final streamController = + StreamController.broadcast(); + EventChannelMock( + channelName: 'flutter.baseflow.com/geolocator_updates_android', + stream: streamController.stream, + ); - // Act - final positionStream = GeolocatorAndroid().getPositionStream(); - final streamQueue = StreamQueue(positionStream); + // Act + final positionStream = GeolocatorAndroid().getPositionStream(); + final streamQueue = StreamQueue(positionStream); - // Emit test error - streamController.addError(PlatformException( - code: 'LOCATION_UPDATE_FAILURE', - message: 'A permission request is already in progress', - details: null)); + // Emit test error + streamController.addError( + PlatformException( + code: 'LOCATION_UPDATE_FAILURE', + message: 'A permission request is already in progress', + details: null, + ), + ); - // Assert - expect( - streamQueue.next, - throwsA( - isA(), - )); + // Assert + expect(streamQueue.next, throwsA(isA())); - // Clean up - streamQueue.cancel(); - streamController.close(); - }); + // Clean up + streamQueue.cancel(); + streamController.close(); + }, + ); - test('Should throw a timeout exception when timeLimit is reached', - () async { - // Arrange - final streamController = StreamController>(); - EventChannelMock( - channelName: 'flutter.baseflow.com/geolocator_updates_android', - stream: streamController.stream, - ); - const expectedArguments = LocationSettings( - accuracy: LocationAccuracy.low, - distanceFilter: 0, - ); + test( + 'Should throw a timeout exception when timeLimit is reached', + () async { + // Arrange + final streamController = StreamController>(); + EventChannelMock( + channelName: 'flutter.baseflow.com/geolocator_updates_android', + stream: streamController.stream, + ); + const expectedArguments = LocationSettings( + accuracy: LocationAccuracy.low, + distanceFilter: 0, + ); - // Act - final positionStream = GeolocatorAndroid().getPositionStream( - locationSettings: LocationSettings( - accuracy: expectedArguments.accuracy, - timeLimit: const Duration(milliseconds: 5), - ), - ); - final streamQueue = StreamQueue(positionStream); + // Act + final positionStream = GeolocatorAndroid().getPositionStream( + locationSettings: LocationSettings( + accuracy: expectedArguments.accuracy, + timeLimit: const Duration(milliseconds: 5), + ), + ); + final streamQueue = StreamQueue(positionStream); - streamController.add(mockPosition.toJson()); + streamController.add(mockPosition.toJson()); - await Future.delayed(const Duration(milliseconds: 5)); + await Future.delayed(const Duration(milliseconds: 5)); - // Assert - expect(await streamQueue.next, mockPosition); - expect(streamQueue.next, throwsA(isA())); - }); + // Assert + expect(await streamQueue.next, mockPosition); + expect(streamQueue.next, throwsA(isA())); + }, + ); test('Should cleanup the previous stream on timeout exception', () async { // Arrange @@ -1249,91 +1252,98 @@ void main() { }); group( - // ignore: lines_longer_than_80_chars - 'getServiceStream: When requesting a stream of location service status updates', - () { - group('And requesting for location service status updates multiple times', + // ignore: lines_longer_than_80_chars + 'getServiceStream: When requesting a stream of location service status updates', + () { + group( + 'And requesting for location service status updates multiple times', () { - test('Should return the same stream', () { - final plugin = GeolocatorAndroid(); - final firstStream = plugin.getServiceStatusStream(); - final secondstream = plugin.getServiceStatusStream(); + test('Should return the same stream', () { + final plugin = GeolocatorAndroid(); + final firstStream = plugin.getServiceStatusStream(); + final secondstream = plugin.getServiceStatusStream(); - expect( - identical(firstStream, secondstream), - true, - ); - }); - }); + expect(identical(firstStream, secondstream), true); + }); + }, + ); - test( + test( // ignore: lines_longer_than_80_chars 'Should receive a stream with location service updates if permissions are granted', () async { - // Arrange - final streamController = StreamController.broadcast(); - EventChannelMock( - channelName: - 'flutter.baseflow.com/geolocator_service_updates_android', - stream: streamController.stream); + // Arrange + final streamController = StreamController.broadcast(); + EventChannelMock( + channelName: + 'flutter.baseflow.com/geolocator_service_updates_android', + stream: streamController.stream, + ); - // Act - final locationServiceStream = - GeolocatorAndroid().getServiceStatusStream(); - final streamQueue = StreamQueue(locationServiceStream); + // Act + final locationServiceStream = + GeolocatorAndroid().getServiceStatusStream(); + final streamQueue = StreamQueue(locationServiceStream); - // Emit test events - streamController.add(0); // disabled value in native enum - streamController.add(1); // enabled value in native enum + // Emit test events + streamController.add(0); // disabled value in native enum + streamController.add(1); // enabled value in native enum - //Assert - expect(await streamQueue.next, ServiceStatus.disabled); - expect(await streamQueue.next, ServiceStatus.enabled); + //Assert + expect(await streamQueue.next, ServiceStatus.disabled); + expect(await streamQueue.next, ServiceStatus.enabled); - // Clean up - await streamQueue.cancel(); - await streamController.close(); - }); + // Clean up + await streamQueue.cancel(); + await streamController.close(); + }, + ); - test( + test( // ignore: lines_longer_than_80_chars 'Should receive an exception if android activity is missing', () async { - // Arrange - final streamController = - StreamController.broadcast(); - EventChannelMock( - channelName: - 'flutter.baseflow.com/geolocator_service_updates_android', - stream: streamController.stream, - ); + // Arrange + final streamController = + StreamController.broadcast(); + EventChannelMock( + channelName: + 'flutter.baseflow.com/geolocator_service_updates_android', + stream: streamController.stream, + ); - // Act - final positionStream = GeolocatorAndroid().getServiceStatusStream(); - final streamQueue = StreamQueue(positionStream); + // Act + final positionStream = GeolocatorAndroid().getServiceStatusStream(); + final streamQueue = StreamQueue(positionStream); - // Emit test error - streamController.addError(PlatformException( - code: 'ACTIVITY_MISSING', - message: 'Activity missing', - details: null)); + // Emit test error + streamController.addError( + PlatformException( + code: 'ACTIVITY_MISSING', + message: 'Activity missing', + details: null, + ), + ); - // Assert - expect( - streamQueue.next, - throwsA( - isA().having( - (e) => e.message, - 'message', - 'Activity missing', + // Assert + expect( + streamQueue.next, + throwsA( + isA().having( + (e) => e.message, + 'message', + 'Activity missing', + ), ), - )); + ); - // Clean up - streamQueue.cancel(); - streamController.close(); - }); - }); + // Clean up + streamQueue.cancel(); + streamController.close(); + }, + ); + }, + ); group('openAppSettings: When opening the App settings', () { test('Should receive true if the page can be opened', () async { @@ -1341,10 +1351,7 @@ void main() { MethodChannelMock( channelName: 'flutter.baseflow.com/geolocator_android', methods: const [ - MethodMock( - methodName: 'openAppSettings', - result: true, - ), + MethodMock(methodName: 'openAppSettings', result: true), ], ); @@ -1353,10 +1360,7 @@ void main() { await GeolocatorAndroid().openAppSettings(); // Assert - expect( - hasOpenedAppSettings, - true, - ); + expect(hasOpenedAppSettings, true); }); test('Should receive false if an error occurred', () async { @@ -1364,10 +1368,7 @@ void main() { MethodChannelMock( channelName: 'flutter.baseflow.com/geolocator_android', methods: const [ - MethodMock( - methodName: 'openAppSettings', - result: false, - ), + MethodMock(methodName: 'openAppSettings', result: false), ], ); @@ -1376,10 +1377,7 @@ void main() { await GeolocatorAndroid().openAppSettings(); // Assert - expect( - hasOpenedAppSettings, - false, - ); + expect(hasOpenedAppSettings, false); }); }); @@ -1389,10 +1387,7 @@ void main() { MethodChannelMock( channelName: 'flutter.baseflow.com/geolocator_android', methods: const [ - MethodMock( - methodName: 'openLocationSettings', - result: true, - ), + MethodMock(methodName: 'openLocationSettings', result: true), ], ); @@ -1401,10 +1396,7 @@ void main() { await GeolocatorAndroid().openLocationSettings(); // Assert - expect( - hasOpenedLocationSettings, - true, - ); + expect(hasOpenedLocationSettings, true); }); test('Should receive false if an error occurred', () async { @@ -1412,10 +1404,7 @@ void main() { MethodChannelMock( channelName: 'flutter.baseflow.com/geolocator_android', methods: const [ - MethodMock( - methodName: 'openLocationSettings', - result: false, - ), + MethodMock(methodName: 'openLocationSettings', result: false), ], ); @@ -1424,106 +1413,93 @@ void main() { await GeolocatorAndroid().openLocationSettings(); // Assert - expect( - hasOpenedLocationSettings, - false, - ); + expect(hasOpenedLocationSettings, false); }); }); group('jsonSerialization: When serializing to json', () { - test('Should produce valid map with all the settings when calling toJson', - () async { - // Arrange - final settings = AndroidSettings( - accuracy: LocationAccuracy.best, - distanceFilter: 5, - forceLocationManager: false, - intervalDuration: const Duration(seconds: 1), - timeLimit: const Duration(seconds: 1), - useMSLAltitude: false, - foregroundNotificationConfig: const ForegroundNotificationConfig( - color: Colors.amber, - enableWakeLock: false, - enableWifiLock: false, - notificationIcon: AndroidResource( - name: 'name', - defType: 'defType', + test( + 'Should produce valid map with all the settings when calling toJson', + () async { + // Arrange + final settings = AndroidSettings( + accuracy: LocationAccuracy.best, + distanceFilter: 5, + forceLocationManager: false, + intervalDuration: const Duration(seconds: 1), + timeLimit: const Duration(seconds: 1), + useMSLAltitude: false, + foregroundNotificationConfig: const ForegroundNotificationConfig( + color: Colors.amber, + enableWakeLock: false, + enableWifiLock: false, + notificationIcon: AndroidResource( + name: 'name', + defType: 'defType', + ), + notificationText: 'text', + notificationTitle: 'title', + setOngoing: true, ), - notificationText: 'text', - notificationTitle: 'title', - setOngoing: true, - ), - ); + ); - // Act - final jsonMap = settings.toJson(); + // Act + final jsonMap = settings.toJson(); - // Assert - expect( - jsonMap['accuracy'], - settings.accuracy.index, - ); - expect( - jsonMap['distanceFilter'], - settings.distanceFilter, - ); - expect( - jsonMap['forceLocationManager'], - settings.forceLocationManager, - ); - expect( - jsonMap['timeInterval'], - settings.intervalDuration!.inMilliseconds, - ); - expect( - jsonMap['useMSLAltitude'], - settings.useMSLAltitude, - ); - expect( - jsonMap['foregroundNotificationConfig']['enableWakeLock'], - settings.foregroundNotificationConfig!.enableWakeLock, - ); - expect( - jsonMap['foregroundNotificationConfig']['enableWifiLock'], - settings.foregroundNotificationConfig!.enableWifiLock, - ); - expect( - jsonMap['foregroundNotificationConfig']['notificationIcon']['name'], - settings.foregroundNotificationConfig!.notificationIcon.name, - ); - expect( - jsonMap['foregroundNotificationConfig']['notificationIcon'] - ['defType'], - settings.foregroundNotificationConfig!.notificationIcon.defType, - ); - expect( - jsonMap['foregroundNotificationConfig']['notificationText'], - settings.foregroundNotificationConfig!.notificationText, - ); - expect( - jsonMap['foregroundNotificationConfig']['notificationTitle'], - settings.foregroundNotificationConfig!.notificationTitle, - ); - expect( - jsonMap['foregroundNotificationConfig']['setOngoing'], - settings.foregroundNotificationConfig!.setOngoing, - ); - expect( - jsonMap['foregroundNotificationConfig']['color'], - settings.foregroundNotificationConfig!.color!.toARGB32, - ); - }); + // Assert + expect(jsonMap['accuracy'], settings.accuracy.index); + expect(jsonMap['distanceFilter'], settings.distanceFilter); + expect( + jsonMap['forceLocationManager'], + settings.forceLocationManager, + ); + expect( + jsonMap['timeInterval'], + settings.intervalDuration!.inMilliseconds, + ); + expect(jsonMap['useMSLAltitude'], settings.useMSLAltitude); + expect( + jsonMap['foregroundNotificationConfig']['enableWakeLock'], + settings.foregroundNotificationConfig!.enableWakeLock, + ); + expect( + jsonMap['foregroundNotificationConfig']['enableWifiLock'], + settings.foregroundNotificationConfig!.enableWifiLock, + ); + expect( + jsonMap['foregroundNotificationConfig']['notificationIcon']['name'], + settings.foregroundNotificationConfig!.notificationIcon.name, + ); + expect( + jsonMap['foregroundNotificationConfig']['notificationIcon'] + ['defType'], + settings.foregroundNotificationConfig!.notificationIcon.defType, + ); + expect( + jsonMap['foregroundNotificationConfig']['notificationText'], + settings.foregroundNotificationConfig!.notificationText, + ); + expect( + jsonMap['foregroundNotificationConfig']['notificationTitle'], + settings.foregroundNotificationConfig!.notificationTitle, + ); + expect( + jsonMap['foregroundNotificationConfig']['setOngoing'], + settings.foregroundNotificationConfig!.setOngoing, + ); + expect( + jsonMap['foregroundNotificationConfig']['color'], + settings.foregroundNotificationConfig!.color!.toARGB32, + ); + }, + ); test('Should receive false if an error occurred', () async { // Arrange MethodChannelMock( channelName: 'flutter.baseflow.com/geolocator_android', methods: const [ - MethodMock( - methodName: 'openLocationSettings', - result: false, - ), + MethodMock(methodName: 'openLocationSettings', result: false), ], ); @@ -1532,10 +1508,7 @@ void main() { await GeolocatorAndroid().openLocationSettings(); // Assert - expect( - hasOpenedLocationSettings, - false, - ); + expect(hasOpenedLocationSettings, false); }); }); }); diff --git a/geolocator_android/test/method_channel_mock.dart b/geolocator_android/test/method_channel_mock.dart index 1043a5ce..549663e3 100644 --- a/geolocator_android/test/method_channel_mock.dart +++ b/geolocator_android/test/method_channel_mock.dart @@ -18,10 +18,8 @@ class MethodChannelMock { final MethodChannel methodChannel; final log = []; - MethodChannelMock({ - required String channelName, - required this.methods, - }) : methodChannel = MethodChannel(channelName) { + MethodChannelMock({required String channelName, required this.methods}) + : methodChannel = MethodChannel(channelName) { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(methodChannel, _handler); }