Show explicit geofence permission status
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
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.android.FlutterActivity
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
@@ -59,8 +66,33 @@ class MainActivity : FlutterActivity() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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:geolocator/geolocator.dart';
|
||||
|
||||
@@ -5,6 +6,7 @@ import '../../homes/providers/homes_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';
|
||||
|
||||
final initialAppThemePresetProvider = Provider<AppThemePreset>(
|
||||
(ref) => AppThemePreset.fallback,
|
||||
@@ -87,3 +89,45 @@ final geofenceSystemStatusProvider = FutureProvider<GeofenceSystemState>((
|
||||
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 '../features/settings/models/app_theme_preset.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 '../models/home_config.dart';
|
||||
import '../providers/providers.dart';
|
||||
@@ -44,6 +45,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
ref.invalidate(geofenceSystemStatusProvider);
|
||||
ref.invalidate(notificationPermissionStatusProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +56,8 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
||||
final themePreset = ref.watch(appThemeProvider);
|
||||
final geofenceStatus = ref.watch(geofenceSystemStatusProvider);
|
||||
final geofenceState = geofenceStatus.asData?.value;
|
||||
final notificationStatus = ref.watch(notificationPermissionStatusProvider);
|
||||
final notificationPermission = notificationStatus.asData?.value;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('НАСТРОЙКИ')),
|
||||
@@ -140,6 +144,25 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
||||
title: _statusTitle(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)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 12),
|
||||
@@ -165,6 +188,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
||||
context: context,
|
||||
home: currentHome,
|
||||
systemState: geofenceState,
|
||||
notificationPermission: notificationPermission,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -238,8 +262,12 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
||||
required BuildContext context,
|
||||
required HomeConfig home,
|
||||
required GeofenceSystemState? systemState,
|
||||
required NotificationPermissionStatus? notificationPermission,
|
||||
}) {
|
||||
final locationNotifier = ref.read(userLocationProvider.notifier);
|
||||
final notificationNotifier = ref.read(
|
||||
notificationPermissionStatusServiceProvider,
|
||||
);
|
||||
final issue = systemState?.issue;
|
||||
|
||||
if (!home.hasCoordinates) {
|
||||
@@ -252,7 +280,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
||||
];
|
||||
}
|
||||
|
||||
return switch (issue) {
|
||||
final actions = switch (issue) {
|
||||
GeofenceSystemIssue.locationServicesDisabled => [
|
||||
FilledButton.tonalIcon(
|
||||
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 {
|
||||
@@ -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 {
|
||||
await Navigator.of(
|
||||
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 {
|
||||
final IconData icon;
|
||||
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