From 50b08401d0f24b119f0cd1ff0e80e75921198538 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 10 Nov 2025 03:52:35 +0000 Subject: [PATCH] fix(android): resolve MainActivity ClassNotFoundException and add exact alarm permission check - Fix MainActivity ClassNotFoundException by using dynamic package launcher intent - Replace hardcoded MainActivity class references with getLaunchIntent() helper - Uses packageManager.getLaunchIntentForPackage() to work with any host app - Removes dependency on specific MainActivity package/class name - Fixes 3 occurrences in NotifyReceiver.kt (alarm clock, notification click, reminder click) - Add exact alarm permission check before scheduling (Android 12+) - Add canScheduleExactAlarms() helper to check SCHEDULE_EXACT_ALARM permission - Check permission before scheduling exact alarms in scheduleExactNotification() - Gracefully fall back to inexact alarms when permission not granted - Prevents SecurityException and provides clear logging - Bump version to 1.0.2 Fixes: - ClassNotFoundException when plugin tries to resolve hardcoded MainActivity path - SecurityException on Android 12+ when exact alarm permission not granted - Plugin now works with any host app regardless of MainActivity package/class All changes maintain backward compatibility and improve reliability. --- .../dailynotification/NotifyReceiver.kt | 89 ++++++++++++------- package.json | 2 +- 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt b/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt index 866354c..b2ce9c0 100644 --- a/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt +++ b/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt @@ -37,6 +37,43 @@ class NotifyReceiver : BroadcastReceiver() { return (triggerAtMillis and 0xFFFF).toInt() } + /** + * Get launch intent for the host app + * Uses package launcher intent to avoid hardcoding MainActivity class name + * This works across all host apps regardless of their MainActivity package/class + * + * @param context Application context + * @return Intent to launch the app, or null if not available + */ + private fun getLaunchIntent(context: Context): Intent? { + return try { + // Use package launcher intent - works for any host app + context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + } catch (e: Exception) { + Log.w(TAG, "Failed to get launch intent for package: ${context.packageName}", e) + null + } + } + + /** + * Check if exact alarm permission is granted + * On Android 12+ (API 31+), SCHEDULE_EXACT_ALARM must be granted at runtime + * + * @param context Application context + * @return true if exact alarms can be scheduled, false otherwise + */ + private fun canScheduleExactAlarms(context: Context): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager + alarmManager?.canScheduleExactAlarms() ?: false + } else { + // Pre-Android 12: exact alarms are always allowed + true + } + } + /** * Schedule an exact notification using AlarmManager * Uses setAlarmClock() for Android 5.0+ for better reliability @@ -85,21 +122,27 @@ class NotifyReceiver : BroadcastReceiver() { Log.i(TAG, "Scheduling alarm: triggerTime=$triggerTimeStr, delayMs=$delayMs, requestCode=$requestCode") + // Check exact alarm permission before scheduling (Android 12+) + val canScheduleExact = canScheduleExactAlarms(context) + if (!canScheduleExact && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Log.w(TAG, "Exact alarm permission not granted. Cannot schedule exact alarm. User must grant SCHEDULE_EXACT_ALARM permission in settings.") + // Fall back to inexact alarm + alarmManager.set( + AlarmManager.RTC_WAKEUP, + triggerAtMillis, + pendingIntent + ) + Log.i(TAG, "Inexact alarm scheduled (exact permission denied): triggerAt=$triggerAtMillis, requestCode=$requestCode") + return + } + try { // Use setAlarmClock() for Android 5.0+ (API 21+) - most reliable method // Shows alarm icon in status bar and is exempt from doze mode if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // Create show intent for alarm clock (opens app when alarm fires) - val showIntent = try { - Intent(context, Class.forName("com.timesafari.dailynotification.MainActivity")).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - } catch (e: ClassNotFoundException) { - Log.w(TAG, "MainActivity not found, using package launcher", e) - context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - } + // Use package launcher intent to avoid hardcoding MainActivity class name + val showIntent = getLaunchIntent(context) val showPendingIntent = if (showIntent != null) { PendingIntent.getActivity( @@ -359,17 +402,8 @@ class NotifyReceiver : BroadcastReceiver() { } // Create intent to launch app when notification is clicked - val intent = try { - Intent(context, Class.forName("com.timesafari.dailynotification.MainActivity")).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - } catch (e: ClassNotFoundException) { - Log.w(TAG, "MainActivity not found, using package launcher", e) - // Fallback: launch app by package name - context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } ?: return - } + // Use package launcher intent to avoid hardcoding MainActivity class name + val intent = getLaunchIntent(context) ?: return val pendingIntent = PendingIntent.getActivity( context, 0, @@ -478,17 +512,8 @@ class NotifyReceiver : BroadcastReceiver() { createReminderNotificationChannel(context, notificationManager) // Create intent to launch app when notification is clicked - val intent = try { - Intent(context, Class.forName("com.timesafari.dailynotification.MainActivity")).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - } catch (e: ClassNotFoundException) { - Log.w(TAG, "MainActivity not found, using package launcher", e) - // Fallback: launch app by package name - context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } ?: return - } + // Use package launcher intent to avoid hardcoding MainActivity class name + val intent = getLaunchIntent(context) ?: return val pendingIntent = PendingIntent.getActivity( context, reminderId.hashCode(), diff --git a/package.json b/package.json index 98a7815..8088930 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@timesafari/daily-notification-plugin", - "version": "1.0.1", + "version": "1.0.2", "description": "TimeSafari Daily Notification Plugin - Enterprise-grade daily notification functionality with dual scheduling, callback support, TTL-at-fire logic, and comprehensive observability across Mobile (Capacitor) and Desktop (Electron) platforms", "main": "dist/plugin.js", "module": "dist/esm/index.js",