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.
This commit is contained in:
Matthew Raymer
2025-11-10 03:52:35 +00:00
parent 37753bb051
commit 50b08401d0
2 changed files with 58 additions and 33 deletions

View File

@@ -37,6 +37,43 @@ class NotifyReceiver : BroadcastReceiver() {
return (triggerAtMillis and 0xFFFF).toInt() 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 * Schedule an exact notification using AlarmManager
* Uses setAlarmClock() for Android 5.0+ for better reliability * 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") 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 { try {
// Use setAlarmClock() for Android 5.0+ (API 21+) - most reliable method // Use setAlarmClock() for Android 5.0+ (API 21+) - most reliable method
// Shows alarm icon in status bar and is exempt from doze mode // Shows alarm icon in status bar and is exempt from doze mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Create show intent for alarm clock (opens app when alarm fires) // Create show intent for alarm clock (opens app when alarm fires)
val showIntent = try { // Use package launcher intent to avoid hardcoding MainActivity class name
Intent(context, Class.forName("com.timesafari.dailynotification.MainActivity")).apply { val showIntent = getLaunchIntent(context)
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
}
}
val showPendingIntent = if (showIntent != null) { val showPendingIntent = if (showIntent != null) {
PendingIntent.getActivity( PendingIntent.getActivity(
@@ -359,17 +402,8 @@ class NotifyReceiver : BroadcastReceiver() {
} }
// Create intent to launch app when notification is clicked // Create intent to launch app when notification is clicked
val intent = try { // Use package launcher intent to avoid hardcoding MainActivity class name
Intent(context, Class.forName("com.timesafari.dailynotification.MainActivity")).apply { val intent = getLaunchIntent(context) ?: return
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
}
val pendingIntent = PendingIntent.getActivity( val pendingIntent = PendingIntent.getActivity(
context, context,
0, 0,
@@ -478,17 +512,8 @@ class NotifyReceiver : BroadcastReceiver() {
createReminderNotificationChannel(context, notificationManager) createReminderNotificationChannel(context, notificationManager)
// Create intent to launch app when notification is clicked // Create intent to launch app when notification is clicked
val intent = try { // Use package launcher intent to avoid hardcoding MainActivity class name
Intent(context, Class.forName("com.timesafari.dailynotification.MainActivity")).apply { val intent = getLaunchIntent(context) ?: return
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
}
val pendingIntent = PendingIntent.getActivity( val pendingIntent = PendingIntent.getActivity(
context, context,
reminderId.hashCode(), reminderId.hashCode(),

View File

@@ -1,6 +1,6 @@
{ {
"name": "@timesafari/daily-notification-plugin", "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", "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", "main": "dist/plugin.js",
"module": "dist/esm/index.js", "module": "dist/esm/index.js",