Skip to content

Commit 4a5b0f1

Browse files
authored
Merge pull request #575 from DevKor-github/feature/issue-529-android-notification-first
fix: align Android schedule notifications with ADR-0008
2 parents 8a0a3a4 + e023c51 commit 4a5b0f1

19 files changed

Lines changed: 385 additions & 254 deletions

android/app/src/main/AndroidManifest.xml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@
3838
android:launchMode="singleTask"
3939
android:taskAffinity="club.devkor.ontime.alarm"
4040
android:theme="@style/NormalTheme"
41-
android:showWhenLocked="true"
42-
android:turnScreenOn="true"
4341
android:excludeFromRecents="true"
4442
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
4543
android:hardwareAccelerated="true" />

android/app/src/main/kotlin/club/devkor/ontime/AlarmRingingActivity.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ class AlarmRingingActivity : Activity() {
6565
override fun onCreate(savedInstanceState: Bundle?) {
6666
super.onCreate(savedInstanceState)
6767
NativeLog.d(TAG, "AlarmRingingActivity onCreate ${NativeLog.summarizeIntent(intent)}")
68+
if (!NativeAlarmPolicy.isAndroidFullScreenAlarmApproved()) {
69+
NativeLog.d(TAG, "AlarmRingingActivity finishing: full-screen alarm approval is disabled")
70+
finish()
71+
return
72+
}
6873
configureWindow()
6974
capturePayload(intent)
7075
buildContent()

android/app/src/main/kotlin/club/devkor/ontime/MainActivity.kt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,15 @@ open class MainActivity : FlutterActivity() {
4545
methodChannel?.setMethodCallHandler { call, result ->
4646
when (call.method) {
4747
"getCapabilities" -> {
48+
val nativeAlarmApproved =
49+
NativeAlarmPolicy.isAndroidFullScreenAlarmApproved()
4850
val capabilities = mapOf(
49-
"supportsNativeAlarm" to true,
50-
"nativeAlarmProvider" to "androidAlarmManager",
51+
"supportsNativeAlarm" to nativeAlarmApproved,
52+
"nativeAlarmProvider" to if (nativeAlarmApproved) {
53+
"androidAlarmManager"
54+
} else {
55+
"none"
56+
},
5157
"fallbackProvider" to "localNotification",
5258
)
5359
NativeLog.d(TAG, "getCapabilities -> $capabilities")
@@ -84,6 +90,16 @@ open class MainActivity : FlutterActivity() {
8490
}
8591

8692
private fun scheduleNativeAlarm(call: MethodCall, result: MethodChannel.Result) {
93+
if (!NativeAlarmPolicy.isAndroidFullScreenAlarmApproved()) {
94+
NativeLog.w(TAG, "scheduleNativeAlarm blocked: full-screen alarm approval is disabled")
95+
result.error(
96+
"unsupported",
97+
"Android native alarms require full-screen alarm policy approval",
98+
null,
99+
)
100+
return
101+
}
102+
87103
val args = call.arguments as? Map<*, *>
88104
if (args == null) {
89105
NativeLog.w(TAG, "scheduleNativeAlarm invalid: missing args")
@@ -267,6 +283,7 @@ open class MainActivity : FlutterActivity() {
267283
}
268284

269285
private fun configureAlarmLaunchWindow(intent: Intent?) {
286+
if (!NativeAlarmPolicy.isAndroidFullScreenAlarmApproved()) return
270287
if (payloadFromIntent(intent) == null) return
271288
NativeLog.d(TAG, "configureAlarmLaunchWindow for alarm launch")
272289
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {

android/app/src/main/kotlin/club/devkor/ontime/NativeAlarmBootReceiver.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ class NativeAlarmBootReceiver : BroadcastReceiver() {
4040
}
4141

4242
private fun restorePersistedNativeAlarms(context: Context) {
43+
if (!NativeAlarmPolicy.isAndroidFullScreenAlarmApproved()) {
44+
NativeLog.d(TAG, "restorePersistedNativeAlarms skipped: full-screen alarm approval is disabled")
45+
return
46+
}
4347
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
4448
if (alarmManager == null) {
4549
NativeLog.w(TAG, "restorePersistedNativeAlarms skipped: AlarmManager unavailable")
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package club.devkor.ontime
2+
3+
object NativeAlarmPolicy {
4+
const val ANDROID_FULL_SCREEN_ALARM_APPROVED = false
5+
6+
fun isAndroidFullScreenAlarmApproved(): Boolean {
7+
return ANDROID_FULL_SCREEN_ALARM_APPROVED
8+
}
9+
}

android/app/src/main/kotlin/club/devkor/ontime/NativeAlarmReceiver.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@ class NativeAlarmReceiver : BroadcastReceiver() {
211211
}
212212

213213
private fun handleFireAlarm(context: Context, intent: Intent) {
214+
if (!NativeAlarmPolicy.isAndroidFullScreenAlarmApproved()) {
215+
NativeLog.d(TAG, "Native alarm broadcast ignored: full-screen alarm approval is disabled")
216+
return
217+
}
214218
val extras = payloadFromIntent(intent)
215219
val requestCode = intent.getIntExtra(
216220
"nativeAlarmId",

lib/core/services/notification_routing.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ bool isScheduleAlarmPayload(Map<dynamic, dynamic>? payload) {
2525
final type = payload['type']?.toString();
2626
final promptVariant = payload['promptVariant']?.toString();
2727
return type == 'schedule_alarm' ||
28-
payload['alarmLaunchPayloadVersion'] != null ||
2928
(promptVariant == 'alarm' && payload['scheduleId'] != null);
3029
}
3130

@@ -56,7 +55,8 @@ NotificationRouteTarget? notificationRouteForData(Map<dynamic, dynamic> data) {
5655
final type = data['type']?.toString();
5756
final scheduleId = data['scheduleId']?.toString();
5857

59-
if (type == 'schedule_alarm' && scheduleId != null) {
58+
if ((type == 'schedule_alarm' || type == 'schedule_notification') &&
59+
scheduleId != null) {
6060
return NotificationRouteTarget(
6161
'/scheduleStart',
6262
extra: Map<String, dynamic>.from(data),

lib/core/services/notification_service.dart

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -316,9 +316,9 @@ class NotificationService {
316316
?.createNotificationChannel(channel);
317317

318318
const alarmChannel = AndroidNotificationChannel(
319-
'scheduled_alarm_channel',
320-
'Scheduled alarms',
321-
description: 'Schedule preparation alarm fallback notifications.',
319+
'scheduled_notification_channel',
320+
'Schedule notifications',
321+
description: 'Schedule preparation notifications.',
322322
importance: Importance.max,
323323
);
324324

@@ -540,13 +540,12 @@ class NotificationService {
540540
scheduledDate: scheduledAt,
541541
notificationDetails: const NotificationDetails(
542542
android: AndroidNotificationDetails(
543-
'scheduled_alarm_channel',
544-
'Scheduled alarms',
545-
channelDescription:
546-
'Schedule preparation alarm fallback notifications.',
543+
'scheduled_notification_channel',
544+
'Schedule notifications',
545+
channelDescription: 'Schedule preparation notifications.',
547546
importance: Importance.max,
548547
priority: Priority.max,
549-
category: AndroidNotificationCategory.alarm,
548+
category: AndroidNotificationCategory.reminder,
550549
icon: '@mipmap/ic_launcher',
551550
playSound: true,
552551
enableVibration: true,

lib/domain/entities/alarm_entities.dart

Lines changed: 72 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,26 @@ import 'package:on_time_front/domain/entities/schedule_entity.dart';
33
import 'package:on_time_front/domain/entities/schedule_with_preparation_entity.dart';
44

55
const alarmDefaultOffset = Duration(minutes: 5);
6-
const alarmLaunchPayloadVersion = '7';
6+
const alarmLaunchPayloadVersion = '8';
77

8-
enum AlarmProvider {
9-
androidAlarmManager,
10-
iosAlarmKit,
11-
localNotification,
12-
none,
13-
}
8+
enum AlarmProvider { androidAlarmManager, iosAlarmKit, localNotification, none }
149

15-
enum AlarmPermissionState {
16-
granted,
17-
denied,
18-
notDetermined,
19-
unsupported,
10+
const androidFullScreenAlarmPolicyApproved = false;
11+
12+
bool nativeAlarmProviderAllowedByReleasePolicy(AlarmProvider provider) {
13+
switch (provider) {
14+
case AlarmProvider.iosAlarmKit:
15+
return true;
16+
case AlarmProvider.androidAlarmManager:
17+
return androidFullScreenAlarmPolicyApproved;
18+
case AlarmProvider.localNotification:
19+
case AlarmProvider.none:
20+
return false;
21+
}
2022
}
2123

24+
enum AlarmPermissionState { granted, denied, notDetermined, unsupported }
25+
2226
enum AlarmPermissionIssue {
2327
nativePermissionDenied,
2428
notificationPermissionDenied,
@@ -222,10 +226,10 @@ class AlarmSettings extends Equatable {
222226

223227
@override
224228
List<Object?> get props => [
225-
alarmsEnabled,
226-
defaultAlarmOffsetMinutes,
227-
updatedAt,
228-
];
229+
alarmsEnabled,
230+
defaultAlarmOffsetMinutes,
231+
updatedAt,
232+
];
229233
}
230234

231235
class AlarmDeviceInfo extends Equatable {
@@ -249,14 +253,14 @@ class AlarmDeviceInfo extends Equatable {
249253

250254
@override
251255
List<Object?> get props => [
252-
deviceId,
253-
platform,
254-
appVersion,
255-
osVersion,
256-
supportsNativeAlarm,
257-
nativeAlarmProvider,
258-
fallbackProvider,
259-
];
256+
deviceId,
257+
platform,
258+
appVersion,
259+
osVersion,
260+
supportsNativeAlarm,
261+
nativeAlarmProvider,
262+
fallbackProvider,
263+
];
260264
}
261265

262266
class AlarmSchedulerCapabilities extends Equatable {
@@ -278,10 +282,10 @@ class AlarmSchedulerCapabilities extends Equatable {
278282

279283
@override
280284
List<Object?> get props => [
281-
supportsNativeAlarm,
282-
nativeAlarmProvider,
283-
fallbackProvider,
284-
];
285+
supportsNativeAlarm,
286+
nativeAlarmProvider,
287+
fallbackProvider,
288+
];
285289
}
286290

287291
class ScheduledAlarmRecord extends Equatable {
@@ -333,28 +337,24 @@ class ScheduledAlarmRecord extends Equatable {
333337

334338
@override
335339
List<Object?> get props => [
336-
scheduleId,
337-
alarmTime,
338-
preparationStartTime,
339-
scheduleFingerprint,
340-
nativeAlarmId,
341-
fallbackNotificationId,
342-
provider,
343-
scheduleTitle,
344-
payload,
345-
];
340+
scheduleId,
341+
alarmTime,
342+
preparationStartTime,
343+
scheduleFingerprint,
344+
nativeAlarmId,
345+
fallbackNotificationId,
346+
provider,
347+
scheduleTitle,
348+
payload,
349+
];
346350
}
347351

348352
class AlarmFailure extends Equatable {
349353
final String? scheduleId;
350354
final AlarmFailureReason reason;
351355
final String? message;
352356

353-
const AlarmFailure({
354-
required this.reason,
355-
this.scheduleId,
356-
this.message,
357-
});
357+
const AlarmFailure({required this.reason, this.scheduleId, this.message});
358358

359359
@override
360360
List<Object?> get props => [scheduleId, reason, message];
@@ -391,18 +391,18 @@ class AlarmReconciliationResult extends Equatable {
391391

392392
@override
393393
List<Object?> get props => [
394-
status,
395-
permissionIssue,
396-
nativeAlarmProvider,
397-
fallbackProvider,
398-
armedScheduleIds,
399-
skippedScheduleCount,
400-
failures,
401-
scheduleWindowStart,
402-
scheduleWindowEnd,
403-
alarmCoverageStart,
404-
alarmCoverageEnd,
405-
];
394+
status,
395+
permissionIssue,
396+
nativeAlarmProvider,
397+
fallbackProvider,
398+
armedScheduleIds,
399+
skippedScheduleCount,
400+
failures,
401+
scheduleWindowStart,
402+
scheduleWindowEnd,
403+
alarmCoverageStart,
404+
alarmCoverageEnd,
405+
];
406406
}
407407

408408
class AlarmStatusReport extends Equatable {
@@ -440,21 +440,21 @@ class AlarmStatusReport extends Equatable {
440440

441441
@override
442442
List<Object?> get props => [
443-
deviceId,
444-
reconciledAt,
445-
scheduleWindowStart,
446-
scheduleWindowEnd,
447-
alarmCoverageStart,
448-
alarmCoverageEnd,
449-
status,
450-
permissionIssue,
451-
nativeAlarmProvider,
452-
fallbackProvider,
453-
armedScheduleCount,
454-
armedScheduleIds,
455-
skippedScheduleCount,
456-
failures,
457-
];
443+
deviceId,
444+
reconciledAt,
445+
scheduleWindowStart,
446+
scheduleWindowEnd,
447+
alarmCoverageStart,
448+
alarmCoverageEnd,
449+
status,
450+
permissionIssue,
451+
nativeAlarmProvider,
452+
fallbackProvider,
453+
armedScheduleCount,
454+
armedScheduleIds,
455+
skippedScheduleCount,
456+
failures,
457+
];
458458
}
459459

460460
bool isAlarmEligibleSchedule(ScheduleWithPreparationEntity schedule) {
@@ -501,14 +501,14 @@ ScheduledAlarmRecord buildScheduledAlarmRecord(
501501
provider: provider,
502502
scheduleTitle: schedule.scheduleName,
503503
payload: {
504-
'type': 'schedule_alarm',
504+
'type': 'schedule_notification',
505505
'alarmLaunchPayloadVersion': alarmLaunchPayloadVersion,
506506
'scheduleId': schedule.id,
507507
'alarmTime': alarmTime.toIso8601String(),
508508
'preparationStartTime': preparationStartTime.toIso8601String(),
509509
'scheduleFingerprint': buildAlarmScheduleFingerprint(schedule),
510510
'placeName': schedule.place.placeName,
511-
'promptVariant': 'alarm',
511+
'promptVariant': 'notification',
512512
},
513513
);
514514
}

lib/domain/use-cases/reconcile_alarms_use_case.dart

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -353,8 +353,7 @@ class ReconcileAlarmsUseCase {
353353
Future<AlarmPermissionState> _checkNativePermission(
354354
AlarmSchedulerCapabilities capabilities,
355355
) async {
356-
if (!capabilities.supportsNativeAlarm ||
357-
capabilities.nativeAlarmProvider == AlarmProvider.none) {
356+
if (!_canUseNativeProvider(capabilities)) {
358357
return AlarmPermissionState.unsupported;
359358
}
360359
return _schedulerService.checkPermission().catchError(
@@ -541,10 +540,18 @@ class ReconcileAlarmsUseCase {
541540
return capabilities.fallbackProvider == AlarmProvider.localNotification;
542541
}
543542
if (record.provider == AlarmProvider.none) return false;
544-
return capabilities.supportsNativeAlarm &&
543+
return _canUseNativeProvider(capabilities) &&
545544
record.provider == capabilities.nativeAlarmProvider;
546545
}
547546

547+
bool _canUseNativeProvider(AlarmSchedulerCapabilities capabilities) {
548+
return capabilities.supportsNativeAlarm &&
549+
capabilities.nativeAlarmProvider != AlarmProvider.none &&
550+
nativeAlarmProviderAllowedByReleasePolicy(
551+
capabilities.nativeAlarmProvider,
552+
);
553+
}
554+
548555
Future<void> _cancelRecords(List<ScheduledAlarmRecord> records) async {
549556
for (final record in records) {
550557
try {

0 commit comments

Comments
 (0)