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:
@@ -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(),
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user