feat: harden geofence and distance diagnostics

This commit is contained in:
Artem Kokos
2026-05-01 09:13:23 +07:00
parent 872ddf9513
commit 91a494adf5
20 changed files with 1639 additions and 260 deletions

View File

@@ -0,0 +1,226 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:geolocator/geolocator.dart';
import '../../../app/load_state.dart';
import '../../../models/home_config.dart';
import '../geofence_logic.dart';
import '../models/geofence_diagnostics.dart';
import '../models/geofence_runtime_state.dart';
import '../services/geofence_notifications_service.dart';
import '../services/geofence_runtime_store.dart';
import 'homes_providers.dart';
final geofenceRuntimeStoreProvider = Provider((ref) => GeofenceRuntimeStore());
final geofenceNotificationsServiceProvider = Provider(
(ref) => GeofenceNotificationsService(),
);
final geofenceDiagnosticsProvider =
NotifierProvider<
GeofenceDiagnosticsNotifier,
LoadState<GeofenceDiagnostics>
>(GeofenceDiagnosticsNotifier.new);
class GeofenceDiagnosticsNotifier
extends Notifier<LoadState<GeofenceDiagnostics>> {
bool _refreshing = false;
@override
LoadState<GeofenceDiagnostics> build() {
return const LoadState.idle(GeofenceDiagnostics.initial());
}
Future<void> refresh() async {
if (_refreshing) return;
_refreshing = true;
final previous = state.data;
state = LoadState.loading(previous);
try {
final currentHome = ref.read(currentHomeProvider);
final runtime = await ref.read(geofenceRuntimeStoreProvider).load();
if (currentHome == null) {
state = LoadState.data(
GeofenceDiagnostics(
activeHome: null,
status: GeofenceStatusKind.noActiveHome,
runtime: runtime,
locationServicesEnabled: true,
locationPermission: LocationPermission.denied,
notificationsEnabled: true,
),
);
return;
}
if (!currentHome.geofenceEnabled) {
state = LoadState.data(
GeofenceDiagnostics(
activeHome: currentHome,
status: GeofenceStatusKind.disabled,
runtime: runtime,
locationServicesEnabled: true,
locationPermission: LocationPermission.denied,
notificationsEnabled: true,
),
);
return;
}
if (!currentHome.hasCoordinates) {
state = LoadState.data(
GeofenceDiagnostics(
activeHome: currentHome,
status: GeofenceStatusKind.missingCoordinates,
runtime: runtime,
locationServicesEnabled: true,
locationPermission: LocationPermission.denied,
notificationsEnabled: true,
),
);
return;
}
final locationServicesEnabled =
await Geolocator.isLocationServiceEnabled();
final locationPermission = await Geolocator.checkPermission();
final notificationsEnabled = await ref
.read(geofenceNotificationsServiceProvider)
.areNotificationsEnabled();
final diagnostics = _buildDiagnostics(
currentHome: currentHome,
runtime: runtime,
locationServicesEnabled: locationServicesEnabled,
locationPermission: locationPermission,
notificationsEnabled: notificationsEnabled,
);
state = LoadState.data(diagnostics);
} catch (error) {
state = LoadState.error(
previous,
'Не удалось обновить состояние geofence: $error',
);
} finally {
_refreshing = false;
}
}
Future<void> requestLocationPermission() async {
await Geolocator.requestPermission();
await refresh();
}
Future<void> requestBackgroundLocationPermission() async {
final result = await Geolocator.requestPermission();
if (!hasBackgroundLocationAccess(result)) {
await Geolocator.openAppSettings();
}
await refresh();
}
Future<void> requestNotificationPermission() async {
await ref
.read(geofenceNotificationsServiceProvider)
.requestNotificationsPermission();
await refresh();
}
Future<void> openAppSettings() async {
await Geolocator.openAppSettings();
}
Future<void> openLocationSettings() async {
await Geolocator.openLocationSettings();
}
GeofenceDiagnostics _buildDiagnostics({
required HomeConfig currentHome,
required GeofenceRuntimeState runtime,
required bool locationServicesEnabled,
required LocationPermission locationPermission,
required bool notificationsEnabled,
}) {
if (!locationServicesEnabled) {
return GeofenceDiagnostics(
activeHome: currentHome,
status: GeofenceStatusKind.locationServicesDisabled,
runtime: runtime,
locationServicesEnabled: false,
locationPermission: locationPermission,
notificationsEnabled: notificationsEnabled,
);
}
if (!hasForegroundLocationAccess(locationPermission)) {
return GeofenceDiagnostics(
activeHome: currentHome,
status: GeofenceStatusKind.locationPermissionDenied,
runtime: runtime,
locationServicesEnabled: true,
locationPermission: locationPermission,
notificationsEnabled: notificationsEnabled,
);
}
if (!hasBackgroundLocationAccess(locationPermission)) {
return GeofenceDiagnostics(
activeHome: currentHome,
status: GeofenceStatusKind.backgroundPermissionDenied,
runtime: runtime,
locationServicesEnabled: true,
locationPermission: locationPermission,
notificationsEnabled: notificationsEnabled,
);
}
if (!notificationsEnabled) {
return GeofenceDiagnostics(
activeHome: currentHome,
status: GeofenceStatusKind.notificationsPermissionDenied,
runtime: runtime,
locationServicesEnabled: true,
locationPermission: locationPermission,
notificationsEnabled: false,
);
}
final failureAt = runtime.failureAtFor(currentHome.id);
final retryRemaining = geofenceRetryRemaining(failureAt);
if (retryRemaining != null) {
return GeofenceDiagnostics(
activeHome: currentHome,
status: GeofenceStatusKind.cooldown,
runtime: runtime,
locationServicesEnabled: true,
locationPermission: locationPermission,
notificationsEnabled: true,
retryRemaining: retryRemaining,
detail: runtime.failureMessageFor(currentHome.id),
);
}
if (runtime.isTriggeredFor(currentHome.id)) {
return GeofenceDiagnostics(
activeHome: currentHome,
status: GeofenceStatusKind.triggered,
runtime: runtime,
locationServicesEnabled: true,
locationPermission: locationPermission,
notificationsEnabled: true,
);
}
return GeofenceDiagnostics(
activeHome: currentHome,
status: GeofenceStatusKind.ready,
runtime: runtime,
locationServicesEnabled: true,
locationPermission: locationPermission,
notificationsEnabled: true,
);
}
}