Harden geofence automation and home editing
This commit is contained in:
@@ -5,6 +5,8 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
import '../services/location_platform_service.dart';
|
||||
|
||||
enum UserLocationIssue {
|
||||
servicesDisabled,
|
||||
permissionDenied,
|
||||
@@ -46,6 +48,10 @@ final userLocationProvider =
|
||||
() => UserLocationNotifier(),
|
||||
);
|
||||
|
||||
final locationPlatformServiceProvider = Provider<LocationPlatformService>(
|
||||
(ref) => DeviceLocationPlatformService(),
|
||||
);
|
||||
|
||||
class UserLocationNotifier extends Notifier<UserLocation> {
|
||||
StreamSubscription<Position>? _sub;
|
||||
int _watchers = 0;
|
||||
@@ -71,9 +77,21 @@ class UserLocationNotifier extends Notifier<UserLocation> {
|
||||
/// стрим остановится только когда все вызовут stopWatching.
|
||||
Future<void> startWatching() async {
|
||||
_watchers++;
|
||||
await _startWatchingIfPossible();
|
||||
}
|
||||
|
||||
Future<void> ensureWatchingStarted() async {
|
||||
if (_watchers == 0 || _sub != null) {
|
||||
return;
|
||||
}
|
||||
await _startWatchingIfPossible();
|
||||
}
|
||||
|
||||
Future<void> _startWatchingIfPossible() async {
|
||||
final locationService = ref.read(locationPlatformServiceProvider);
|
||||
if (_sub != null) return;
|
||||
|
||||
final permissionState = await _ensurePermission();
|
||||
final permissionState = await _ensurePermission(requestIfDenied: false);
|
||||
if (!permissionState.isGranted) {
|
||||
state = permissionState.toLocation();
|
||||
return;
|
||||
@@ -81,7 +99,7 @@ class UserLocationNotifier extends Notifier<UserLocation> {
|
||||
|
||||
if (!state.hasPosition) {
|
||||
try {
|
||||
final last = await Geolocator.getLastKnownPosition();
|
||||
final last = await locationService.getLastKnownPosition();
|
||||
if (last != null) {
|
||||
state = _fromPosition(last);
|
||||
}
|
||||
@@ -93,17 +111,19 @@ class UserLocationNotifier extends Notifier<UserLocation> {
|
||||
distanceFilter: 20,
|
||||
);
|
||||
|
||||
_sub = Geolocator.getPositionStream(locationSettings: settings).listen(
|
||||
(pos) => state = _fromPosition(pos),
|
||||
onError: (e) {
|
||||
debugPrint('Ошибка стрима геолокации: $e');
|
||||
state = UserLocation(
|
||||
error: 'Не удалось отслеживать позицию: $e',
|
||||
issue: UserLocationIssue.unavailable,
|
||||
updatedAt: state.updatedAt,
|
||||
_sub = locationService
|
||||
.getPositionStream(locationSettings: settings)
|
||||
.listen(
|
||||
(pos) => state = _fromPosition(pos),
|
||||
onError: (e) {
|
||||
debugPrint('Ошибка стрима геолокации: $e');
|
||||
state = UserLocation(
|
||||
error: 'Не удалось отслеживать позицию: $e',
|
||||
issue: UserLocationIssue.unavailable,
|
||||
updatedAt: state.updatedAt,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Остановить отслеживание. Вызывать из dispose экрана.
|
||||
@@ -116,20 +136,21 @@ class UserLocationNotifier extends Notifier<UserLocation> {
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
final permissionState = await _ensurePermission();
|
||||
final locationService = ref.read(locationPlatformServiceProvider);
|
||||
final permissionState = await _ensurePermission(requestIfDenied: false);
|
||||
if (!permissionState.isGranted) {
|
||||
state = permissionState.toLocation();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final last = await Geolocator.getLastKnownPosition();
|
||||
final last = await locationService.getLastKnownPosition();
|
||||
if (last != null) {
|
||||
state = _fromPosition(last);
|
||||
return;
|
||||
}
|
||||
|
||||
final pos = await Geolocator.getCurrentPosition(
|
||||
final pos = await locationService.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.low,
|
||||
timeLimit: Duration(seconds: 10),
|
||||
@@ -146,35 +167,40 @@ class UserLocationNotifier extends Notifier<UserLocation> {
|
||||
}
|
||||
|
||||
Future<void> requestPermission() async {
|
||||
await Geolocator.requestPermission();
|
||||
final locationService = ref.read(locationPlatformServiceProvider);
|
||||
await locationService.requestPermission();
|
||||
if (_watchers > 0 && _sub == null) {
|
||||
await startWatching();
|
||||
await _startWatchingIfPossible();
|
||||
return;
|
||||
}
|
||||
await refresh();
|
||||
}
|
||||
|
||||
Future<void> openAppSettings() async {
|
||||
await Geolocator.openAppSettings();
|
||||
await ref.read(locationPlatformServiceProvider).openAppSettings();
|
||||
}
|
||||
|
||||
Future<void> openLocationSettings() async {
|
||||
await Geolocator.openLocationSettings();
|
||||
await ref.read(locationPlatformServiceProvider).openLocationSettings();
|
||||
}
|
||||
|
||||
/// Проверить сервис и пермишены. Возвращает null если всё ок,
|
||||
/// иначе строку с причиной ошибки.
|
||||
Future<_LocationPermissionState> _ensurePermission() async {
|
||||
if (!await Geolocator.isLocationServiceEnabled()) {
|
||||
Future<_LocationPermissionState> _ensurePermission({
|
||||
required bool requestIfDenied,
|
||||
}) async {
|
||||
final locationService = ref.read(locationPlatformServiceProvider);
|
||||
|
||||
if (!await locationService.isLocationServiceEnabled()) {
|
||||
return const _LocationPermissionState(
|
||||
issue: UserLocationIssue.servicesDisabled,
|
||||
message: 'Геолокация выключена',
|
||||
);
|
||||
}
|
||||
|
||||
var perm = await Geolocator.checkPermission();
|
||||
if (perm == LocationPermission.denied) {
|
||||
perm = await Geolocator.requestPermission();
|
||||
var perm = await locationService.checkPermission();
|
||||
if (perm == LocationPermission.denied && requestIfDenied) {
|
||||
perm = await locationService.requestPermission();
|
||||
}
|
||||
if (perm == LocationPermission.denied) {
|
||||
return const _LocationPermissionState(
|
||||
|
||||
14
lib/features/homes/services/home_connection_change.dart
Normal file
14
lib/features/homes/services/home_connection_change.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import '../../../models/home_config.dart';
|
||||
|
||||
bool hasHomeConnectionChanges({
|
||||
required HomeConfig? originalHome,
|
||||
required String normalizedUrl,
|
||||
required String apiKey,
|
||||
required String originalApiKey,
|
||||
}) {
|
||||
if (originalHome == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return normalizedUrl != originalHome.url || apiKey != originalApiKey;
|
||||
}
|
||||
69
lib/features/homes/services/location_platform_service.dart
Normal file
69
lib/features/homes/services/location_platform_service.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
abstract class LocationPlatformService {
|
||||
Future<bool> isLocationServiceEnabled();
|
||||
|
||||
Future<LocationPermission> checkPermission();
|
||||
|
||||
Future<LocationPermission> requestPermission();
|
||||
|
||||
Future<Position?> getLastKnownPosition();
|
||||
|
||||
Future<Position> getCurrentPosition({
|
||||
required LocationSettings locationSettings,
|
||||
});
|
||||
|
||||
Stream<Position> getPositionStream({
|
||||
required LocationSettings locationSettings,
|
||||
});
|
||||
|
||||
Future<bool> openAppSettings();
|
||||
|
||||
Future<bool> openLocationSettings();
|
||||
}
|
||||
|
||||
class DeviceLocationPlatformService implements LocationPlatformService {
|
||||
@override
|
||||
Future<bool> isLocationServiceEnabled() {
|
||||
return Geolocator.isLocationServiceEnabled();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LocationPermission> checkPermission() {
|
||||
return Geolocator.checkPermission();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LocationPermission> requestPermission() {
|
||||
return Geolocator.requestPermission();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Position?> getLastKnownPosition() {
|
||||
return Geolocator.getLastKnownPosition();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Position> getCurrentPosition({
|
||||
required LocationSettings locationSettings,
|
||||
}) {
|
||||
return Geolocator.getCurrentPosition(locationSettings: locationSettings);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Position> getPositionStream({
|
||||
required LocationSettings locationSettings,
|
||||
}) {
|
||||
return Geolocator.getPositionStream(locationSettings: locationSettings);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> openAppSettings() {
|
||||
return Geolocator.openAppSettings();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> openLocationSettings() {
|
||||
return Geolocator.openLocationSettings();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
import '../../homes/providers/homes_providers.dart';
|
||||
import '../../homes/providers/location_providers.dart';
|
||||
import '../../shared/providers/core_providers.dart';
|
||||
import '../models/app_theme_preset.dart';
|
||||
import '../models/geofence_system_state.dart';
|
||||
import '../models/notification_permission_status.dart';
|
||||
import '../services/geofence_system_status_service.dart';
|
||||
import '../services/notification_permission_status_service.dart';
|
||||
|
||||
final initialAppThemePresetProvider = Provider<AppThemePreset>(
|
||||
(ref) => AppThemePreset.fallback,
|
||||
@@ -29,53 +30,11 @@ class AppThemeNotifier extends Notifier<AppThemePreset> {
|
||||
}
|
||||
}
|
||||
|
||||
abstract class GeofenceSystemStatusService {
|
||||
Future<GeofenceSystemState> inspect({
|
||||
required bool hasActiveHome,
|
||||
required bool hasCoordinates,
|
||||
});
|
||||
}
|
||||
|
||||
class DeviceGeofenceSystemStatusService implements GeofenceSystemStatusService {
|
||||
@override
|
||||
Future<GeofenceSystemState> inspect({
|
||||
required bool hasActiveHome,
|
||||
required bool hasCoordinates,
|
||||
}) async {
|
||||
if (!hasActiveHome) {
|
||||
return const GeofenceSystemState(GeofenceSystemIssue.noActiveHome);
|
||||
}
|
||||
if (!hasCoordinates) {
|
||||
return const GeofenceSystemState(GeofenceSystemIssue.missingCoordinates);
|
||||
}
|
||||
if (!await Geolocator.isLocationServiceEnabled()) {
|
||||
return const GeofenceSystemState(
|
||||
GeofenceSystemIssue.locationServicesDisabled,
|
||||
);
|
||||
}
|
||||
|
||||
final permission = await Geolocator.checkPermission();
|
||||
return switch (permission) {
|
||||
LocationPermission.denied => const GeofenceSystemState(
|
||||
GeofenceSystemIssue.permissionDenied,
|
||||
),
|
||||
LocationPermission.deniedForever => const GeofenceSystemState(
|
||||
GeofenceSystemIssue.permissionDeniedForever,
|
||||
),
|
||||
LocationPermission.whileInUse => const GeofenceSystemState(
|
||||
GeofenceSystemIssue.backgroundPermissionRequired,
|
||||
),
|
||||
LocationPermission.always => const GeofenceSystemState(
|
||||
GeofenceSystemIssue.ready,
|
||||
),
|
||||
_ => const GeofenceSystemState(GeofenceSystemIssue.permissionDenied),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
final geofenceSystemStatusServiceProvider =
|
||||
Provider<GeofenceSystemStatusService>(
|
||||
(ref) => DeviceGeofenceSystemStatusService(),
|
||||
(ref) => DeviceGeofenceSystemStatusService(
|
||||
locationPlatformService: ref.read(locationPlatformServiceProvider),
|
||||
),
|
||||
);
|
||||
|
||||
final geofenceSystemStatusProvider = FutureProvider<GeofenceSystemState>((
|
||||
@@ -90,49 +49,6 @@ final geofenceSystemStatusProvider = FutureProvider<GeofenceSystemState>((
|
||||
);
|
||||
});
|
||||
|
||||
abstract class NotificationPermissionStatusService {
|
||||
Future<NotificationPermissionStatus> inspect();
|
||||
|
||||
Future<void> requestPermission();
|
||||
|
||||
Future<void> openSettings();
|
||||
}
|
||||
|
||||
class DeviceNotificationPermissionStatusService
|
||||
implements NotificationPermissionStatusService {
|
||||
static const _channel = MethodChannel('ignis/geofence_automation');
|
||||
|
||||
@override
|
||||
Future<NotificationPermissionStatus> inspect() async {
|
||||
try {
|
||||
final value = await _channel.invokeMethod<String>(
|
||||
'getNotificationPermissionStatus',
|
||||
);
|
||||
return NotificationPermissionStatus.fromPlatformValue(value);
|
||||
} on MissingPluginException {
|
||||
return NotificationPermissionStatus.unsupported;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestPermission() async {
|
||||
try {
|
||||
await _channel.invokeMethod<void>('requestNotificationPermission');
|
||||
} on MissingPluginException {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> openSettings() async {
|
||||
try {
|
||||
await _channel.invokeMethod<void>('openNotificationSettings');
|
||||
} on MissingPluginException {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final notificationPermissionStatusServiceProvider =
|
||||
Provider<NotificationPermissionStatusService>(
|
||||
(ref) => DeviceNotificationPermissionStatusService(),
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
import '../../homes/services/location_platform_service.dart';
|
||||
import '../models/geofence_system_state.dart';
|
||||
|
||||
abstract class GeofenceSystemStatusService {
|
||||
Future<GeofenceSystemState> inspect({
|
||||
required bool hasActiveHome,
|
||||
required bool hasCoordinates,
|
||||
});
|
||||
}
|
||||
|
||||
class DeviceGeofenceSystemStatusService implements GeofenceSystemStatusService {
|
||||
final LocationPlatformService locationPlatformService;
|
||||
|
||||
DeviceGeofenceSystemStatusService({required this.locationPlatformService});
|
||||
|
||||
@override
|
||||
Future<GeofenceSystemState> inspect({
|
||||
required bool hasActiveHome,
|
||||
required bool hasCoordinates,
|
||||
}) async {
|
||||
if (!hasActiveHome) {
|
||||
return const GeofenceSystemState(GeofenceSystemIssue.noActiveHome);
|
||||
}
|
||||
if (!hasCoordinates) {
|
||||
return const GeofenceSystemState(GeofenceSystemIssue.missingCoordinates);
|
||||
}
|
||||
if (!await locationPlatformService.isLocationServiceEnabled()) {
|
||||
return const GeofenceSystemState(
|
||||
GeofenceSystemIssue.locationServicesDisabled,
|
||||
);
|
||||
}
|
||||
|
||||
final permission = await locationPlatformService.checkPermission();
|
||||
return switch (permission) {
|
||||
LocationPermission.denied => const GeofenceSystemState(
|
||||
GeofenceSystemIssue.permissionDenied,
|
||||
),
|
||||
LocationPermission.deniedForever => const GeofenceSystemState(
|
||||
GeofenceSystemIssue.permissionDeniedForever,
|
||||
),
|
||||
LocationPermission.whileInUse => const GeofenceSystemState(
|
||||
GeofenceSystemIssue.backgroundPermissionRequired,
|
||||
),
|
||||
LocationPermission.always => const GeofenceSystemState(
|
||||
GeofenceSystemIssue.ready,
|
||||
),
|
||||
_ => const GeofenceSystemState(GeofenceSystemIssue.permissionDenied),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../models/notification_permission_status.dart';
|
||||
|
||||
abstract class NotificationPermissionStatusService {
|
||||
Future<NotificationPermissionStatus> inspect();
|
||||
|
||||
Future<void> requestPermission();
|
||||
|
||||
Future<void> openSettings();
|
||||
}
|
||||
|
||||
class DeviceNotificationPermissionStatusService
|
||||
implements NotificationPermissionStatusService {
|
||||
static const _channel = MethodChannel('ignis/geofence_automation');
|
||||
|
||||
@override
|
||||
Future<NotificationPermissionStatus> inspect() async {
|
||||
try {
|
||||
final value = await _channel.invokeMethod<String>(
|
||||
'getNotificationPermissionStatus',
|
||||
);
|
||||
return NotificationPermissionStatus.fromPlatformValue(value);
|
||||
} on MissingPluginException {
|
||||
return NotificationPermissionStatus.unsupported;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestPermission() async {
|
||||
try {
|
||||
await _channel.invokeMethod<void>('requestNotificationPermission');
|
||||
} on MissingPluginException {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> openSettings() async {
|
||||
try {
|
||||
await _channel.invokeMethod<void>('openNotificationSettings');
|
||||
} on MissingPluginException {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user