WIP: Change DailyNotificationReceiver to exported=true for AlarmManager broadcasts
The DailyNotificationReceiver was not being triggered when scheduled alarms fired, preventing notifications from appearing at the scheduled time. Changed android:exported from "false" to "true" to allow AlarmManager broadcasts to reach the receiver, especially when the app is closed or the device is in doze mode. This is a work-in-progress change to diagnose why notifications aren't firing. The receiver should log "DN|RECEIVE_START" when triggered, but we were not seeing these logs even when alarms were scheduled. Next steps: - Test if receiver is now triggered when alarm fires - Verify notifications appear at scheduled time - Consider adding permission check if keeping exported=true for security
This commit is contained in:
@@ -44,15 +44,18 @@
|
||||
</activity>
|
||||
|
||||
<!-- Daily Notification Plugin Receivers (must be inside application) -->
|
||||
<!-- DailyNotificationReceiver: Handles alarm-triggered notifications -->
|
||||
<!-- Note: exported="true" allows AlarmManager to trigger this receiver -->
|
||||
<receiver
|
||||
android:name="com.timesafari.dailynotification.DailyNotificationReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.timesafari.daily.NOTIFICATION" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- NotifyReceiver: Handles notification delivery -->
|
||||
<receiver
|
||||
android:name="com.timesafari.dailynotification.NotifyReceiver"
|
||||
android:enabled="true"
|
||||
|
||||
@@ -68,8 +68,8 @@ public class MainActivity extends BridgeActivity {
|
||||
registerPlugin(SharedImagePlugin.class);
|
||||
|
||||
// Register DailyNotification plugin
|
||||
// TODO: Uncomment when DailyNotificationPlugin Java class is implemented
|
||||
// registerPlugin(com.timesafari.dailynotification.DailyNotificationPlugin.class);
|
||||
// Plugin is written in Kotlin but compiles to Java-compatible bytecode
|
||||
registerPlugin(com.timesafari.dailynotification.DailyNotificationPlugin.class);
|
||||
|
||||
// Initialize SQLite
|
||||
//registerPlugin(SQLite.class);
|
||||
|
||||
@@ -689,6 +689,30 @@ export default class PushNotificationPermission extends Vue {
|
||||
"[PushNotificationPermission] Starting native notification setup",
|
||||
);
|
||||
|
||||
// Import and check plugin availability before using service
|
||||
const { DailyNotification } = await import(
|
||||
"@/plugins/DailyNotificationPlugin"
|
||||
);
|
||||
if (!DailyNotification) {
|
||||
logger.error(
|
||||
"[PushNotificationPermission] DailyNotification plugin not available",
|
||||
);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: NOTIFY_PUSH_SETUP_ERROR.title,
|
||||
text: "DailyNotification plugin is not available. Please rebuild the app.",
|
||||
},
|
||||
PUSH_NOTIFICATION_TIMEOUT_SHORT,
|
||||
);
|
||||
this.callback(false, "", this.messageInput);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
"[PushNotificationPermission] Plugin available, getting service instance",
|
||||
);
|
||||
const service = NotificationService.getInstance();
|
||||
|
||||
// Request permissions
|
||||
@@ -741,6 +765,31 @@ export default class PushNotificationPermission extends Vue {
|
||||
},
|
||||
);
|
||||
|
||||
// Check permissions one more time before scheduling
|
||||
const finalPermissionCheck = await service.checkPermissions();
|
||||
logger.debug(
|
||||
"[PushNotificationPermission] Final permission check before scheduling:",
|
||||
JSON.stringify(finalPermissionCheck, null, 2),
|
||||
);
|
||||
|
||||
if (!finalPermissionCheck.granted) {
|
||||
logger.warn(
|
||||
"[PushNotificationPermission] Permissions not fully granted. " +
|
||||
"Notification may not fire. Details:",
|
||||
finalPermissionCheck.details,
|
||||
);
|
||||
// On Android 12+, exact alarms might need to be enabled in system settings
|
||||
if (
|
||||
!finalPermissionCheck.details.exactAlarm &&
|
||||
Capacitor.getPlatform() === "android"
|
||||
) {
|
||||
logger.warn(
|
||||
"[PushNotificationPermission] Exact alarm permission not granted. " +
|
||||
"User may need to enable 'Exact alarms' in Android system settings.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const success = await service.scheduleDailyNotification({
|
||||
time: time24h,
|
||||
title,
|
||||
@@ -765,6 +814,13 @@ export default class PushNotificationPermission extends Vue {
|
||||
return;
|
||||
}
|
||||
|
||||
// Log final status after scheduling
|
||||
const statusAfterSchedule = await service.getStatus();
|
||||
logger.info(
|
||||
"[PushNotificationPermission] Notification status after scheduling:",
|
||||
JSON.stringify(statusAfterSchedule, null, 2),
|
||||
);
|
||||
|
||||
// Save to settings
|
||||
const timeText = this.notificationTimeText;
|
||||
const settingsToSave: Record<string, string> = {};
|
||||
@@ -784,13 +840,22 @@ export default class PushNotificationPermission extends Vue {
|
||||
settingsToSave,
|
||||
);
|
||||
|
||||
// Show success message
|
||||
// Check if exact alarm permission is needed (Android 12+)
|
||||
const needsExactAlarmWarning =
|
||||
Capacitor.getPlatform() === "android" &&
|
||||
!finalPermissionCheck.details.exactAlarm;
|
||||
|
||||
// Show success message with optional warning
|
||||
const successMessage = needsExactAlarmWarning
|
||||
? `${NOTIFY_PUSH_SUCCESS.message} Note: If notifications don't appear, enable "Exact alarms" in Android Settings → Apps → TimeSafari → App settings.`
|
||||
: NOTIFY_PUSH_SUCCESS.message;
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: NOTIFY_PUSH_SUCCESS.title,
|
||||
text: NOTIFY_PUSH_SUCCESS.message,
|
||||
text: successMessage,
|
||||
},
|
||||
PUSH_NOTIFICATION_TIMEOUT_LONG,
|
||||
);
|
||||
@@ -802,6 +867,14 @@ export default class PushNotificationPermission extends Vue {
|
||||
"[PushNotificationPermission] Error in native notification setup:",
|
||||
error,
|
||||
);
|
||||
// Log additional error details for debugging
|
||||
if (error instanceof Error) {
|
||||
logger.error("[PushNotificationPermission] Error details:", {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name,
|
||||
});
|
||||
}
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -50,6 +50,10 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request notification permissions from the OS
|
||||
* Shows native permission dialog on first call
|
||||
*/
|
||||
/**
|
||||
* Request notification permissions from the OS
|
||||
* Shows native permission dialog on first call
|
||||
@@ -58,31 +62,132 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
||||
try {
|
||||
logger.debug("[NativeNotificationService] Requesting permissions");
|
||||
|
||||
// Check if plugin is available
|
||||
if (!DailyNotification) {
|
||||
logger.error(
|
||||
"[NativeNotificationService] DailyNotification plugin is not available. " +
|
||||
"Make sure the plugin is registered in MainActivity.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the method exists
|
||||
if (
|
||||
typeof DailyNotification.requestNotificationPermissions !== "function"
|
||||
) {
|
||||
logger.error(
|
||||
"[NativeNotificationService] requestNotificationPermissions method not found on plugin. " +
|
||||
"Available methods: " +
|
||||
Object.keys(DailyNotification).join(", "),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
"[NativeNotificationService] Calling requestNotificationPermissions...",
|
||||
);
|
||||
|
||||
// Use requestNotificationPermissions() which is the method exposed by iOS
|
||||
// For Android, this should also work via the plugin bridge
|
||||
const result = await DailyNotification.requestNotificationPermissions();
|
||||
|
||||
logger.debug("[NativeNotificationService] Permission result:", result);
|
||||
logger.debug(
|
||||
"[NativeNotificationService] Permission result:",
|
||||
JSON.stringify(result, null, 2),
|
||||
);
|
||||
logger.debug(
|
||||
"[NativeNotificationService] Result type:",
|
||||
typeof result,
|
||||
"Keys:",
|
||||
result ? Object.keys(result) : "null",
|
||||
);
|
||||
|
||||
// The result may be PermissionStatus (with granted boolean) or PermissionStatusResult
|
||||
// Handle both formats for compatibility
|
||||
if ("granted" in result && typeof result.granted === "boolean") {
|
||||
// The result is PermissionStatus which has:
|
||||
// - granted?: boolean
|
||||
// - notifications: PermissionState ("granted" | "denied" | "prompt")
|
||||
// Handle all possible formats for compatibility
|
||||
if (
|
||||
result &&
|
||||
"granted" in result &&
|
||||
typeof result.granted === "boolean"
|
||||
) {
|
||||
logger.debug(
|
||||
"[NativeNotificationService] Using 'granted' field:",
|
||||
result.granted,
|
||||
);
|
||||
return result.granted;
|
||||
}
|
||||
|
||||
// Check the notifications PermissionState field
|
||||
if (result && "notifications" in result) {
|
||||
const notificationsState = result.notifications;
|
||||
logger.debug(
|
||||
"[NativeNotificationService] Notifications state:",
|
||||
notificationsState,
|
||||
);
|
||||
// PermissionState can be "granted", "denied", or "prompt"
|
||||
if (notificationsState === "granted") {
|
||||
return true;
|
||||
}
|
||||
if (notificationsState === "denied") {
|
||||
return false;
|
||||
}
|
||||
// If "prompt", the user hasn't decided yet, so return false
|
||||
if (notificationsState === "prompt") {
|
||||
logger.warn(
|
||||
"[NativeNotificationService] Permission still in prompt state after request",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for PermissionStatusResult format (from checkPermissionStatus)
|
||||
if (
|
||||
result &&
|
||||
"allPermissionsGranted" in result &&
|
||||
typeof result.allPermissionsGranted === "boolean"
|
||||
) {
|
||||
logger.debug(
|
||||
"[NativeNotificationService] Using 'allPermissionsGranted' field:",
|
||||
result.allPermissionsGranted,
|
||||
);
|
||||
return result.allPermissionsGranted;
|
||||
}
|
||||
|
||||
// If result is a boolean directly (some plugin versions might return this)
|
||||
if (typeof result === "boolean") {
|
||||
logger.debug("[NativeNotificationService] Result is boolean:", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fallback: check status after requesting
|
||||
logger.debug(
|
||||
"[NativeNotificationService] Falling back to checkPermissionStatus...",
|
||||
);
|
||||
const status = await DailyNotification.checkPermissionStatus();
|
||||
logger.debug(
|
||||
"[NativeNotificationService] Status check result:",
|
||||
JSON.stringify(status, null, 2),
|
||||
);
|
||||
return status.allPermissionsGranted;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[NativeNotificationService] Permission request failed:",
|
||||
error,
|
||||
);
|
||||
// Log additional error details for debugging
|
||||
if (error instanceof Error) {
|
||||
logger.error("[NativeNotificationService] Error details:", {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name,
|
||||
});
|
||||
} else {
|
||||
logger.error(
|
||||
"[NativeNotificationService] Non-Error exception:",
|
||||
JSON.stringify(error, null, 2),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -94,8 +199,13 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
||||
try {
|
||||
const status = await DailyNotification.checkPermissionStatus();
|
||||
|
||||
// Calculate granted status from individual permissions
|
||||
const allGranted =
|
||||
status.allPermissionsGranted ||
|
||||
(status.notificationsEnabled && status.exactAlarmEnabled);
|
||||
|
||||
return {
|
||||
granted: status.allPermissionsGranted,
|
||||
granted: allGranted,
|
||||
details: {
|
||||
notifications: status.notificationsEnabled,
|
||||
exactAlarm: status.exactAlarmEnabled,
|
||||
@@ -134,6 +244,41 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
||||
},
|
||||
);
|
||||
|
||||
// Note: The notification channel should be created automatically by the plugin
|
||||
// when the notification is delivered. The "Channel does not exist" warning
|
||||
// during permission checks is expected and not a problem - the channel will
|
||||
// be created when the receiver tries to show the notification.
|
||||
|
||||
// Check permissions before scheduling to ensure everything is set up correctly
|
||||
logger.debug(
|
||||
"[NativeNotificationService] Checking permissions before scheduling",
|
||||
);
|
||||
const permissionStatus = await this.checkPermissions();
|
||||
logger.info(
|
||||
"[NativeNotificationService] Permission status:",
|
||||
JSON.stringify(permissionStatus, null, 2),
|
||||
);
|
||||
|
||||
// Check if permissions are actually granted (all details must be true)
|
||||
const allPermissionsGranted =
|
||||
permissionStatus.details?.notifications &&
|
||||
permissionStatus.details?.exactAlarm &&
|
||||
permissionStatus.details?.backgroundRefresh;
|
||||
|
||||
if (!allPermissionsGranted) {
|
||||
logger.warn(
|
||||
"[NativeNotificationService] Permissions not fully granted. Details:",
|
||||
permissionStatus.details,
|
||||
);
|
||||
// Continue anyway - the plugin might handle permission requests internally
|
||||
// but log a warning for debugging
|
||||
} else {
|
||||
logger.debug(
|
||||
"[NativeNotificationService] All permissions granted:",
|
||||
permissionStatus.details,
|
||||
);
|
||||
}
|
||||
|
||||
// Cancel any existing notification with the same ID before scheduling a new one
|
||||
// This ensures the old notification is removed from iOS notification center
|
||||
try {
|
||||
@@ -155,6 +300,48 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
||||
);
|
||||
}
|
||||
|
||||
// Log current time and scheduled time for debugging
|
||||
const now = new Date();
|
||||
const [hours, minutes] = options.time.split(":").map(Number);
|
||||
const scheduledTime = new Date();
|
||||
scheduledTime.setHours(hours, minutes, 0, 0);
|
||||
|
||||
// If scheduled time is in the past, it should be scheduled for tomorrow
|
||||
if (scheduledTime < now) {
|
||||
scheduledTime.setDate(scheduledTime.getDate() + 1);
|
||||
logger.info(
|
||||
"[NativeNotificationService] Scheduled time is in the past, will schedule for tomorrow:",
|
||||
{
|
||||
requestedTime: options.time,
|
||||
currentTime: now.toISOString(),
|
||||
scheduledFor: scheduledTime.toISOString(),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
logger.info("[NativeNotificationService] Scheduling notification:", {
|
||||
requestedTime: options.time,
|
||||
currentTime: now.toISOString(),
|
||||
scheduledFor: scheduledTime.toISOString(),
|
||||
minutesUntilNotification: Math.round(
|
||||
(scheduledTime.getTime() - now.getTime()) / 1000 / 60,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
"[NativeNotificationService] Calling scheduleDailyReminder with options:",
|
||||
{
|
||||
id: this.reminderId,
|
||||
title: options.title,
|
||||
body: options.body,
|
||||
time: options.time,
|
||||
repeatDaily: true,
|
||||
sound: true,
|
||||
vibration: true,
|
||||
priority: options.priority || "normal",
|
||||
},
|
||||
);
|
||||
|
||||
await DailyNotification.scheduleDailyReminder({
|
||||
id: this.reminderId,
|
||||
title: options.title,
|
||||
@@ -166,9 +353,20 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
||||
priority: options.priority || "normal",
|
||||
});
|
||||
|
||||
// Verify the notification was actually scheduled
|
||||
logger.info(
|
||||
"[NativeNotificationService] scheduleDailyReminder call completed successfully",
|
||||
{
|
||||
reminderId: this.reminderId,
|
||||
requestedTime: options.time,
|
||||
},
|
||||
);
|
||||
|
||||
// Verify the notification was actually scheduled (if method is available)
|
||||
// Note: getScheduledReminders() is not implemented on Android, so we
|
||||
// only verify on iOS. On Android, we assume success if no error was thrown.
|
||||
try {
|
||||
logger.debug(
|
||||
"[NativeNotificationService] Verifying notification was scheduled",
|
||||
"[NativeNotificationService] Attempting to verify notification was scheduled",
|
||||
);
|
||||
const remindersResult = await DailyNotification.getScheduledReminders();
|
||||
// Handle both array and object with reminders property
|
||||
@@ -176,7 +374,9 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
||||
? remindersResult
|
||||
: (remindersResult as { reminders: typeof remindersResult })
|
||||
.reminders || [];
|
||||
const scheduledReminder = reminders.find((r) => r.id === this.reminderId);
|
||||
const scheduledReminder = reminders.find(
|
||||
(r) => r.id === this.reminderId,
|
||||
);
|
||||
|
||||
if (scheduledReminder && scheduledReminder.isScheduled) {
|
||||
// Verify the time matches what we scheduled
|
||||
@@ -193,7 +393,7 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"[NativeNotificationService] Daily notification scheduled successfully:",
|
||||
"[NativeNotificationService] Daily notification verified as scheduled:",
|
||||
{
|
||||
id: scheduledReminder.id,
|
||||
time: scheduledReminder.time,
|
||||
@@ -215,8 +415,30 @@ export class NativeNotificationService implements NotificationServiceInterface {
|
||||
})),
|
||||
},
|
||||
);
|
||||
// On iOS, if verification fails, return false
|
||||
// On Android, this method isn't available, so we'll fall through to return true
|
||||
return false;
|
||||
}
|
||||
} catch (verifyError) {
|
||||
// If getScheduledReminders() is not implemented (Android), assume success
|
||||
// since scheduleDailyReminder() completed without error
|
||||
if (
|
||||
verifyError instanceof Error &&
|
||||
verifyError.message.includes("not implemented")
|
||||
) {
|
||||
logger.debug(
|
||||
"[NativeNotificationService] getScheduledReminders() not available on this platform (expected on Android). " +
|
||||
"Assuming success since scheduleDailyReminder() completed without error.",
|
||||
);
|
||||
return true;
|
||||
}
|
||||
// For other errors, log but still assume success since the schedule call succeeded
|
||||
logger.warn(
|
||||
"[NativeNotificationService] Verification failed, but schedule call succeeded:",
|
||||
verifyError,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("[NativeNotificationService] Schedule failed:", error);
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user