Request notification permission for geofence alerts
This commit is contained in:
@@ -14,7 +14,7 @@ Android-клиент для self-hosted backend [Ignis Core](https://git.akokos.
|
||||
|
||||
## Гео-автоматизация
|
||||
|
||||
Для активного дома приложение может зарегистрировать системный Android geofence. После подтверждённого `EXIT` запускается короткая фоновая задача, которая проверяет группы и выключает только те, что реально включены.
|
||||
Для активного дома приложение может зарегистрировать системный Android geofence. После подтверждённого `EXIT` запускается короткая фоновая задача, которая проверяет группы и выключает только те, что реально включены. После успешной фоновой обработки приложение может показать локальное подтверждение через Android notifications.
|
||||
|
||||
Это не polling каждые 15 минут. Основной триггер здесь событийный:
|
||||
- geofence регистрируется нативно через Android geofencing API;
|
||||
@@ -102,6 +102,7 @@ flutter test
|
||||
2. При необходимости задать координаты дома.
|
||||
3. Включить "выключать свет при уходе".
|
||||
4. Выдать Android-разрешения на геолокацию, включая background location.
|
||||
5. Разрешить уведомления, если нужны подтверждения о срабатывании geofence.
|
||||
|
||||
API-ключи хранятся отдельно от списка домов в `flutter_secure_storage`. Старые ключи из `SharedPreferences` мигрируются автоматически.
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
@@ -12,6 +13,8 @@ import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private var pendingNotificationPermissionResult: MethodChannel.Result? = null
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
@@ -67,9 +70,10 @@ class MainActivity : FlutterActivity() {
|
||||
}
|
||||
}
|
||||
"getNotificationPermissionStatus" -> {
|
||||
result.success(
|
||||
if (isNotificationPermissionEnabled()) "enabled" else "disabled",
|
||||
)
|
||||
result.success(getNotificationPermissionStatusValue())
|
||||
}
|
||||
"requestNotificationPermission" -> {
|
||||
requestNotificationPermission(result)
|
||||
}
|
||||
"openNotificationSettings" -> {
|
||||
val intent =
|
||||
@@ -85,6 +89,70 @@ class MainActivity : FlutterActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray,
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
if (requestCode != notificationPermissionRequestCode) {
|
||||
return
|
||||
}
|
||||
|
||||
pendingNotificationPermissionResult?.success(getNotificationPermissionStatusValue())
|
||||
pendingNotificationPermissionResult = null
|
||||
}
|
||||
|
||||
private fun requestNotificationPermission(result: MethodChannel.Result) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
result.success(getNotificationPermissionStatusValue())
|
||||
return
|
||||
}
|
||||
|
||||
if (pendingNotificationPermissionResult != null) {
|
||||
result.error(
|
||||
"request_in_progress",
|
||||
"Notification permission request is already in progress",
|
||||
null,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
result.success(getNotificationPermissionStatusValue())
|
||||
return
|
||||
}
|
||||
|
||||
pendingNotificationPermissionResult = result
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
notificationPermissionRequestCode,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getNotificationPermissionStatusValue(): String {
|
||||
if (isNotificationPermissionEnabled()) {
|
||||
return "enabled"
|
||||
}
|
||||
|
||||
val runtimePermissionGranted =
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
|
||||
ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.POST_NOTIFICATIONS,
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
return if (!runtimePermissionGranted) {
|
||||
"requestable"
|
||||
} else {
|
||||
"settings_required"
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNotificationPermissionEnabled(): Boolean {
|
||||
val runtimePermissionGranted =
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
|
||||
@@ -95,4 +163,8 @@ class MainActivity : FlutterActivity() {
|
||||
return runtimePermissionGranted &&
|
||||
NotificationManagerCompat.from(this).areNotificationsEnabled()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val notificationPermissionRequestCode = 4102
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
enum NotificationPermissionStatus {
|
||||
enabled,
|
||||
disabled,
|
||||
requestable,
|
||||
settingsRequired,
|
||||
unsupported;
|
||||
|
||||
static NotificationPermissionStatus fromPlatformValue(String? value) {
|
||||
return switch (value) {
|
||||
'enabled' => NotificationPermissionStatus.enabled,
|
||||
'disabled' => NotificationPermissionStatus.disabled,
|
||||
'requestable' => NotificationPermissionStatus.requestable,
|
||||
'settings_required' => NotificationPermissionStatus.settingsRequired,
|
||||
_ => NotificationPermissionStatus.unsupported,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -93,6 +93,8 @@ final geofenceSystemStatusProvider = FutureProvider<GeofenceSystemState>((
|
||||
abstract class NotificationPermissionStatusService {
|
||||
Future<NotificationPermissionStatus> inspect();
|
||||
|
||||
Future<void> requestPermission();
|
||||
|
||||
Future<void> openSettings();
|
||||
}
|
||||
|
||||
@@ -112,6 +114,15 @@ class DeviceNotificationPermissionStatusService
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestPermission() async {
|
||||
try {
|
||||
await _channel.invokeMethod<void>('requestNotificationPermission');
|
||||
} on MissingPluginException {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> openSettings() async {
|
||||
try {
|
||||
|
||||
@@ -58,11 +58,12 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
||||
final geofenceState = geofenceStatus.asData?.value;
|
||||
final notificationStatus = ref.watch(notificationPermissionStatusProvider);
|
||||
final notificationPermission = notificationStatus.asData?.value;
|
||||
final bottomInset = MediaQuery.paddingOf(context).bottom;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('НАСТРОЙКИ')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, bottomInset + 24),
|
||||
children: [
|
||||
_SectionTitle(
|
||||
title: 'Дом и подключение',
|
||||
@@ -321,7 +322,21 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
||||
],
|
||||
};
|
||||
|
||||
if (notificationPermission == NotificationPermissionStatus.disabled) {
|
||||
if (notificationPermission == NotificationPermissionStatus.requestable) {
|
||||
actions.add(
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () async {
|
||||
await notificationNotifier.requestPermission();
|
||||
ref.invalidate(notificationPermissionStatusProvider);
|
||||
},
|
||||
icon: const Icon(Icons.notifications_active_outlined),
|
||||
label: const Text('Разрешить уведомления'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (notificationPermission ==
|
||||
NotificationPermissionStatus.settingsRequired) {
|
||||
actions.add(
|
||||
OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
@@ -631,7 +646,10 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
||||
return switch (status) {
|
||||
NotificationPermissionStatus.enabled =>
|
||||
Icons.notifications_active_outlined,
|
||||
NotificationPermissionStatus.disabled => Icons.notifications_off_outlined,
|
||||
NotificationPermissionStatus.requestable =>
|
||||
Icons.notifications_off_outlined,
|
||||
NotificationPermissionStatus.settingsRequired =>
|
||||
Icons.notifications_paused_outlined,
|
||||
NotificationPermissionStatus.unsupported ||
|
||||
null => Icons.notifications_none_outlined,
|
||||
};
|
||||
@@ -643,7 +661,8 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
||||
) {
|
||||
return switch (status) {
|
||||
NotificationPermissionStatus.enabled => Colors.green,
|
||||
NotificationPermissionStatus.disabled => Theme.of(
|
||||
NotificationPermissionStatus.requestable ||
|
||||
NotificationPermissionStatus.settingsRequired => Theme.of(
|
||||
context,
|
||||
).colorScheme.error,
|
||||
NotificationPermissionStatus.unsupported || null => Colors.white70,
|
||||
@@ -654,8 +673,10 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen>
|
||||
return switch (status) {
|
||||
NotificationPermissionStatus.enabled =>
|
||||
'После срабатывания geofence приложение сможет прислать подтверждение.',
|
||||
NotificationPermissionStatus.disabled =>
|
||||
'Автовыключение сработает и без них, но подтверждение ты не увидишь.',
|
||||
NotificationPermissionStatus.requestable =>
|
||||
'Android ещё может показать системный запрос на уведомления.',
|
||||
NotificationPermissionStatus.settingsRequired =>
|
||||
'Автовыключение сработает и без них, но подтверждение нужно включить вручную.',
|
||||
NotificationPermissionStatus.unsupported =>
|
||||
'На этой платформе отдельный статус уведомлений не используется.',
|
||||
null => 'Проверяем, доступны ли уведомления для подтверждения.',
|
||||
|
||||
@@ -13,7 +13,7 @@ void main() {
|
||||
overrides: [
|
||||
notificationPermissionStatusServiceProvider.overrideWithValue(
|
||||
_FakeNotificationPermissionStatusService(
|
||||
NotificationPermissionStatus.disabled,
|
||||
NotificationPermissionStatus.requestable,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -24,7 +24,7 @@ void main() {
|
||||
notificationPermissionStatusProvider.future,
|
||||
);
|
||||
|
||||
expect(status, NotificationPermissionStatus.disabled);
|
||||
expect(status, NotificationPermissionStatus.requestable);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -38,6 +38,9 @@ class _FakeNotificationPermissionStatusService
|
||||
@override
|
||||
Future<NotificationPermissionStatus> inspect() async => result;
|
||||
|
||||
@override
|
||||
Future<void> requestPermission() async {}
|
||||
|
||||
@override
|
||||
Future<void> openSettings() async {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user