Replace geofence polling with native Android geofence

This commit is contained in:
Artem Kokos
2026-05-12 11:23:44 +07:00
parent 0a5ef9af17
commit 1963488479
38 changed files with 1099 additions and 1931 deletions

View File

@@ -1,47 +0,0 @@
import 'package:geolocator/geolocator.dart';
const double geofenceThresholdMeters = 500.0;
const Duration geofenceRetryCooldown = Duration(minutes: 30);
bool hasForegroundLocationAccess(LocationPermission permission) {
return permission == LocationPermission.whileInUse ||
permission == LocationPermission.always;
}
bool hasBackgroundLocationAccess(LocationPermission permission) {
return permission == LocationPermission.always;
}
Duration? geofenceRetryRemaining(DateTime? lastFailureAt, {DateTime? now}) {
if (lastFailureAt == null) return null;
final currentTime = now ?? DateTime.now();
final remaining = lastFailureAt
.add(geofenceRetryCooldown)
.difference(currentTime);
if (remaining <= Duration.zero) {
return null;
}
return remaining;
}
String formatGeofenceRetry(Duration remaining) {
if (remaining.inHours >= 1) {
final hours = remaining.inHours;
final minutes = remaining.inMinutes.remainder(60);
if (minutes == 0) return '$hours ч';
return '$hours ч $minutes мин';
}
final minutes = remaining.inMinutes;
if (minutes > 0) return '$minutes мин';
return '${remaining.inSeconds.clamp(1, 59)} сек';
}
String formatDistanceMeters(double meters) {
if (meters < 1000) {
return '${meters.round()} м';
}
return '${(meters / 1000).toStringAsFixed(1)} км';
}

View File

@@ -1,49 +0,0 @@
import 'package:workmanager/workmanager.dart';
import '../../models/home_config.dart';
import '../../services/geofence_worker.dart';
import 'services/geofence_runtime_store.dart';
/// Синхронизировать состояние фонового таска с настройками домов.
/// Вызывать при старте приложения и при изменении настроек.
///
/// Геофенс работает только для текущего активного дома.
/// Если активный дом не готов -- таск снимается и runtime разоружается.
Future<void> syncGeofenceTask(
List<HomeConfig> homes, {
required HomeConfig? currentHome,
}) async {
final runtimeStore = GeofenceRuntimeStore();
final activeHome = currentHome;
final needGeofence =
activeHome != null &&
homes.any((home) => home.id == activeHome.id) &&
activeHome.geofenceReady;
if (needGeofence) {
await runtimeStore.armForHome(activeHome.id);
try {
await Workmanager().registerPeriodicTask(
geofenceTaskUniqueName,
geofenceTaskName,
frequency: const Duration(minutes: 15),
constraints: Constraints(networkType: NetworkType.connected),
existingWorkPolicy: ExistingPeriodicWorkPolicy.replace,
backoffPolicy: BackoffPolicy.linear,
backoffPolicyDelay: const Duration(minutes: 1),
);
} catch (_) {
// В тестах и на неполной платформенной инициализации
// не даём workmanager уронить остальное приложение.
}
} else {
await runtimeStore.disarm();
try {
await Workmanager().cancelByUniqueName(geofenceTaskUniqueName);
} catch (_) {
// См. комментарий выше: runtime должен синхронизироваться
// даже если platform plugin недоступен.
}
}
}

View File

