Baseflow / flutter-geolocator

Android and iOS Geolocation plugin for Flutter
https://baseflow.com/
MIT License
1.24k stars 656 forks source link

[Bug]: getPositionStream causes exception when run on device without GMS (using default location manager) #1401

Closed vanelizarov closed 10 months ago

vanelizarov commented 10 months ago

Please check the following before submitting a new issue.

Please select affected platform(s)

Steps to reproduce

Run any example with getPositonStream on AOSP Android emulator

Expected results

No exception

Actual results

getPositionStream causes an java.lang.IllegalStateException: passive location requests must have an explicit minimum update interval The problem is that minUpdateIntervalMillis property in location request is implicitly set to -1 by default

Code sample

Code sample ```dart Geolocator.getPositionStream( locationSettings: AndroidSettings( accuracy: LocationAccuracy.lowest, timeLimit: const Duration(minutes: 1), ), ).listen((pos) => debugPrint('${pos}')) ```

Screenshots or video

Screenshots or video demonstration [Upload media here]

Version

10.1.0

Flutter Doctor output

Doctor output ```console [✓] Flutter (Channel stable, 3.16.5, on macOS 14.2 23C64 darwin-arm64, locale en-US) • Flutter version 3.16.5 on channel stable at /Users/vanelizarov/flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision 78666c8dc5 (12 days ago), 2023-12-19 16:14:14 -0800 • Engine revision 3f3e560236 • Dart version 3.2.3 • DevTools version 2.28.4 [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0) • Android SDK at /Users/vanelizarov/Library/Android/sdk • Platform android-34, build-tools 34.0.0 • ANDROID_HOME = /Users/vanelizarov/Library/Android/sdk • ANDROID_SDK_ROOT = /Users/vanelizarov/Library/Android/sdk • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 17.0.7+0-17.0.7b1000.6-10550314) • All Android licenses accepted. [✓] Xcode - develop for iOS and macOS (Xcode 15.1) • Xcode at /Applications/Xcode.app/Contents/Developer • Build 15C65 • CocoaPods version 1.14.3 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 2023.1) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 17.0.7+0-17.0.7b1000.6-10550314) [✓] VS Code (version 1.85.1) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.80.0 [✓] Connected device (4 available) • Android SDK built for arm64 (mobile) • emulator-5554 • android-arm64 • Android 12 (API 31) (emulator) • macOS (desktop) • macos • darwin-arm64 • macOS 14.2 23C64 darwin-arm64 • Chrome (web) • chrome • web-javascript • Google Chrome 120.0.6099.129 [✓] Network resources • All expected network resources are available. • No issues found! ```
TimHoogstrate commented 10 months ago

Dear @vanelizarov,

I tried running your example on a GMS-less emulator but I am unable to reproduce this issue? Do you have the same issue when you run the example app?

Kind regards,

vanelizarov commented 10 months ago

@TimHoogstrate

Yes, it's reproducible on AOSP-based API 31 (Android 12) image. You can run example from geolocator_android replacing AndroidSettings instance on line 285 with one I provided above and then click on red "play" button

