Skip to content

Commit a99efca

Browse files
committed
fix: align Android schedule notifications with ADR-0008
1 parent eabe037 commit a99efca

19 files changed

Lines changed: 405 additions & 267 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
@@ -267,9 +267,9 @@ class NotificationService {
267267
?.createNotificationChannel(channel);
268268

269269
const alarmChannel = AndroidNotificationChannel(
270-
'scheduled_alarm_channel',
271-
'Scheduled alarms',
272-
description: 'Schedule preparation alarm fallback notifications.',
270+
'scheduled_notification_channel',
271+
'Schedule notifications',
272+
description: 'Schedule preparation notifications.',
273273
importance: Importance.max,
274274
);
275275

@@ -491,13 +491,12 @@ class NotificationService {
491491
scheduledDate: scheduledAt,
492492
notificationDetails: const NotificationDetails(
493493
android: AndroidNotificationDetails(
494-
'scheduled_alarm_channel',
495-
'Scheduled alarms',
496-
channelDescription:
497-
'Schedule preparation alarm fallback notifications.',
494+
'scheduled_notification_channel',
495+
'Schedule notifications',
496+
channelDescription: 'Schedule preparation notifications.',
498497
importance: Importance.max,
499498
priority: Priority.max,
500-
category: AndroidNotificationCategory.alarm,
499+
category: AndroidNotificationCategory.reminder,
501500
icon: '@mipmap/ic_launcher',
502501
playSound: true,
503502
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
}

0 commit comments

Comments
 (0)