diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/ChannelManager.java b/android/plugin/src/main/java/com/timesafari/dailynotification/ChannelManager.java new file mode 100644 index 0000000..8515828 --- /dev/null +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/ChannelManager.java @@ -0,0 +1,193 @@ +package com.timesafari.dailynotification; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.provider.Settings; +import android.util.Log; + +/** + * Manages notification channels and ensures they are properly configured + * for reliable notification delivery. + * + * Handles channel creation, importance checking, and provides deep links + * to channel settings when notifications are blocked. + * + * @author Matthew Raymer + * @version 1.0 + */ +public class ChannelManager { + private static final String TAG = "ChannelManager"; + private static final String DEFAULT_CHANNEL_ID = "daily_default"; + private static final String DEFAULT_CHANNEL_NAME = "Daily Notifications"; + private static final String DEFAULT_CHANNEL_DESCRIPTION = "Daily notifications from TimeSafari"; + + private final Context context; + private final NotificationManager notificationManager; + + public ChannelManager(Context context) { + this.context = context; + this.notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + } + + /** + * Ensures the default notification channel exists and is properly configured. + * Creates the channel if it doesn't exist. + * + * @return true if channel is ready for notifications, false if blocked + */ + public boolean ensureChannelExists() { + try { + Log.d(TAG, "Ensuring notification channel exists"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = notificationManager.getNotificationChannel(DEFAULT_CHANNEL_ID); + + if (channel == null) { + Log.d(TAG, "Creating notification channel"); + createDefaultChannel(); + return true; + } else { + Log.d(TAG, "Channel exists with importance: " + channel.getImportance()); + return channel.getImportance() != NotificationManager.IMPORTANCE_NONE; + } + } else { + // Pre-Oreo: channels don't exist, always ready + Log.d(TAG, "Pre-Oreo device, channels not applicable"); + return true; + } + } catch (Exception e) { + Log.e(TAG, "Error ensuring channel exists", e); + return false; + } + } + + /** + * Checks if the notification channel is enabled and can deliver notifications. + * + * @return true if channel is enabled, false if blocked + */ + public boolean isChannelEnabled() { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = notificationManager.getNotificationChannel(DEFAULT_CHANNEL_ID); + if (channel == null) { + Log.w(TAG, "Channel does not exist"); + return false; + } + + int importance = channel.getImportance(); + Log.d(TAG, "Channel importance: " + importance); + return importance != NotificationManager.IMPORTANCE_NONE; + } else { + // Pre-Oreo: always enabled + return true; + } + } catch (Exception e) { + Log.e(TAG, "Error checking channel status", e); + return false; + } + } + + /** + * Gets the current channel importance level. + * + * @return importance level, or -1 if channel doesn't exist + */ + public int getChannelImportance() { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = notificationManager.getNotificationChannel(DEFAULT_CHANNEL_ID); + if (channel != null) { + return channel.getImportance(); + } + } + return -1; + } catch (Exception e) { + Log.e(TAG, "Error getting channel importance", e); + return -1; + } + } + + /** + * Opens the notification channel settings for the user to enable notifications. + * + * @return true if settings intent was launched, false otherwise + */ + public boolean openChannelSettings() { + try { + Log.d(TAG, "Opening channel settings"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName()) + .putExtra(Settings.EXTRA_CHANNEL_ID, DEFAULT_CHANNEL_ID) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + context.startActivity(intent); + Log.d(TAG, "Channel settings opened"); + return true; + } else { + Log.d(TAG, "Channel settings not available on pre-Oreo"); + return false; + } + } catch (Exception e) { + Log.e(TAG, "Error opening channel settings", e); + return false; + } + } + + /** + * Creates the default notification channel with high importance. + */ + private void createDefaultChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + DEFAULT_CHANNEL_ID, + DEFAULT_CHANNEL_NAME, + NotificationManager.IMPORTANCE_HIGH + ); + channel.setDescription(DEFAULT_CHANNEL_DESCRIPTION); + channel.enableLights(true); + channel.enableVibration(true); + channel.setShowBadge(true); + + notificationManager.createNotificationChannel(channel); + Log.d(TAG, "Default channel created with HIGH importance"); + } + } + + /** + * Gets the default channel ID for use in notifications. + * + * @return the default channel ID + */ + public String getDefaultChannelId() { + return DEFAULT_CHANNEL_ID; + } + + /** + * Logs the current channel status for debugging. + */ + public void logChannelStatus() { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = notificationManager.getNotificationChannel(DEFAULT_CHANNEL_ID); + if (channel != null) { + Log.i(TAG, "Channel Status - ID: " + channel.getId() + + ", Importance: " + channel.getImportance() + + ", Enabled: " + (channel.getImportance() != NotificationManager.IMPORTANCE_NONE)); + } else { + Log.w(TAG, "Channel does not exist"); + } + } else { + Log.i(TAG, "Pre-Oreo device, channels not applicable"); + } + } catch (Exception e) { + Log.e(TAG, "Error logging channel status", e); + } + } +} diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java index ee63725..ce6112f 100644 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java @@ -76,6 +76,7 @@ public class DailyNotificationPlugin extends Plugin { private DailyNotificationStorage storage; private DailyNotificationScheduler scheduler; private DailyNotificationFetcher fetcher; + private ChannelManager channelManager; // SQLite database components private DailyNotificationDatabase database; @@ -119,6 +120,13 @@ public class DailyNotificationPlugin extends Plugin { storage = new DailyNotificationStorage(getContext()); scheduler = new DailyNotificationScheduler(getContext(), alarmManager); fetcher = new DailyNotificationFetcher(getContext(), storage); + channelManager = new ChannelManager(getContext()); + + // Ensure notification channel exists and is properly configured + if (!channelManager.ensureChannelExists()) { + Log.w(TAG, "Notification channel is blocked - notifications will not appear"); + channelManager.logChannelStatus(); + } // Check if recovery is needed (app startup recovery) checkAndPerformRecovery(); @@ -1081,6 +1089,129 @@ public class DailyNotificationPlugin extends Plugin { } } + /** + * Check if notification channel is enabled + * + * @param call Plugin call + */ + @PluginMethod + public void isChannelEnabled(PluginCall call) { + try { + Log.d(TAG, "Checking channel status"); + ensureStorageInitialized(); + + boolean enabled = channelManager.isChannelEnabled(); + int importance = channelManager.getChannelImportance(); + + JSObject result = new JSObject(); + result.put("enabled", enabled); + result.put("importance", importance); + result.put("channelId", channelManager.getDefaultChannelId()); + + Log.d(TAG, "Channel status - enabled: " + enabled + ", importance: " + importance); + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error checking channel status", e); + call.reject("Error checking channel status: " + e.getMessage()); + } + } + + /** + * Open notification channel settings + * + * @param call Plugin call + */ + @PluginMethod + public void openChannelSettings(PluginCall call) { + try { + Log.d(TAG, "Opening channel settings"); + + boolean opened = channelManager.openChannelSettings(); + + JSObject result = new JSObject(); + result.put("opened", opened); + + if (opened) { + Log.d(TAG, "Channel settings opened successfully"); + } else { + Log.w(TAG, "Could not open channel settings"); + } + + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error opening channel settings", e); + call.reject("Error opening channel settings: " + e.getMessage()); + } + } + + /** + * Get comprehensive notification status including permissions and channel + * + * @param call Plugin call + */ + @PluginMethod + public void checkStatus(PluginCall call) { + try { + Log.d(TAG, "Checking comprehensive notification status"); + ensureStorageInitialized(); + + // Check POST_NOTIFICATIONS permission + boolean postNotificationsGranted = false; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + postNotificationsGranted = getContext().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) + == PackageManager.PERMISSION_GRANTED; + } else { + postNotificationsGranted = true; // Pre-Android 13, always granted + } + + // Check channel status + boolean channelEnabled = channelManager.isChannelEnabled(); + int channelImportance = channelManager.getChannelImportance(); + + // Check exact alarm permission + boolean exactAlarmsGranted = false; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + exactAlarmsGranted = alarmManager.canScheduleExactAlarms(); + } else { + exactAlarmsGranted = true; // Pre-Android 12, always granted + } + + // Check if we can schedule now + boolean canScheduleNow = postNotificationsGranted && channelEnabled && exactAlarmsGranted; + + // Get next scheduled notification time (if any) + long nextScheduledAt = -1; + try { + // This would need to be implemented to check actual scheduled alarms + // For now, return -1 to indicate unknown + } catch (Exception e) { + Log.w(TAG, "Could not determine next scheduled time", e); + } + + JSObject result = new JSObject(); + result.put("postNotificationsGranted", postNotificationsGranted); + result.put("channelEnabled", channelEnabled); + result.put("channelImportance", channelImportance); + result.put("exactAlarmsGranted", exactAlarmsGranted); + result.put("canScheduleNow", canScheduleNow); + result.put("nextScheduledAt", nextScheduledAt); + result.put("channelId", channelManager.getDefaultChannelId()); + + Log.i(TAG, "Status check - canSchedule: " + canScheduleNow + + ", postNotifications: " + postNotificationsGranted + + ", channelEnabled: " + channelEnabled + + ", exactAlarms: " + exactAlarmsGranted); + + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error checking status", e); + call.reject("Error checking status: " + e.getMessage()); + } + } + /** * Maintain rolling window (for testing or manual triggers) *