System image image
Error log ```shell W/GooglePlayServicesUtil( 4277): com.baseflow.geolocator_example requires the Google Play Store, but it is missing. E/FlutterGeolocator( 4277): Geolocator position updates started W/GooglePlayServicesUtil( 4277): com.baseflow.geolocator_example requires the Google Play Store, but it is missing. E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): Failed to open event stream E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): java.lang.IllegalStateException: passive location requests must have an explicit minimum update interval E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at androidx.core.util.Preconditions.checkState(Preconditions.java:169) E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at androidx.core.location.LocationRequestCompat$Builder.build(LocationRequestCompat.java:488) E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at com.baseflow.geolocator.location.LocationManagerClient.startPositionUpdates(LocationManagerClient.java:186) E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at com.baseflow.geolocator.location.GeolocationManager.startPositionUpdates(GeolocationManager.java:54) E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at com.baseflow.geolocator.StreamHandlerImpl.onListen(StreamHandlerImpl.java:137) E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at io.flutter.plugin.common.EventChannel$IncomingStreamRequestHandler.onListen(EventChannel.java:218) E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at io.flutter.plugin.common.EventChannel$IncomingStreamRequestHandler.onMessage(EventChannel.java:197) E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at io.flutter.embedding.engine.dart.DartMessenger.invokeHandler(DartMessenger.java:295) E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at io.flutter.embedding.engine.dart.DartMessenger.lambda$dispatchMessageToQueue$0$io-flutter-embedding-engine-dart-DartMessenger(DartMessenger.java:322) E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at io.flutter.embedding.engine.dart.DartMessenger$$ExternalSyntheticLambda0.run(Unknown Source:12) E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at android.os.Handler.handleCallback(Handler.java:938) E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at android.os.Handler.dispatchMessage(Handler.java:99) E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at android.os.Looper.loopOnce(Looper.java:201) E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at android.os.Looper.loop(Looper.java:288) E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at android.app.ActivityThread.main(ActivityThread.java:7839) E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at java.lang.reflect.Method.invoke(Native Method) E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) E/EventChannel#flutter.baseflow.com/geolocator_updates_android( 4277): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003) D/EGL_emulation( 4277): app_time_stats: avg=3383.37ms min=11.17ms max=6755.57ms count=2 ```
Full example code ```dart import 'dart:async'; import 'dart:io' show Platform; import 'package:baseflow_plugin_template/baseflow_plugin_template.dart'; import 'package:flutter/material.dart'; import 'package:geolocator_android/geolocator_android.dart'; 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)); void main() { runApp(const GeolocatorWidget()); } /// Example [Widget] showing the functionalities of the geolocator plugin. class GeolocatorWidget extends StatefulWidget { /// Creates a new GeolocatorWidget. const GeolocatorWidget({Key? key}) : super(key: key); /// Utility method to create a page with the Baseflow templating. static ExamplePage createPage() { return ExamplePage(Icons.location_on, (context) => const GeolocatorWidget()); } @override _GeolocatorWidgetState createState() => _GeolocatorWidgetState(); } class _GeolocatorWidgetState extends State { static const String _kLocationServicesDisabledMessage = 'Location services are disabled.'; static const String _kPermissionDeniedMessage = 'Permission denied.'; static const String _kPermissionDeniedForeverMessage = 'Permission denied forever.'; static const String _kPermissionGrantedMessage = 'Permission granted.'; final GeolocatorPlatform geolocatorAndroid = GeolocatorPlatform.instance; final List<_PositionItem> _positionItems = <_PositionItem>[]; StreamSubscription? _positionStreamSubscription; StreamSubscription? _serviceStatusStreamSubscription; @override void initState() { super.initState(); _toggleServiceStatusStream(); } PopupMenuButton _createActions() { return PopupMenuButton( elevation: 40, onSelected: (value) async { switch (value) { case 1: _getLocationAccuracy(); break; case 2: _requestTemporaryFullAccuracy(); break; case 3: _openAppSettings(); break; case 4: _openLocationSettings(); break; case 5: setState(_positionItems.clear); break; default: break; } }, itemBuilder: (context) => [ const PopupMenuItem( child: Text("Get Location Accuracy"), value: 1, ), if (Platform.isIOS) const PopupMenuItem( child: Text("Request Temporary Full Accuracy"), value: 2, ), const PopupMenuItem( child: Text("Open App Settings"), value: 3, ), if (Platform.isAndroid) const PopupMenuItem( child: Text("Open Location Settings"), value: 4, ), const PopupMenuItem( child: Text("Clear"), value: 5, ), ], ); } @override Widget build(BuildContext context) { 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.background, 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( child: (_positionStreamSubscription == null || _positionStreamSubscription!.isPaused) ? const Icon(Icons.play_arrow) : const Icon(Icons.pause), onPressed: _toggleListening, tooltip: (_positionStreamSubscription == null) ? 'Start position updates' : _positionStreamSubscription!.isPaused ? 'Resume' : 'Pause', backgroundColor: _determineButtonColor(), ), sizedBox, FloatingActionButton( child: const Icon(Icons.my_location), onPressed: _getCurrentPosition, ), sizedBox, FloatingActionButton( child: const Icon(Icons.bookmark), onPressed: _getLastKnownPosition, ), ], ), ), ) ]); } Future _getCurrentPosition() async { final hasPermission = await _handlePermission(); if (!hasPermission) { return; } final position = await geolocatorAndroid.getCurrentPosition(); _updatePositionList( _PositionItemType.position, position.toString(), ); } Future _handlePermission() async { bool serviceEnabled; LocationPermission permission; // Test if location services are enabled. serviceEnabled = await geolocatorAndroid.isLocationServiceEnabled(); if (!serviceEnabled) { // Location services are not enabled don't continue // accessing the position and request users of the // App to enable the location services. _updatePositionList( _PositionItemType.log, _kLocationServicesDisabledMessage, ); return false; } permission = await geolocatorAndroid.checkPermission(); if (permission == LocationPermission.denied) { permission = await geolocatorAndroid.requestPermission(); if (permission == LocationPermission.denied) { // Permissions are denied, next time you could try // requesting permissions again (this is also where // Android's shouldShowRequestPermissionRationale // returned true. According to Android guidelines // your App should show an explanatory UI now. _updatePositionList( _PositionItemType.log, _kPermissionDeniedMessage, ); return false; } } if (permission == LocationPermission.deniedForever) { // Permissions are denied forever, handle appropriately. _updatePositionList( _PositionItemType.log, _kPermissionDeniedForeverMessage, ); return false; } // When we reach here, permissions are granted and we can // continue accessing the position of the device. _updatePositionList( _PositionItemType.log, _kPermissionGrantedMessage, ); return true; } void _updatePositionList(_PositionItemType type, String displayValue) { _positionItems.add(_PositionItem(type, displayValue)); setState(() {}); } bool _isListening() => !(_positionStreamSubscription == null || _positionStreamSubscription!.isPaused); Color _determineButtonColor() { return _isListening() ? Colors.green : Colors.red; } void _toggleServiceStatusStream() { if (_serviceStatusStreamSubscription == null) { final serviceStatusStream = geolocatorAndroid.getServiceStatusStream(); _serviceStatusStreamSubscription = serviceStatusStream.handleError((error) { _serviceStatusStreamSubscription?.cancel(); _serviceStatusStreamSubscription = null; }).listen((serviceStatus) { String serviceStatusValue; if (serviceStatus == ServiceStatus.enabled) { serviceStatusValue = 'enabled'; } else { serviceStatusValue = 'disabled'; } _updatePositionList( _PositionItemType.log, 'Location service has been $serviceStatusValue', ); }); } } Future _toggleListening() async { final hasPermission = await _handlePermission(); if (!hasPermission) { return; } if (_positionStreamSubscription == null) { final androidSettings = AndroidSettings( accuracy: LocationAccuracy.lowest, timeLimit: const Duration(minutes: 1), ); final positionStream = geolocatorAndroid.getPositionStream(locationSettings: androidSettings); _positionStreamSubscription = positionStream.handleError((error) { _positionStreamSubscription?.cancel(); _positionStreamSubscription = null; }).listen((position) { debugPrint(position.altitude.toString()); _updatePositionList( _PositionItemType.position, position.toString(), ); }); _positionStreamSubscription?.pause(); } setState(() { if (_positionStreamSubscription == null) { return; } String statusDisplayValue; if (_positionStreamSubscription!.isPaused) { _positionStreamSubscription!.resume(); statusDisplayValue = 'resumed'; } else { _positionStreamSubscription!.pause(); statusDisplayValue = 'paused'; } _updatePositionList( _PositionItemType.log, 'Listening for position updates $statusDisplayValue', ); }); } @override void dispose() { if (_positionStreamSubscription != null) { _positionStreamSubscription!.cancel(); _positionStreamSubscription = null; } super.dispose(); } void _getLastKnownPosition() async { final position = await geolocatorAndroid.getLastKnownPosition(); if (position != null) { _updatePositionList( _PositionItemType.position, position.toString(), ); } else { _updatePositionList( _PositionItemType.log, 'No last known position available', ); } } void _getLocationAccuracy() async { final status = await geolocatorAndroid.getLocationAccuracy(); _handleLocationAccuracyStatus(status); } void _requestTemporaryFullAccuracy() async { final status = await geolocatorAndroid.requestTemporaryFullAccuracy( purposeKey: "TemporaryPreciseAccuracy", ); _handleLocationAccuracyStatus(status); } void _handleLocationAccuracyStatus(LocationAccuracyStatus status) { String locationAccuracyStatusValue; if (status == LocationAccuracyStatus.precise) { locationAccuracyStatusValue = 'Precise'; } else if (status == LocationAccuracyStatus.reduced) { locationAccuracyStatusValue = 'Reduced'; } else { locationAccuracyStatusValue = 'Unknown'; } _updatePositionList( _PositionItemType.log, '$locationAccuracyStatusValue location accuracy granted.', ); } void _openAppSettings() async { final opened = await geolocatorAndroid.openAppSettings(); String displayValue; if (opened) { displayValue = 'Opened Application Settings.'; } else { displayValue = 'Error opening Application Settings.'; } _updatePositionList( _PositionItemType.log, displayValue, ); } void _openLocationSettings() async { final opened = await geolocatorAndroid.openLocationSettings(); String displayValue; if (opened) { displayValue = 'Opened Location Settings'; } else { displayValue = 'Error opening Location Settings'; } _updatePositionList( _PositionItemType.log, displayValue, ); } } enum _PositionItemType { log, position, } class _PositionItem { _PositionItem(this.type, this.displayValue); final _PositionItemType type; final String displayValue; } ```
TimHoogstrate commented 10 months ago

Dear @vanelizarov,

However, the fix in your PR seems fine. I am still not able to reproduce the issue with the applied settings. But I am running a different AS version and emulator (although I don't think this will make a difference). I'll keep this issue open for so that the team can join me in the investigation. Linked PR

mvanbeusekom commented 10 months ago

Hi @vanelizarov,

Thank you for reporting the issue and creating a PR with the solution. We have just merged your PR and published version 4.4.1 to pub.dev which should resolve this issue.

vanelizarov commented 10 months ago

Hi @vanelizarov,

Thank you for reporting the issue and creating a PR with the solution. We have just merged your PR and published version 4.4.1 to pub.dev which should resolve this issue.

Cool!