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