tlserver / flutter_map_location_marker

A flutter map plugin for displaying device current location.
https://pub.dev/packages/flutter_map_location_marker
BSD 3-Clause "New" or "Revised" License
102 stars 93 forks source link

Turning of location access causes Unhandled Exception: The location service on the device is disabled. #18

Closed comatory closed 2 years ago

comatory commented 2 years ago

Since the plugin creates and manages its own position stream (see https://github.com/tlserver/flutter_map_location_marker/issues/15), the error cause by turning off location access is not handled.

On application level, I can decide whether to build LocationMarkerLayerWidget based on my application permission or service status. However since the stream is created with the plugin, this error reaches the position stream before LocationMarkerLayerWidget is removed from widget tree.

There should be onError handler or at least let the user know via callback that problem occured with the ability to somehow recover from the error.

Console output:

E/flutter (24738): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: The location service on the device is disabled.
E/flutter (24738): #0      MethodChannelGeolocator.getPositionStream.<anonymous closure> (package:geolocator_platform_interface/src/implementations/method_channel_geolocator.dart:188:9)
E/flutter (24738): #1      _invokeErrorHandler (dart:async/async_error.dart:45:24)
E/flutter (24738): #2      _HandleErrorStream._handleError (dart:async/stream_pipe.dart:269:9)
E/flutter (24738): #3      _ForwardingStreamSubscription._handleError (dart:async/stream_pipe.dart:157:13)
E/flutter (24738): #4      _rootRunBinary (dart:async/zone.dart:1452:47)
E/flutter (24738): #5      _CustomZone.runBinary (dart:async/zone.dart:1342:19)
E/flutter (24738): #6      _CustomZone.runBinaryGuarded (dart:async/zone.dart:1252:7)
E/flutter (24738): #7      _BufferingStreamSubscription._sendError.sendError (dart:async/stream_impl.dart:360:15)
E/flutter (24738): #8      _BufferingStreamSubscription._sendError (dart:async/stream_impl.dart:378:7)
E/flutter (24738): #9      _BufferingStreamSubscription._addError (dart:async/stream_impl.dart:280:7)
E/flutter (24738): #10     _ForwardingStreamSubscription._addError (dart:async/stream_pipe.dart:128:11)
E/flutter (24738): #11     _ForwardingStream._handleError (dart:async/stream_pipe.dart:95:10)
E/flutter (24738): #12     _ForwardingStreamSubscription._handleError (dart:async/stream_pipe.dart:157:13)
E/flutter (24738): #13     _rootRunBinary (dart:async/zone.dart:1452:47)
E/flutter (24738): #14     _CustomZone.runBinary (dart:async/zone.dart:1342:19)
E/flutter (24738): #15     _CustomZone.runBinaryGuarded (dart:async/zone.dart:1252:7)
E/flutter (24738): #16     _BufferingStreamSubscription._sendError.sendError (dart:async/stream_impl.dart:360:15)
E/flutter (24738): #17     _BufferingStreamSubscription._sendError (dart:async/stream_impl.dart:378:7)
E/flutter (24738): #18     _BufferingStreamSubscription._addError (dart:async/stream_impl.dart:280:7)
E/flutter (24738): #19     _SyncBroadcastStreamController._sendError.<anonymous closure> (dart:async/broadcast_stream_controller.dart:393:20)
E/flutter (24738): #20     _BroadcastStreamController._forEachListener (dart:async/broadcast_stream_controller.dart:323:15)
E/flutter (24738): #21     _SyncBroadcastStreamController._sendError (dart:async/broadcast_stream_controller.dart:392:5)
E/flutter (24738): #22     _AsBroadcastStreamController.addError (dart:async/broadcast_stream_controller.dart:487:5)
E/flutter (24738): #23     _rootRunBinary (dart:async/zone.dart:1452:47)
E/flutter (24738): #24     _CustomZone.runBinary (dart:async/zone.dart:1342:19)
E/flutter (24738): #25     _CustomZone.runBinaryGuarded (dart:async/zone.dart:1252:7)
E/flutter (24738): #26     _BufferingStreamSubscription._sendError.sendError (dart:async/stream_impl.dart:360:15)
E/flutter (24738): #27     _BufferingStreamSubscription._sendError (dart:async/stream_impl.dart:378:7)
E/flutter (24738): #28     _DelayedError.perform (dart:async/stream_impl.dart:602:14)
E/flutter (24738): #29     _StreamImplEvents.handleNext (dart:async/stream_impl.dart:706:11)
E/flutter (24738): #30     _PendingEvents.schedule.<anonymous closure> (dart:async/stream_impl.dart:663:7)
E/flutter (24738): #31     _rootRun (dart:async/zone.dart:1420:47)
E/flutter (24738): #32     _CustomZone.run (dart:async/zone.dart:1328:19)
E/flutter (24738): #33     _CustomZone.runGuarded (dart:async/zone.dart:1236:7)
E/flutter (24738): #34     _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1276:23)
E/flutter (24738): #35     _rootRun (dart:async/zone.dart:1428:13)
E/flutter (24738): #36     _CustomZone.run (dart:async/zone.dart:1328:19)
E/flutter (24738): #37     _CustomZone.runGuarded (dart:async/zone.dart:1236:7)
E/flutter (24738): #38     _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1276:23)
E/flutter (24738): #39     _microtaskLoop (dart:async/schedule_microtask.dart:40:21)
E/flutter (24738): #40     _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5)
E/flutter (24738):
tlserver commented 2 years ago

Can you provide reproduction step?

comatory commented 2 years ago

@tlserver Sure

This is the toy app I created to reproduce this:

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_location_marker/flutter_map_location_marker.dart';
import 'package:latlong2/latlong.dart';
import 'package:geolocator/geolocator.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter map location marker',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool _locationServiceEnabled = false;
  LocationPermission? _permission;
  StreamSubscription<ServiceStatus>? _serviceStatusSubscription;
  MapController? _controller;

  @override
  void initState() {
    super.initState();
    _getLocationPermission();
    _serviceStatusSubscription =
        Geolocator.getServiceStatusStream().listen(_handleServiceStatusChange);
  }

  @override
  void dispose() {
    _serviceStatusSubscription?.cancel();
    super.dispose();
  }

  Future<bool> _getLocationPermission() async {
    final locationServiceEnabled = await Geolocator.isLocationServiceEnabled();

    try {
      final permission = await Geolocator.requestPermission();
      setState(() {
        _locationServiceEnabled = locationServiceEnabled;
        _permission = permission;
        _logPermissionAndServiceStatus(
            locationServiceEnabled: locationServiceEnabled,
            permission: permission);
      });
      return Future.value(
          locationServiceEnabled && _isLocationPermitted(permission));
    } on Exception catch (_) {
      const permission = LocationPermission.unableToDetermine;
      setState(() {
        _locationServiceEnabled = locationServiceEnabled;
        _permission = permission;
        _logPermissionAndServiceStatus(
            locationServiceEnabled: locationServiceEnabled,
            permission: permission);
      });
      return Future.value(false);
    }
  }

  Future<void> _setCurrentLocationCenter(bool allowed) async {
    if (!allowed) {
      return;
    }

    final position = await Geolocator.getCurrentPosition();

    final center = LatLng(position.latitude, position.longitude);
    _controller?.move(center, 13.0);
  }

  Future<void> _handleServiceStatusChange(ServiceStatus status) async {
    try {
      final permission = await Geolocator.checkPermission();
      final locationServiceEnabled = _isLocationServiceEnabled(status);
      setState(() {
        _permission = permission;
        _locationServiceEnabled = locationServiceEnabled;
        _logPermissionAndServiceStatus(
            locationServiceEnabled: locationServiceEnabled,
            permission: permission);
      });
    } on Exception catch (_) {
      final locationServiceEnabled = _isLocationServiceEnabled(status);
      const permission = LocationPermission.unableToDetermine;
      setState(() {
        _locationServiceEnabled = locationServiceEnabled;
        _permission = permission;
        _logPermissionAndServiceStatus(
            locationServiceEnabled: locationServiceEnabled,
            permission: permission);
      });
    }
  }

  bool _isLocationServiceEnabled(ServiceStatus status) =>
      status == ServiceStatus.enabled;
  bool _isLocationPermitted(LocationPermission? permission) =>
      permission == LocationPermission.always ||
      permission == LocationPermission.whileInUse;

  void _logPermissionAndServiceStatus({
    required bool locationServiceEnabled,
    required LocationPermission permission,
  }) {
    debugPrint(
        'Location service enabled? $locationServiceEnabled\tLocation permission: $permission');
  }

  void _handleMapCreated(MapController controller) {
    _controller = controller;
    _setCurrentLocationCenter(
        _locationServiceEnabled && _isLocationPermitted(_permission));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: SizedBox.expand(
          child: FlutterMap(
        options:
            MapOptions(onMapCreated: _handleMapCreated, zoom: 13.0, plugins: [
          if (_locationServiceEnabled && _isLocationPermitted(_permission))
            const LocationMarkerPlugin(),
        ]),
        children: [
          TileLayerWidget(
              options: TileLayerOptions(
            urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
            subdomains: ['a', 'b', 'c'],
            attributionBuilder: (_) {
              return const Text("© OpenStreetMap contributors");
            },
          )),
          if (_locationServiceEnabled && _isLocationPermitted(_permission))
            LocationMarkerLayerWidget(),
        ],
      )),
    );
  }
}

I am also attaching the video so you know how exactly I'm turning off the services. In the code you can see that I conditionally check whether I have

https://user-images.githubusercontent.com/5990424/151961354-9f0379c3-2670-4f52-8216-d12ce966ba11.mov

If no, I do not use the plugin. In theory it would work but I think since the plugin uses Geolocator.getPositionStream internally, that stream receives error first and only later is the plugin removed.

I raised the issue here that this is problematic, I think this is another issue caused by the design.

Have you had a look at https://github.com/tlserver/flutter_map_location_marker/pull/16 that I created? If the position was provided from outside (as an option), I could handle this error in my application code.

But you should still handle this internally if you wish to keep using geolocator as dependency, it should have onError handler at minimum. At the moment telemetry in my app is reporting it as uncaught error.

tlserver commented 2 years ago

Fixed. Now v2.1 provide a callback for error handling and v3 exposed the stream.