Skip to content

Commit bf2b83e

Browse files
authored
Merge pull request #574 from DevKor-github/feature/issue-534-alarm-permission-policy
fix: unify alarm fallback permission policy
2 parents 2c437a7 + 360660e commit bf2b83e

8 files changed

Lines changed: 408 additions & 87 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Keep Delivery Enabled With Fallback Notifications
2+
3+
When exact alarm or AlarmKit permission is denied but notification fallback is available, OnTime will keep schedule notifications enabled and arm them through Fallback Notification delivery. Exact timing or AlarmKit recovery remains a non-blocking improvement path, while disabling schedule notifications is reserved for states where no native or fallback delivery path can currently notify the user.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import 'package:equatable/equatable.dart';
2+
import 'package:on_time_front/domain/entities/alarm_entities.dart';
3+
4+
enum AlarmDeliveryMode { nativeAlarm, fallbackNotification, unavailable }
5+
6+
class AlarmDeliveryPolicy extends Equatable {
7+
const AlarmDeliveryPolicy._({
8+
required this.mode,
9+
required this.activeProvider,
10+
required this.blockingPermissionIssue,
11+
required this.recoveryPermissionIssue,
12+
});
13+
14+
factory AlarmDeliveryPolicy.evaluate({
15+
required AlarmSchedulerCapabilities capabilities,
16+
required AlarmPermissionState nativePermission,
17+
required AlarmPermissionState fallbackPermission,
18+
}) {
19+
if (capabilities.supportsNativeAlarm &&
20+
capabilities.nativeAlarmProvider != AlarmProvider.none &&
21+
nativePermission == AlarmPermissionState.granted) {
22+
return AlarmDeliveryPolicy._(
23+
mode: AlarmDeliveryMode.nativeAlarm,
24+
activeProvider: capabilities.nativeAlarmProvider,
25+
blockingPermissionIssue: null,
26+
recoveryPermissionIssue: null,
27+
);
28+
}
29+
30+
if (capabilities.fallbackProvider == AlarmProvider.localNotification &&
31+
fallbackPermission == AlarmPermissionState.granted) {
32+
return AlarmDeliveryPolicy._(
33+
mode: AlarmDeliveryMode.fallbackNotification,
34+
activeProvider: AlarmProvider.localNotification,
35+
blockingPermissionIssue: null,
36+
recoveryPermissionIssue: _nativeRecoveryIssue(
37+
capabilities,
38+
nativePermission,
39+
),
40+
);
41+
}
42+
43+
final blockingIssue = _blockingPermissionIssue(
44+
capabilities,
45+
nativePermission,
46+
fallbackPermission,
47+
);
48+
return AlarmDeliveryPolicy._(
49+
mode: AlarmDeliveryMode.unavailable,
50+
activeProvider: AlarmProvider.none,
51+
blockingPermissionIssue: blockingIssue,
52+
recoveryPermissionIssue: null,
53+
);
54+
}
55+
56+
final AlarmDeliveryMode mode;
57+
final AlarmProvider activeProvider;
58+
final AlarmPermissionIssue? blockingPermissionIssue;
59+
final AlarmPermissionIssue? recoveryPermissionIssue;
60+
61+
bool get canDeliver => activeProvider != AlarmProvider.none;
62+
63+
bool get isUnsupported {
64+
return mode == AlarmDeliveryMode.unavailable &&
65+
blockingPermissionIssue == null;
66+
}
67+
68+
bool get shouldDisableAlarms {
69+
return mode == AlarmDeliveryMode.unavailable &&
70+
blockingPermissionIssue != null;
71+
}
72+
73+
@override
74+
List<Object?> get props => [
75+
mode,
76+
activeProvider,
77+
blockingPermissionIssue,
78+
recoveryPermissionIssue,
79+
];
80+
81+
static AlarmPermissionIssue? _nativeRecoveryIssue(
82+
AlarmSchedulerCapabilities capabilities,
83+
AlarmPermissionState nativePermission,
84+
) {
85+
if (!capabilities.supportsNativeAlarm ||
86+
capabilities.nativeAlarmProvider == AlarmProvider.none) {
87+
return null;
88+
}
89+
if (nativePermission == AlarmPermissionState.denied ||
90+
nativePermission == AlarmPermissionState.notDetermined) {
91+
return AlarmPermissionIssue.nativePermissionDenied;
92+
}
93+
return null;
94+
}
95+
96+
static AlarmPermissionIssue? _blockingPermissionIssue(
97+
AlarmSchedulerCapabilities capabilities,
98+
AlarmPermissionState nativePermission,
99+
AlarmPermissionState fallbackPermission,
100+
) {
101+
if (capabilities.supportsNativeAlarm &&
102+
capabilities.nativeAlarmProvider != AlarmProvider.none &&
103+
(nativePermission == AlarmPermissionState.denied ||
104+
nativePermission == AlarmPermissionState.notDetermined)) {
105+
return AlarmPermissionIssue.nativePermissionDenied;
106+
}
107+
if (capabilities.fallbackProvider == AlarmProvider.localNotification &&
108+
(fallbackPermission == AlarmPermissionState.denied ||
109+
fallbackPermission == AlarmPermissionState.notDetermined)) {
110+
return AlarmPermissionIssue.notificationPermissionDenied;
111+
}
112+
return null;
113+
}
114+
}

