Show explicit geofence permission status
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
package ru.akokos.ignis_app
|
package ru.akokos.ignis_app
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
@@ -59,8 +66,33 @@ class MainActivity : FlutterActivity() {
|
|||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"getNotificationPermissionStatus" -> {
|
||||||
|
result.success(
|
||||||
|
if (isNotificationPermissionEnabled()) "enabled" else "disabled",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"openNotificationSettings" -> {
|
||||||
|
val intent =
|
||||||
|
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
|
||||||
|
putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
|
||||||
|
putExtra("android.provider.extra.APP_PACKAGE", packageName)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isNotificationPermissionEnabled(): Boolean {
|
||||||
|
val runtimePermissionGranted =
|
||||||
|
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS,
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
return runtimePermissionGranted &&
|
||||||
|
NotificationManagerCompat.from(this).areNotificationsEnabled()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
enum NotificationPermissionStatus {
|
||||||
|
enabled,
|
||||||
|
disabled,
|
||||||
|
unsupported;
|
||||||
|
|
||||||
|
static NotificationPermissionStatus fromPlatformValue(String? value) {
|
||||||
|
return switch (value) {
|
||||||
|
'enabled' => NotificationPermissionStatus.enabled,
|
||||||
|
'disabled' => NotificationPermissionStatus.disabled,
|
||||||
|
_ => NotificationPermissionStatus.unsupported,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
|
||||||
@@ -5,6 +6,7 @@ import '../../homes/providers/homes_providers.dart';
|
|||||||
import '../../shared/providers/core_providers.dart';
|
import '../../shared/providers/core_providers.dart';
|
||||||
import '../models/app_theme_preset.dart';
|
import '../models/app_theme_preset.dart';
|
||||||
import '../models/geofence_system_state.dart';
|
import '../models/geofence_system_state.dart';
|
||||||
|
import '../models/notification_permission_status.dart';
|
||||||
|
|
||||||
final initialAppThemePresetProvider = Provider<AppThemePreset>(
|
final initialAppThemePresetProvider = Provider<AppThemePreset>(
|
||||||
(ref) => AppThemePreset.fallback,
|
(ref) => AppThemePreset.fallback,
|
||||||
@@ -87,3 +89,45 @@ final geofenceSystemStatusProvider = FutureProvider<GeofenceSystemState>((
|
|||||||
hasCoordinates: currentHome?.hasCoordinates == true,
|
hasCoordinates: currentHome?.hasCoordinates == true,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
abstract class NotificationPermissionStatusService {
|
||||||
|
Future<NotificationPermissionStatus> inspect();
|
||||||
|
|
||||||
|
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> openSettings() async {
|
||||||
|
try {
|
||||||
|
await _channel.invokeMethod<void>('openNotificationSettings');
|
||||||
|
} on MissingPluginException {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final notificationPermissionStatusServiceProvider =
|
||||||
|
Provider<NotificationPermissionStatusService>(
|
||||||
|
(ref) => DeviceNotificationPermissionStatusService(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final notificationPermissionStatusProvider =
|
||||||
|
FutureProvider<NotificationPermissionStatus>((ref) async {
|
||||||
|
return ref.watch(notificationPermissionStatusServiceProvider).inspect();
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '../app/error_message.dart';
|
|||||||
import '../app/load_state.dart';
|
import '../app/load_state.dart';
|
||||||
import '../features/settings/models/app_theme_preset.dart';
|
import '../features/settings/models/app_theme_preset.dart';
|
||||||
import '../features/settings/models/geofence_system_state.dart';
|
import '../features/settings/models/geofence_system_state.dart';
|
||||||
|
import '../features/settings/models/notification_permission_status.dart';
|
||||||
import '../features/settings/providers/settings_providers.dart';
|
import '../features/settings/providers/settings_providers.dart';
|
||||||
import '../models/home_config.dart';
|
import '../models/home_config.dart';
|
||||||
import '../providers/providers.dart';
|
import '../providers/providers.dart';
|
||||||
@@ -44,6 +45,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
|||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
ref.invalidate(geofenceSystemStatusProvider);
|
ref.invalidate(geofenceSystemStatusProvider);
|
||||||
|
ref.invalidate(notificationPermissionStatusProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +56,8 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
|||||||
final themePreset = ref.watch(appThemeProvider);
|
final themePreset = ref.watch(appThemeProvider);
|
||||||
final geofenceStatus = ref.watch(geofenceSystemStatusProvider);
|
final geofenceStatus = ref.watch(geofenceSystemStatusProvider);
|
||||||
final geofenceState = geofenceStatus.asData?.value;
|
final geofenceState = geofenceStatus.asData?.value;
|
||||||
|
final notificationStatus = ref.watch(notificationPermissionStatusProvider);
|
||||||
|
final notificationPermission = notificationStatus.asData?.value;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('НАСТРОЙКИ')),
|
appBar: AppBar(title: const Text('НАСТРОЙКИ')),
|
||||||
@@ -140,6 +144,25 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
|||||||
title: _statusTitle(geofenceState, currentHome),
|
title: _statusTitle(geofenceState, currentHome),
|
||||||
subtitle: _statusSubtitle(geofenceState, currentHome),
|
subtitle: _statusSubtitle(geofenceState, currentHome),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_PermissionTile(
|
||||||
|
icon: _locationPermissionIcon(geofenceState),
|
||||||
|
color: _locationPermissionColor(context, geofenceState),
|
||||||
|
title: 'Геолокация',
|
||||||
|
subtitle: _locationPermissionSubtitle(geofenceState),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_PermissionTile(
|
||||||
|
icon: _notificationPermissionIcon(notificationPermission),
|
||||||
|
color: _notificationPermissionColor(
|
||||||
|
context,
|
||||||
|
notificationPermission,
|
||||||
|
),
|
||||||
|
title: 'Уведомления',
|
||||||
|
subtitle: _notificationPermissionSubtitle(
|
||||||
|
notificationPermission,
|
||||||
|
),
|
||||||
|
),
|
||||||
if (_savingGeofence)
|
if (_savingGeofence)
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.only(top: 12),
|
padding: EdgeInsets.only(top: 12),
|
||||||
@@ -165,6 +188,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
|||||||
context: context,
|
context: context,
|
||||||
home: currentHome,
|
home: currentHome,
|
||||||
systemState: geofenceState,
|
systemState: geofenceState,
|
||||||
|
notificationPermission: notificationPermission,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -238,8 +262,12 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
|||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required HomeConfig home,
|
required HomeConfig home,
|
||||||
required GeofenceSystemState? systemState,
|
required GeofenceSystemState? systemState,
|
||||||
|
required NotificationPermissionStatus? notificationPermission,
|
||||||
}) {
|
}) {
|
||||||
final locationNotifier = ref.read(userLocationProvider.notifier);
|
final locationNotifier = ref.read(userLocationProvider.notifier);
|
||||||
|
final notificationNotifier = ref.read(
|
||||||
|
notificationPermissionStatusServiceProvider,
|
||||||
|
);
|
||||||
final issue = systemState?.issue;
|
final issue = systemState?.issue;
|
||||||
|
|
||||||
if (!home.hasCoordinates) {
|
if (!home.hasCoordinates) {
|
||||||
@@ -252,7 +280,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return switch (issue) {
|
final actions = switch (issue) {
|
||||||
GeofenceSystemIssue.locationServicesDisabled => [
|
GeofenceSystemIssue.locationServicesDisabled => [
|
||||||
FilledButton.tonalIcon(
|
FilledButton.tonalIcon(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -292,6 +320,21 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (notificationPermission == NotificationPermissionStatus.disabled) {
|
||||||
|
actions.add(
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
await notificationNotifier.openSettings();
|
||||||
|
ref.invalidate(notificationPermissionStatusProvider);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.notifications_outlined),
|
||||||
|
label: const Text('Уведомления Android'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _setGeofenceEnabled(HomeConfig home, bool enabled) async {
|
Future<void> _setGeofenceEnabled(HomeConfig home, bool enabled) async {
|
||||||
@@ -536,6 +579,89 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IconData _locationPermissionIcon(GeofenceSystemState? state) {
|
||||||
|
return switch (state?.issue) {
|
||||||
|
GeofenceSystemIssue.ready => Icons.verified_outlined,
|
||||||
|
GeofenceSystemIssue.locationServicesDisabled => Icons.location_disabled,
|
||||||
|
GeofenceSystemIssue.permissionDenied ||
|
||||||
|
GeofenceSystemIssue.permissionDeniedForever ||
|
||||||
|
GeofenceSystemIssue.backgroundPermissionRequired =>
|
||||||
|
Icons.gpp_bad_outlined,
|
||||||
|
_ => Icons.my_location_outlined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _locationPermissionColor(
|
||||||
|
BuildContext context,
|
||||||
|
GeofenceSystemState? state,
|
||||||
|
) {
|
||||||
|
return switch (state?.issue) {
|
||||||
|
GeofenceSystemIssue.ready => Colors.green,
|
||||||
|
GeofenceSystemIssue.locationServicesDisabled ||
|
||||||
|
GeofenceSystemIssue.permissionDenied ||
|
||||||
|
GeofenceSystemIssue.permissionDeniedForever ||
|
||||||
|
GeofenceSystemIssue.backgroundPermissionRequired => Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.error,
|
||||||
|
_ => Colors.white70,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String _locationPermissionSubtitle(GeofenceSystemState? state) {
|
||||||
|
return switch (state?.issue) {
|
||||||
|
GeofenceSystemIssue.ready =>
|
||||||
|
'Доступ «Всегда», geofence может работать в фоне.',
|
||||||
|
GeofenceSystemIssue.locationServicesDisabled =>
|
||||||
|
'Системная геолокация сейчас выключена.',
|
||||||
|
GeofenceSystemIssue.permissionDenied =>
|
||||||
|
'Разрешение на геолокацию ещё не выдано.',
|
||||||
|
GeofenceSystemIssue.permissionDeniedForever =>
|
||||||
|
'Доступ к геолокации запрещён в Android.',
|
||||||
|
GeofenceSystemIssue.backgroundPermissionRequired =>
|
||||||
|
'Есть только доступ при использовании приложения.',
|
||||||
|
GeofenceSystemIssue.missingCoordinates =>
|
||||||
|
'Сначала задай координаты дома, потом появится полный статус.',
|
||||||
|
GeofenceSystemIssue.noActiveHome =>
|
||||||
|
'Без активного дома проверять нечего.',
|
||||||
|
null => 'Проверяем системный статус геолокации.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _notificationPermissionIcon(NotificationPermissionStatus? status) {
|
||||||
|
return switch (status) {
|
||||||
|
NotificationPermissionStatus.enabled =>
|
||||||
|
Icons.notifications_active_outlined,
|
||||||
|
NotificationPermissionStatus.disabled => Icons.notifications_off_outlined,
|
||||||
|
NotificationPermissionStatus.unsupported ||
|
||||||
|
null => Icons.notifications_none_outlined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _notificationPermissionColor(
|
||||||
|
BuildContext context,
|
||||||
|
NotificationPermissionStatus? status,
|
||||||
|
) {
|
||||||
|
return switch (status) {
|
||||||
|
NotificationPermissionStatus.enabled => Colors.green,
|
||||||
|
NotificationPermissionStatus.disabled => Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.error,
|
||||||
|
NotificationPermissionStatus.unsupported || null => Colors.white70,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String _notificationPermissionSubtitle(NotificationPermissionStatus? status) {
|
||||||
|
return switch (status) {
|
||||||
|
NotificationPermissionStatus.enabled =>
|
||||||
|
'После срабатывания geofence приложение сможет прислать подтверждение.',
|
||||||
|
NotificationPermissionStatus.disabled =>
|
||||||
|
'Автовыключение сработает и без них, но подтверждение ты не увидишь.',
|
||||||
|
NotificationPermissionStatus.unsupported =>
|
||||||
|
'На этой платформе отдельный статус уведомлений не используется.',
|
||||||
|
null => 'Проверяем, доступны ли уведомления для подтверждения.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _openHomeEditor(BuildContext context, HomeConfig home) async {
|
Future<void> _openHomeEditor(BuildContext context, HomeConfig home) async {
|
||||||
await Navigator.of(
|
await Navigator.of(
|
||||||
context,
|
context,
|
||||||
@@ -679,6 +805,41 @@ class _StatusTile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _PermissionTile extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
|
||||||
|
const _PermissionTile({
|
||||||
|
required this.icon,
|
||||||
|
required this.color,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: color, size: 20),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(title, style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(subtitle, style: const TextStyle(color: Colors.white54)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _EmptySectionState extends StatelessWidget {
|
class _EmptySectionState extends StatelessWidget {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String title;
|
final String title;
|
||||||
|
|||||||
43
test/notification_permission_status_provider_test.dart
Normal file
43
test/notification_permission_status_provider_test.dart
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:ignis_app/features/settings/models/notification_permission_status.dart';
|
||||||
|
import 'package:ignis_app/features/settings/providers/settings_providers.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
test(
|
||||||
|
'notification permission status provider returns service result',
|
||||||
|
() async {
|
||||||
|
final container = ProviderContainer(
|
||||||
|
overrides: [
|
||||||
|
notificationPermissionStatusServiceProvider.overrideWithValue(
|
||||||
|
_FakeNotificationPermissionStatusService(
|
||||||
|
NotificationPermissionStatus.disabled,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
addTearDown(container.dispose);
|
||||||
|
|
||||||
|
final status = await container.read(
|
||||||
|
notificationPermissionStatusProvider.future,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(status, NotificationPermissionStatus.disabled);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeNotificationPermissionStatusService
|
||||||
|
implements NotificationPermissionStatusService {
|
||||||
|
final NotificationPermissionStatus result;
|
||||||
|
|
||||||
|
_FakeNotificationPermissionStatusService(this.result);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<NotificationPermissionStatus> inspect() async => result;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> openSettings() async {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user