@@ -1,123 +0,0 @@
import 'package:geolocator/geolocator.dart';
import '../../../models/home_config.dart';
import '../geofence_logic.dart';
import 'geofence_runtime_state.dart';
enum GeofenceStatusKind {
noActiveHome,
disabled,
missingCoordinates,
locationServicesDisabled,
locationPermissionDenied,
backgroundPermissionDenied,
notificationsPermissionDenied,
cooldown,
triggered,
ready,
}
class GeofenceDiagnostics {
final HomeConfig? activeHome;
final GeofenceStatusKind status;
final GeofenceRuntimeState runtime;
final bool locationServicesEnabled;
final LocationPermission locationPermission;
final bool notificationsEnabled;
final Duration? retryRemaining;
final String? detail;
const GeofenceDiagnostics({
required this.activeHome,
required this.status,
required this.runtime,
required this.locationServicesEnabled,
required this.locationPermission,
required this.notificationsEnabled,
this.retryRemaining,
this.detail,
});
const GeofenceDiagnostics.initial()
: activeHome = null,
status = GeofenceStatusKind.noActiveHome,
runtime = const GeofenceRuntimeState(),
locationServicesEnabled = true,
locationPermission = LocationPermission.denied,
notificationsEnabled = true,
retryRemaining = null,
detail = null;
String get title {
return switch (status) {
GeofenceStatusKind.noActiveHome => 'Геофенс не активен',
GeofenceStatusKind.disabled => 'Автовыключение выключено',
GeofenceStatusKind.missingCoordinates => 'Нет координат дома',
GeofenceStatusKind.locationServicesDisabled => 'Геолокация выключена',
GeofenceStatusKind.locationPermissionDenied => 'Нет доступа к геолокации',
GeofenceStatusKind.backgroundPermissionDenied =>
'Нет фонового доступа к геолокации',
GeofenceStatusKind.notificationsPermissionDenied =>
'Нет доступа к уведомлениям',
GeofenceStatusKind.cooldown => 'Повтор отложен',
GeofenceStatusKind.triggered => 'Геофенс уже сработал',
GeofenceStatusKind.ready => 'Геофенс активен',
};
}
String get message {
final homeName = activeHome?.name ?? 'активного дома';
return switch (status) {
GeofenceStatusKind.noActiveHome =>
'Сначала выберите активный дом. Без этого фоновой автоматике нечего отслеживать.',
GeofenceStatusKind.disabled =>
'Для дома "$homeName" автовыключение пока отключено.',
GeofenceStatusKind.missingCoordinates =>
'У дома "$homeName" не заданы координаты. Без них расстояние считать не из чего.',
GeofenceStatusKind.locationServicesDisabled =>
'На устройстве выключена геолокация. И карта расстояний, и фоновый geofence сейчас слепые как кроты.',
GeofenceStatusKind.locationPermissionDenied =>
'Приложению не дали доступ к геолокации. Разрешите доступ хотя бы во время использования.',
GeofenceStatusKind.backgroundPermissionDenied =>
'Для фонового geofence нужен доступ к локации "Всегда". Иначе в фоне Android эту магию задушит.',
GeofenceStatusKind.notificationsPermissionDenied =>
'Свет выключить мы ещё попробуем, но честно отчитаться пользователю не сможем, пока уведомления запрещены.',
GeofenceStatusKind.cooldown =>
'Последняя попытка выключить свет сорвалась. Повторим позже, чтобы не долбить backend без остановки.',
GeofenceStatusKind.triggered =>
'Свет уже был автоматически выключен. Повторно бахать не будем, пока вы не вернётесь в домашний радиус.',
GeofenceStatusKind.ready =>
'Фоновая автоматика готова следить за домом "$homeName" и выключить свет после ухода.',
};
}
String? get secondaryMessage {
if (status == GeofenceStatusKind.cooldown && retryRemaining != null) {
return 'Повтор через ${formatGeofenceRetry(retryRemaining!)}.';
}
if (status == GeofenceStatusKind.triggered &&
runtime.lastSuccessAt != null &&
runtime.lastSuccessHomeId == activeHome?.id) {
return 'Последнее успешное срабатывание уже зафиксировано.';
}
if (detail != null && detail!.trim().isNotEmpty) {
return detail;
}
return null;
}
bool get canRequestLocation =>
status == GeofenceStatusKind.locationPermissionDenied;
bool get canRequestBackgroundLocation =>
status == GeofenceStatusKind.backgroundPermissionDenied;
bool get canOpenLocationSettings =>
status == GeofenceStatusKind.locationServicesDisabled;
bool get canRequestNotifications =>
status == GeofenceStatusKind.notificationsPermissionDenied;
}

View File