lib/domain/use-cases/reconcile_alarms_use_case.dart

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
22
import 'package:injectable/injectable.dart';
33
import 'package:on_time_front/core/services/alarm_scheduler_service.dart';
44
import 'package:on_time_front/core/services/fallback_alarm_notification_service.dart';
5+
import 'package:on_time_front/domain/entities/alarm_delivery_policy.dart';
56
import 'package:on_time_front/domain/entities/alarm_entities.dart';
67
import 'package:on_time_front/domain/entities/schedule_with_preparation_entity.dart';
78
import 'package:on_time_front/domain/repositories/alarm_registry_repository.dart';
@@ -217,6 +218,11 @@ class ReconcileAlarmsUseCase {
217218
'$_logTag permissions native=$nativePermission '
218219
'fallback=$fallbackPermission',
219220
);
221+
final deliveryPolicy = AlarmDeliveryPolicy.evaluate(
222+
capabilities: capabilities,
223+
nativePermission: nativePermission,
224+
fallbackPermission: fallbackPermission,
225+
);
220226

221227
final finalRecords = <ScheduledAlarmRecord>[];
222228
final failures = <AlarmFailure>[];
@@ -238,7 +244,7 @@ class ReconcileAlarmsUseCase {
238244
final scheduled = await _scheduleRecord(
239245
desired,
240246
capabilities: capabilities,
241-
nativePermission: nativePermission,
247+
deliveryPolicy: deliveryPolicy,
242248
fallbackPermission: fallbackPermission,
243249
);
244250

@@ -282,8 +288,7 @@ class ReconcileAlarmsUseCase {
282288
armedCount: finalRecords.length,
283289
failures: failures,
284290
permissionIssue: permissionIssue,
285-
capabilities: capabilities,
286-
fallbackPermission: fallbackPermission,
291+
deliveryPolicy: deliveryPolicy,
287292
);
288293

289294
final result = _result(
@@ -371,12 +376,10 @@ class ReconcileAlarmsUseCase {
371376
Future<_ScheduleAttempt> _scheduleRecord(
372377
ScheduledAlarmRecord desired, {
373378
required AlarmSchedulerCapabilities capabilities,
374-
required AlarmPermissionState nativePermission,
379+
required AlarmDeliveryPolicy deliveryPolicy,
375380
required AlarmPermissionState fallbackPermission,
376381
}) async {
377-
if (capabilities.supportsNativeAlarm &&
378-
capabilities.nativeAlarmProvider != AlarmProvider.none &&
379-
nativePermission == AlarmPermissionState.granted) {
382+
if (deliveryPolicy.mode == AlarmDeliveryMode.nativeAlarm) {
380383
final nativeRecord = desired.copyWith(
381384
provider: capabilities.nativeAlarmProvider,
382385
);
@@ -415,7 +418,8 @@ class ReconcileAlarmsUseCase {
415418
}
416419
}
417420

418-
if (capabilities.fallbackProvider == AlarmProvider.localNotification &&
421+
if ((deliveryPolicy.mode == AlarmDeliveryMode.fallbackNotification ||
422+
capabilities.fallbackProvider == AlarmProvider.localNotification) &&
419423
fallbackPermission == AlarmPermissionState.granted) {
420424
final fallbackRecord = desired.copyWith(
421425
provider: AlarmProvider.localNotification,
@@ -455,17 +459,9 @@ class ReconcileAlarmsUseCase {
455459
}
456460
}
457461

458-
if (nativePermission == AlarmPermissionState.denied ||
459-
nativePermission == AlarmPermissionState.notDetermined) {
460-
return const _ScheduleAttempt(
461-
permissionIssue: AlarmPermissionIssue.nativePermissionDenied,
462-
);
463-
}
464-
if (fallbackPermission == AlarmPermissionState.denied ||
465-
fallbackPermission == AlarmPermissionState.notDetermined) {
466-
return const _ScheduleAttempt(
467-
permissionIssue: AlarmPermissionIssue.notificationPermissionDenied,
468-
);
462+
final blockingIssue = deliveryPolicy.blockingPermissionIssue;
463+
if (blockingIssue != null) {
464+
return _ScheduleAttempt(permissionIssue: blockingIssue);
469465
}
470466

471467
return const _ScheduleAttempt(
@@ -479,17 +475,15 @@ class ReconcileAlarmsUseCase {
479475
required int armedCount,
480476
required List<AlarmFailure> failures,
481477
required AlarmPermissionIssue? permissionIssue,
482-
required AlarmSchedulerCapabilities capabilities,
483-
required AlarmPermissionState fallbackPermission,
478+
required AlarmDeliveryPolicy deliveryPolicy,
484479
}) {
485-
final hasAnyProvider =
486-
capabilities.supportsNativeAlarm ||
487-
fallbackPermission == AlarmPermissionState.granted;
488-
if (!hasAnyProvider) {
480+
if (!deliveryPolicy.canDeliver) {
489481
if (permissionIssue != null) {
490482
return AlarmReconciliationStatus.permissionNeeded;
491483
}
492-
return AlarmReconciliationStatus.unsupported;
484+
return deliveryPolicy.isUnsupported
485+
? AlarmReconciliationStatus.unsupported
486+
: AlarmReconciliationStatus.permissionNeeded;
493487
}
494488
if (desiredCount > armedCount || failures.isNotEmpty) {
495489
return armedCount == 0 && permissionIssue != null

lib/presentation/app/cubit/alarm_gate_cubit.dart

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:on_time_front/core/di/di_setup.dart';
44
import 'package:on_time_front/core/logging/app_logger.dart';
55
import 'package:on_time_front/core/services/alarm_scheduler_service.dart';
66
import 'package:on_time_front/core/services/fallback_alarm_notification_service.dart';
7+
import 'package:on_time_front/domain/entities/alarm_delivery_policy.dart';
78
import 'package:on_time_front/domain/entities/alarm_entities.dart';
89
import 'package:on_time_front/domain/repositories/alarm_repository.dart';
910
import 'package:on_time_front/domain/use-cases/cancel_all_alarms_use_case.dart';
@@ -45,14 +46,14 @@ class AlarmGateCubit extends Cubit<AlarmGateState> {
4546
bool enableAlarmsOnGrant = false,
4647
}) async {
4748
final capabilities = await _alarmSchedulerService.getCapabilities();
48-
final permission = await _checkEffectivePermission(capabilities);
49-
if (permission == AlarmPermissionState.unsupported) {
49+
final delivery = await _checkDeliveryPolicy(capabilities);
50+
if (delivery.policy.isUnsupported) {
5051
emit(const AlarmGateState.unsupported());
5152
return;
5253
}
5354

5455
final prefs = await SharedPreferences.getInstance();
55-
if (permission == AlarmPermissionState.granted) {
56+
if (delivery.policy.canDeliver) {
5657
await prefs.remove(_dismissedKey);
5758
if (enableAlarmsOnGrant) {
5859
await _enableAlarmsBestEffort();
@@ -62,7 +63,7 @@ class AlarmGateCubit extends Cubit<AlarmGateState> {
6263
}
6364

6465
if (disableAlarmsWhenPermissionMissing &&
65-
!await _hasGrantedFallbackPermission(capabilities)) {
66+
delivery.policy.shouldDisableAlarms) {
6667
await _disableAlarmsBestEffort();
6768
}
6869

@@ -76,57 +77,58 @@ class AlarmGateCubit extends Cubit<AlarmGateState> {
7677

7778
Future<AlarmPermissionState> requestPermission() async {
7879
final capabilities = await _alarmSchedulerService.getCapabilities();
79-
final permission = await _requestEffectivePermission(capabilities);
80-
if (permission == AlarmPermissionState.unsupported) {
80+
final delivery = await _requestDeliveryPolicy(capabilities);
81+
final permission = delivery.permissionState;
82+
if (delivery.policy.isUnsupported) {
8183
emit(const AlarmGateState.unsupported());
8284
return permission;
8385
}
8486

85-
if (permission == AlarmPermissionState.granted) {
87+
if (delivery.policy.canDeliver) {
8688
final prefs = await SharedPreferences.getInstance();
8789
await prefs.remove(_dismissedKey);
8890
await _enableAlarmsBestEffort();
8991
emit(const AlarmGateState.allowed());
9092
return permission;
9193
}
9294

93-
if (!await _hasGrantedFallbackPermission(capabilities)) {
95+
if (delivery.policy.shouldDisableAlarms) {
9496
await _disableAlarmsBestEffort();
9597
}
9698
emit(const AlarmGateState.required());
9799
return permission;
98100
}
99101

100-
Future<AlarmPermissionState> _checkEffectivePermission(
102+
Future<_EvaluatedAlarmDelivery> _checkDeliveryPolicy(
101103
AlarmSchedulerCapabilities capabilities,
102104
) async {
103105
final nativePermission = await _checkNativePermission(capabilities);
104-
if (nativePermission == AlarmPermissionState.granted) {
105-
return nativePermission;
106-
}
107-
108-
if (nativePermission == AlarmPermissionState.unsupported) {
109-
return _checkFallbackPermission(capabilities);
110-
}
111-
return nativePermission;
106+
final fallbackPermission = await _checkFallbackPermission(capabilities);
107+
return _EvaluatedAlarmDelivery(
108+
policy: AlarmDeliveryPolicy.evaluate(
109+
capabilities: capabilities,
110+
nativePermission: nativePermission,
111+
fallbackPermission: fallbackPermission,
112+
),
113+
nativePermission: nativePermission,
114+
fallbackPermission: fallbackPermission,
115+
);
112116
}
113117

114-
Future<AlarmPermissionState> _requestEffectivePermission(
118+
Future<_EvaluatedAlarmDelivery> _requestDeliveryPolicy(
115119
AlarmSchedulerCapabilities capabilities,
116120
) async {
117121
final nativePermission = await _requestNativePermission(capabilities);
118-
if (nativePermission == AlarmPermissionState.granted) {
119-
return nativePermission;
120-
}
121-
122-
if (nativePermission == AlarmPermissionState.unsupported) {
123-
return _requestFallbackPermission(capabilities);
124-
}
125122
final fallbackPermission = await _requestFallbackPermission(capabilities);
126-
if (fallbackPermission == AlarmPermissionState.granted) {
127-
return fallbackPermission;
128-
}
129-
return nativePermission;
123+
return _EvaluatedAlarmDelivery(
124+
policy: AlarmDeliveryPolicy.evaluate(
125+
capabilities: capabilities,
126+
nativePermission: nativePermission,
127+
fallbackPermission: fallbackPermission,
128+
),
129+
nativePermission: nativePermission,
130+
fallbackPermission: fallbackPermission,
131+
);
130132
}
131133

132134
Future<AlarmPermissionState> _checkNativePermission(
@@ -171,7 +173,8 @@ class AlarmGateCubit extends Cubit<AlarmGateState> {
171173
final prefs = await SharedPreferences.getInstance();
172174
await prefs.setBool(_dismissedKey, true);
173175
final capabilities = await _alarmSchedulerService.getCapabilities();
174-
if (!await _hasGrantedFallbackPermission(capabilities)) {
176+
final delivery = await _checkDeliveryPolicy(capabilities);
177+
if (delivery.policy.shouldDisableAlarms) {
175178
await _disableAlarmsBestEffort();
176179
}
177180
emit(const AlarmGateState.dismissed());
@@ -183,13 +186,6 @@ class AlarmGateCubit extends Cubit<AlarmGateState> {
183186
capabilities.nativeAlarmProvider == AlarmProvider.iosAlarmKit;
184187
}
185188

186-
Future<bool> _hasGrantedFallbackPermission(
187-
AlarmSchedulerCapabilities capabilities,
188-
) async {
189-
return await _checkFallbackPermission(capabilities) ==
190-
AlarmPermissionState.granted;
191-
}
192-
193189
Future<void> _enableAlarmsBestEffort() async {
194190
try {
195191
await _alarmRepository.updateAlarmSettings(alarmsEnabled: true);
@@ -212,3 +208,25 @@ class AlarmGateCubit extends Cubit<AlarmGateState> {
212208
}
213209
}
214210
}
211+
212+
class _EvaluatedAlarmDelivery {
213+
const _EvaluatedAlarmDelivery({
214+
required this.policy,
215+
required this.nativePermission,
216+
required this.fallbackPermission,
217+
});
218+
219+
final AlarmDeliveryPolicy policy;
220+
final AlarmPermissionState nativePermission;
221+
final AlarmPermissionState fallbackPermission;
222+
223+
AlarmPermissionState get permissionState {
224+
if (policy.canDeliver) return AlarmPermissionState.granted;
225+
if (policy.isUnsupported) return AlarmPermissionState.unsupported;
226+
if (policy.blockingPermissionIssue ==
227+
AlarmPermissionIssue.notificationPermissionDenied) {
228+
return fallbackPermission;
229+
}
230+
return nativePermission;
231+
}
232+
}

0 commit comments

Comments
 (0)