import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import '../../core/region/geo.dart'; /// What stands between the app and a position fix. enum LocationAvailability { /// Ready to locate. granted, /// Location is switched off on the device. The user has to enable it in /// system settings; asking for permission would not help. serviceDisabled, /// Not granted yet, but askable. denied, /// Refused permanently. Only system settings can undo it, so the UI must send /// the user there rather than asking again into the void. deniedForever, } /// Reads the device position. /// /// Behind an interface so the permission dance can be tested without a device /// and without the plugin's static methods. abstract interface class LocationService { Future availability(); /// Asks for permission if it has not been granted yet. Future request(); /// A single coarse fix, or null if it could not be obtained. Future currentPoint(); } class GeolocatorLocationService implements LocationService { const GeolocatorLocationService(); /// Deliberately coarse. /// /// This app centres a map and picks the nearest weather station, and stations /// are kilometres apart. Street-level precision would buy nothing and would /// mean asking for a more invasive permission, so the app declares only /// `ACCESS_COARSE_LOCATION` and asks for low accuracy to match. /// /// `forceLocationManager` uses the platform's own LocationManager instead of /// the Google Play Services fused provider. The fused provider triggers a /// "your device will need to use Location Accuracy" prompt, and declining it /// yields **no fix at all** — an absurd outcome for an app that only wants an /// approximate one. It also drops the Play Services dependency, so location /// works on devices that do not have them. static LocationSettings get _settings { if (defaultTargetPlatform == TargetPlatform.android) { return AndroidSettings( accuracy: LocationAccuracy.low, forceLocationManager: true, timeLimit: const Duration(seconds: 20), ); } return const LocationSettings( accuracy: LocationAccuracy.low, timeLimit: Duration(seconds: 20), ); } @override Future availability() async { if (!await Geolocator.isLocationServiceEnabled()) { return LocationAvailability.serviceDisabled; } return _map(await Geolocator.checkPermission()); } @override Future request() async { if (!await Geolocator.isLocationServiceEnabled()) { return LocationAvailability.serviceDisabled; } final current = await Geolocator.checkPermission(); if (current == LocationPermission.denied) { return _map(await Geolocator.requestPermission()); } return _map(current); } @override Future currentPoint() async { if (await availability() != LocationAvailability.granted) return null; try { final position = await Geolocator.getCurrentPosition( locationSettings: _settings, ); return GeoPoint(position.longitude, position.latitude); } on Object { // A fresh fix can take seconds indoors, or time out entirely. For // centring a map a slightly stale position is worth far more than // nothing, so fall back to whatever the system already knows. try { final last = await Geolocator.getLastKnownPosition(); if (last == null) return null; return GeoPoint(last.longitude, last.latitude); } on Object { return null; } } } static LocationAvailability _map(LocationPermission permission) => switch (permission) { LocationPermission.always || LocationPermission.whileInUse => LocationAvailability.granted, LocationPermission.deniedForever => LocationAvailability.deniedForever, LocationPermission.denied || LocationPermission.unableToDetermine => LocationAvailability.denied, }; } final locationServiceProvider = Provider( (ref) => const GeolocatorLocationService(), ); /// Whether the user has seen the prominent disclosure in this session. /// /// Google Play requires the explanation to come **before** the system dialog. /// Session-scoped rather than persisted: showing it again after an app restart /// costs one tap, and a user who forgot what they agreed to deserves to be /// reminded. class LocationDisclosureShown extends Notifier { @override bool build() => false; void markShown() => state = true; } final locationDisclosureShownProvider = NotifierProvider( LocationDisclosureShown.new, );