@@ -1,193 +0,0 @@
class GeofenceRuntimeState {
final String? armedHomeId;
final DateTime? lastCheckAt;
final double? lastDistanceMeters;
final String? triggeredHomeId;
final DateTime? triggeredAt;
final String? lastSuccessHomeId;
final DateTime? lastSuccessAt;
final String? lastFailureHomeId;
final DateTime? lastFailureAt;
final String? lastFailureMessage;
const GeofenceRuntimeState({
this.armedHomeId,
this.lastCheckAt,
this.lastDistanceMeters,
this.triggeredHomeId,
this.triggeredAt,
this.lastSuccessHomeId,
this.lastSuccessAt,
this.lastFailureHomeId,
this.lastFailureAt,
this.lastFailureMessage,
});
bool isTriggeredFor(String homeId) =>
triggeredHomeId == homeId && triggeredAt != null;
DateTime? failureAtFor(String homeId) {
if (lastFailureHomeId != homeId) return null;
return lastFailureAt;
}
String? failureMessageFor(String homeId) {
if (lastFailureHomeId != homeId) return null;
return lastFailureMessage;
}
GeofenceRuntimeState armForHome(String homeId) {
if (armedHomeId == homeId) return this;
return GeofenceRuntimeState(
armedHomeId: homeId,
lastSuccessHomeId: lastSuccessHomeId,
lastSuccessAt: lastSuccessAt,
lastFailureHomeId: lastFailureHomeId,
lastFailureAt: lastFailureAt,
lastFailureMessage: lastFailureMessage,
);
}
GeofenceRuntimeState disarm() {
return GeofenceRuntimeState(
lastSuccessHomeId: lastSuccessHomeId,
lastSuccessAt: lastSuccessAt,
lastFailureHomeId: lastFailureHomeId,
lastFailureAt: lastFailureAt,
lastFailureMessage: lastFailureMessage,
);
}
GeofenceRuntimeState recordInsideHome(
String homeId, {
required DateTime checkedAt,
required double distanceMeters,
}) {
return GeofenceRuntimeState(
armedHomeId: armedHomeId,
lastCheckAt: checkedAt,
lastDistanceMeters: distanceMeters,
lastSuccessHomeId: lastSuccessHomeId,
lastSuccessAt: lastSuccessAt,
lastFailureHomeId: lastFailureHomeId == homeId ? null : lastFailureHomeId,
lastFailureAt: lastFailureHomeId == homeId ? null : lastFailureAt,
lastFailureMessage: lastFailureHomeId == homeId
? null
: lastFailureMessage,
);
}
GeofenceRuntimeState recordOutsideCheck(
String homeId, {
required DateTime checkedAt,
required double distanceMeters,
}) {
return GeofenceRuntimeState(
armedHomeId: armedHomeId ?? homeId,
lastCheckAt: checkedAt,
lastDistanceMeters: distanceMeters,
triggeredHomeId: triggeredHomeId,
triggeredAt: triggeredAt,
lastSuccessHomeId: lastSuccessHomeId,
lastSuccessAt: lastSuccessAt,
lastFailureHomeId: lastFailureHomeId,
lastFailureAt: lastFailureAt,
lastFailureMessage: lastFailureMessage,
);
}
GeofenceRuntimeState recordSuccess(
String homeId, {
required DateTime triggeredAt,
required double distanceMeters,
}) {
return GeofenceRuntimeState(
armedHomeId: homeId,
lastCheckAt: triggeredAt,
lastDistanceMeters: distanceMeters,
triggeredHomeId: homeId,
triggeredAt: triggeredAt,
lastSuccessHomeId: homeId,
lastSuccessAt: triggeredAt,
lastFailureHomeId: lastFailureHomeId == homeId ? null : lastFailureHomeId,
lastFailureAt: lastFailureHomeId == homeId ? null : lastFailureAt,
lastFailureMessage: lastFailureHomeId == homeId
? null
: lastFailureMessage,
);
}
GeofenceRuntimeState recordFailure(
String homeId, {
required DateTime failedAt,
required double distanceMeters,
required String message,
}) {
return GeofenceRuntimeState(
armedHomeId: homeId,
lastCheckAt: failedAt,
lastDistanceMeters: distanceMeters,
triggeredHomeId: triggeredHomeId == homeId ? null : triggeredHomeId,
triggeredAt: triggeredHomeId == homeId ? null : triggeredAt,
lastSuccessHomeId: lastSuccessHomeId,
lastSuccessAt: lastSuccessAt,
lastFailureHomeId: homeId,
lastFailureAt: failedAt,
lastFailureMessage: message,
);
}
GeofenceRuntimeState removeHome(String homeId) {
return GeofenceRuntimeState(
armedHomeId: armedHomeId == homeId ? null : armedHomeId,
lastCheckAt: armedHomeId == homeId ? null : lastCheckAt,
lastDistanceMeters: armedHomeId == homeId ? null : lastDistanceMeters,
triggeredHomeId: triggeredHomeId == homeId ? null : triggeredHomeId,
triggeredAt: triggeredHomeId == homeId ? null : triggeredAt,
lastSuccessHomeId: lastSuccessHomeId == homeId ? null : lastSuccessHomeId,
lastSuccessAt: lastSuccessHomeId == homeId ? null : lastSuccessAt,
lastFailureHomeId: lastFailureHomeId == homeId ? null : lastFailureHomeId,
lastFailureAt: lastFailureHomeId == homeId ? null : lastFailureAt,
lastFailureMessage: lastFailureHomeId == homeId
? null
: lastFailureMessage,
);
}
Map<String, dynamic> toJson() => {
if (armedHomeId != null) 'armedHomeId': armedHomeId,
if (lastCheckAt != null) 'lastCheckAt': lastCheckAt!.millisecondsSinceEpoch,
if (lastDistanceMeters != null) 'lastDistanceMeters': lastDistanceMeters,
if (triggeredHomeId != null) 'triggeredHomeId': triggeredHomeId,
if (triggeredAt != null) 'triggeredAt': triggeredAt!.millisecondsSinceEpoch,
if (lastSuccessHomeId != null) 'lastSuccessHomeId': lastSuccessHomeId,
if (lastSuccessAt != null)
'lastSuccessAt': lastSuccessAt!.millisecondsSinceEpoch,
if (lastFailureHomeId != null) 'lastFailureHomeId': lastFailureHomeId,
if (lastFailureAt != null)
'lastFailureAt': lastFailureAt!.millisecondsSinceEpoch,
if (lastFailureMessage != null) 'lastFailureMessage': lastFailureMessage,
};
factory GeofenceRuntimeState.fromJson(Map<String, dynamic> json) {
return GeofenceRuntimeState(
armedHomeId: json['armedHomeId'] as String?,
lastCheckAt: _fromMillis(json['lastCheckAt']),
lastDistanceMeters: (json['lastDistanceMeters'] as num?)?.toDouble(),
triggeredHomeId: json['triggeredHomeId'] as String?,
triggeredAt: _fromMillis(json['triggeredAt']),
lastSuccessHomeId: json['lastSuccessHomeId'] as String?,
lastSuccessAt: _fromMillis(json['lastSuccessAt']),
lastFailureHomeId: json['lastFailureHomeId'] as String?,
lastFailureAt: _fromMillis(json['lastFailureAt']),
lastFailureMessage: json['lastFailureMessage'] as String?,
);
}
static DateTime? _fromMillis(Object? value) {
final millis = (value as num?)?.toInt();
if (millis == null) return null;
return DateTime.fromMillisecondsSinceEpoch(millis);
}
}

View File

@@ -1,226 +0,0 @@
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,
);
}
}

View File

@@ -1,8 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../models/home_config.dart';
import '../geofence_task_sync.dart';
import '../services/geofence_runtime_store.dart';
import '../../auth/providers/auth_providers.dart';
import '../../shared/providers/core_providers.dart';
@@ -22,6 +20,7 @@ class CurrentHomeNotifier extends Notifier<HomeConfig?> {
if (state != null) {
await _initApi(state!);
}
await ref.read(geofenceAutomationServiceProvider).syncActiveHome(state);
}
/// Переключиться на другой дом
@@ -30,6 +29,7 @@ class CurrentHomeNotifier extends Notifier<HomeConfig?> {
await svc.setCurrentHomeId(home.id);
state = home;
await _initApi(home);
await ref.read(geofenceAutomationServiceProvider).syncActiveHome(state);
}
/// Выбрать дом как активный и сразу проверить auth-state.
@@ -41,11 +41,9 @@ class CurrentHomeNotifier extends Notifier<HomeConfig?> {
try {
await switchTo(home);
await ref.read(authInfoProvider.notifier).load(failOnError: true);
await syncGeofenceTask(ref.read(homesProvider), currentHome: state);
} catch (error) {
await _restoreSelection(previousHome);
ref.read(authInfoProvider.notifier).restore(previousAuthState);
await syncGeofenceTask(ref.read(homesProvider), currentHome: state);
rethrow;
}
}
@@ -53,7 +51,7 @@ class CurrentHomeNotifier extends Notifier<HomeConfig?> {
Future<void> clear() async {
await ref.read(settingsServiceProvider).setCurrentHomeId(null);
state = null;
await syncGeofenceTask(ref.read(homesProvider), currentHome: null);
await ref.read(geofenceAutomationServiceProvider).syncActiveHome(null);
}
/// Инициализировать API-клиент текущим домом
@@ -74,6 +72,7 @@ class CurrentHomeNotifier extends Notifier<HomeConfig?> {
await svc.setCurrentHomeId(home.id);
state = home;
await _initApi(home);
await ref.read(geofenceAutomationServiceProvider).syncActiveHome(state);
}
}
@@ -96,7 +95,6 @@ class HomesNotifier extends Notifier<List<HomeConfig>> {
Future<void> remove(String id) async {
await ref.read(settingsServiceProvider).deleteHome(id);
await GeofenceRuntimeStore().removeHome(id);
await load();
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter/services.dart';
import '../../../models/home_config.dart';
import '../../../services/settings_service.dart';
class GeofenceAutomationService {
GeofenceAutomationService({SettingsService? settingsService})
: _settingsService = settingsService ?? SettingsService();
static const _channel = MethodChannel('ignis/geofence_automation');
final SettingsService _settingsService;
Future<void> syncActiveHome(HomeConfig? home) async {
if (home == null || !home.geofenceReady) {
await _invoke('disarmGeofence');
return;
}
final apiKey = await _settingsService.requireHomeApiKey(home.id);
await _invoke('armGeofence', {
'homeId': home.id,
'baseUrl': home.url,
'apiKey': apiKey,
'latitude': home.latitude,
'longitude': home.longitude,
'radiusMeters': home.geofenceRadiusMeters,
});
}
Future<void> _invoke(String method, [Map<String, Object?>? arguments]) async {
try {
await _channel.invokeMethod<void>(method, arguments);
} on MissingPluginException {
// В тестах и на не-Android средах platform channel может отсутствовать.
} on PlatformException {
rethrow;
}
}
}

View File

@@ -1,53 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
class GeofenceNotificationsService {
final FlutterLocalNotificationsPlugin _plugin;
GeofenceNotificationsService({FlutterLocalNotificationsPlugin? plugin})
: _plugin = plugin ?? FlutterLocalNotificationsPlugin();
Future<void> initialize() async {
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) {
return;
}
const settings = InitializationSettings(
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
);
await _plugin.initialize(settings);
}
Future<bool> areNotificationsEnabled() async {
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) {
return true;
}
try {
final android = _plugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
return await android?.areNotificationsEnabled() ?? true;
} catch (_) {
return true;
}
}
Future<bool> requestNotificationsPermission() async {
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) {
return true;
}
try {
final android = _plugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin
>();
final granted = await android?.requestNotificationsPermission();
return granted ?? await areNotificationsEnabled();
} catch (_) {
return false;
}
}
}

View File

@@ -1,53 +0,0 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/geofence_runtime_state.dart';
class GeofenceRuntimeStore {
static const String _runtimeKey = 'ignis_geofence_runtime';
Future<GeofenceRuntimeState> load() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(_runtimeKey);
if (raw == null || raw.isEmpty) {
return const GeofenceRuntimeState();
}
final decoded = jsonDecode(raw);
if (decoded is! Map<String, dynamic>) {
return const GeofenceRuntimeState();
}
return GeofenceRuntimeState.fromJson(decoded);
}
Future<void> save(GeofenceRuntimeState state) async {
final prefs = await SharedPreferences.getInstance();
final data = state.toJson();
if (data.isEmpty) {
await prefs.remove(_runtimeKey);
return;
}
await prefs.setString(_runtimeKey, jsonEncode(data));
}
Future<GeofenceRuntimeState> armForHome(String homeId) async {
final next = (await load()).armForHome(homeId);
await save(next);
return next;
}
Future<GeofenceRuntimeState> disarm() async {
final next = (await load()).disarm();
await save(next);
return next;
}
Future<GeofenceRuntimeState> removeHome(String homeId) async {
final next = (await load()).removeHome(homeId);
await save(next);
return next;
}
}

View File

@@ -11,6 +11,8 @@ import '../../../services/api_client.dart';
import '../../homes/providers/homes_providers.dart';
import '../../shared/providers/core_providers.dart';
final remotePollingEnabledProvider = Provider<bool>((ref) => true);
final groupsProvider = NotifierProvider<GroupsNotifier, List<IgnisGroup>>(
() => GroupsNotifier(),
);

View File

@@ -1,5 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../homes/services/geofence_automation_service.dart';
import '../../../services/api_client.dart';
import '../../../services/settings_service.dart';
@@ -8,3 +9,9 @@ final settingsServiceProvider = Provider((ref) => SettingsService());
/// API-клиент текущего дома. Конфигурация меняется через init().
final apiProvider = Provider((ref) => IgnisApi());
/// Нативная geofence-автоматика Android.
final geofenceAutomationServiceProvider = Provider(
(ref) =>
GeofenceAutomationService(settingsService: ref.read(settingsServiceProvider)),
);