Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

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

Closed
3 of 8 tasks
vanelizarov opened this issue Dec 31, 2023 · 5 comments · Fixed by #1402
Closed
3 of 8 tasks
Assignees

Comments

@vanelizarov
Copy link
Contributor

Please check the following before submitting a new issue.

Please select affected platform(s)

  • Android
  • iOS
  • Linux
  • macOS
  • Web
  • Windows

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
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
[✓] 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
Copy link
Contributor

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
Copy link
Contributor Author

@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
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
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<GeolocatorWidget> {
  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<Position>? _positionStreamSubscription;
  StreamSubscription<ServiceStatus>? _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<void> _getCurrentPosition() async {
    final hasPermission = await _handlePermission();

    if (!hasPermission) {
      return;
    }

    final position = await geolocatorAndroid.getCurrentPosition();
    _updatePositionList(
      _PositionItemType.position,
      position.toString(),
    );
  }

  Future<bool> _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<void> _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
Copy link
Contributor

TimHoogstrate commented Jan 4, 2024

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
Copy link
Member

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
Copy link
Contributor Author

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants